CleanupMonster.psm1

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
 
    .PARAMETER ToFQDN
    Converts DistinguishedName to Fully Qualified Domain Name (FQDN)
    This will only work for very specific cases, and will not really convert all Distinguished Names to FQDN
 
    .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')]
        [Parameter(ParameterSetName = 'ToFQDN')]
        [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,
        [Parameter(ParameterSetName = 'ToFQDN')][switch] $ToFQDN
    )
    Process {
        foreach ($Distinguished in $DistinguishedName) {
            if ($ToDomainCN) {
                $DN = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1'
                $CN = $DN -replace ',DC=', '.' -replace "DC="
                if ($CN) {
                    $CN
                }
            } elseif ($ToOrganizationalUnit) {

                if ($Distinguished -match '^CN=[^,\\]+(?:\\,[^,\\]+)*,(.+)$') {

                    $matches[1]
                } elseif ($Distinguished -match '^(OU=|CN=)') {

                    $Distinguished
                }
            } elseif ($ToMultipleOrganizationalUnit) {

                $Parts = $Distinguished -split '(?<!\\),'
                $Results = [System.Collections.ArrayList]::new()

                if ($IncludeParent) {
                    $null = $Results.Add($Distinguished)
                }

                for ($i = 1; $i -lt $Parts.Count; $i++) {
                    $CurrentPath = $Parts[$i..($Parts.Count - 1)] -join ','
                    if ($CurrentPath -match '^(OU=|CN=)' -and $CurrentPath -notmatch '^DC=') {
                        $null = $Results.Add($CurrentPath)
                    }
                }

                foreach ($R in $Results) {
                    if ($R -match '^(OU=|CN=)') {
                        $R
                    }
                }
            } elseif ($ToDC) {

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

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

                if ($Distinguished -match '^CN=(.+?),(?:(?:OU|CN).+,)*((?:DC=.+,?)+)$') {
                    $cnPart = $matches[1] -replace '\\,', ',' 
                    $dcPart = $matches[2] -replace 'DC=', '' -replace ',', '.'
                    "$cnPart.$dcPart"
                } elseif ($Distinguished -match '^CN=(.+?),((?:DC=.+,?)+)$') {
                    $cnPart = $matches[1] -replace '\\,', ',' 
                    $dcPart = $matches[2] -replace 'DC=', '' -replace ',', '.'
                    "$cnPart.$dcPart"
                }
            } else {
                $Regex = '^CN=(?<cn>.+?)(?<!\\),(?<ou>(?:(?:OU|CN).+?(?<!\\),)+(?<dc>DC.+?))$'

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

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

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

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

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

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

        $Systems = @{

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

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

            '20348'        = 'Windows Server 2022'
            '19042'        = 'Windows Server 2019 20H2'
            '19041'        = 'Windows Server 2019 2004'
            '18363'        = 'Windows Server 2019 1909'
            '18362'        = "Windows Server 2019 1903" 
            '17763'        = "Windows Server 2019 1809" 
            '17134'        = "Windows Server 2016 1803" 
            '14393'        = "Windows Server 2016 1607"
            '9600'         = 'Windows Server 2012 R2'
            '7601'         = 'Windows Server 2008 R2'
            '3790'         = 'Windows Server 2003'
        }
        $System = $Systems[$OperatingSystemVersion]
        if (-not $System) {
            $System = $OperatingSystemVersion
        }
    } else {
        $System = $OperatingSystem
    }
    if ($System) {
        $System
    } else {
        'Unknown'
    }
}
function Get-GitHubVersion { 
    <#
    .SYNOPSIS
    Get the latest version of a GitHub repository and compare with local version
 
    .DESCRIPTION
    Get the latest version of a GitHub repository and compare with local version
 
    .PARAMETER Cmdlet
    Cmdlet to find module for
 
    .PARAMETER RepositoryOwner
    Repository owner
 
    .PARAMETER RepositoryName
    Repository name
 
    .EXAMPLE
    Get-GitHubVersion -Cmdlet 'Start-DelegationModel' -RepositoryOwner 'evotecit' -RepositoryName 'DelegationModel'
 
    .NOTES
    General notes
    #>

    [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)"
        }
    } else {
        "Current: Unknown"
    }
}
function Get-WinADForestDetails { 
    <#
    .SYNOPSIS
    Get details about Active Directory Forest, Domains and Domain Controllers in a single query
 
    .DESCRIPTION
    Get details about Active Directory Forest, Domains and Domain Controllers in a single query
 
    .PARAMETER Forest
    Target different Forest, by default current forest is used
 
    .PARAMETER ExcludeDomains
    Exclude domain from search, by default whole forest is scanned
 
    .PARAMETER IncludeDomains
    Include only specific domains, by default whole forest is scanned
 
    .PARAMETER ExcludeDomainControllers
    Exclude specific domain controllers, by default there are no exclusions, as long as VerifyDomainControllers switch is enabled. Otherwise this parameter is ignored.
 
    .PARAMETER IncludeDomainControllers
    Include only specific domain controllers, by default all domain controllers are included, as long as VerifyDomainControllers switch is enabled. Otherwise this parameter is ignored.
 
    .PARAMETER SkipRODC
    Skip Read-Only Domain Controllers. By default all domain controllers are included.
 
    .PARAMETER ExtendedForestInformation
    Ability to provide Forest Information from another command to speed up processing
 
    .PARAMETER Filter
    Filter for Get-ADDomainController
 
    .PARAMETER TestAvailability
    Check if Domain Controllers are available
 
    .PARAMETER Test
    Pick what to check for availability. Options are: All, Ping, WinRM, PortOpen, Ping+WinRM, Ping+PortOpen, WinRM+PortOpen. Default is All
 
    .PARAMETER Ports
    Ports to check for availability. Default is 135
 
    .PARAMETER PortsTimeout
    Ports timeout for availability check. Default is 100
 
    .PARAMETER PingCount
    How many pings to send. Default is 1
 
    .PARAMETER PreferWritable
    Prefer writable domain controllers over read-only ones when returning Query Servers
 
    .PARAMETER Extended
    Return extended information about domains with NETBIOS names
 
    .EXAMPLE
    Get-WinADForestDetails | Format-Table
 
    .EXAMPLE
    Get-WinADForestDetails -Forest 'ad.evotec.xyz' | Format-Table
 
    .NOTES
    General notes
    #>

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

    if (-not $ExtendedForestInformation) {

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

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

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

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

            $Domain
        }

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

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

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

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

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

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

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

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

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

        $Findings
    } else {

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

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

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

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

            [Array] $Findings['DomainDomainControllers'][$Domain]
        }
        $Findings
    }
}
function Get-WinADSIDHistory { 
    <#
    .SYNOPSIS
    Retrieves SID History information for objects in Active Directory forest.
 
    .DESCRIPTION
    This function collects and analyzes SID History information for all objects in the Active Directory forest.
    It provides detailed information about internal and external SID history values, including statistics about
    users, groups, and computers that have SID history attributes.
 
    .PARAMETER Forest
    The name of the Active Directory forest to analyze. If not specified, uses the current forest.
 
    .PARAMETER ExcludeDomains
    An array of domain names to exclude from the analysis.
 
    .PARAMETER IncludeDomains
    An array of domain names to include in the analysis. Also aliased as 'Domain' or 'Domains'.
 
    .PARAMETER ExtendedForestInformation
    A hashtable containing extended forest information. Usually provided by Get-WinADForestDetails.
 
    .PARAMETER All
    Switch to return all information including domain SIDs and statistics. If not specified, returns only object information.
 
    .EXAMPLE
    Get-WinADSIDHistory -Forest "contoso.com"
 
    Returns a list of all objects with SID history in the specified forest.
 
    .EXAMPLE
    Get-WinADSIDHistory -IncludeDomains "domain1.local","domain2.local" -All
 
    Returns detailed SID history information including statistics for specified domains.
 
    .EXAMPLE
    Get-WinADSIDHistory -ExcludeDomains "legacy.local" -All
 
    Returns detailed SID history information for all domains except the specified excluded domain.
 
    .NOTES
    The function returns:
    - Object details (Name, Domain, Enabled status, etc.)
    - SID History count and values
    - Internal vs External vs Unknown SID information
    - Domain translation for SID values
    - Statistics about object types and status
    #>

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

    $Script:Reporting = [ordered] @{}
    $Script:Reporting['Version'] = Get-GitHubVersion -Cmdlet 'Invoke-ADEssentials' -RepositoryOwner 'evotecit' -RepositoryName 'ADEssentials'

    # Lets get all information about the forest
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExcludeDomainControllers $ExcludeDomainControllers -IncludeDomainControllers $IncludeDomainControllers -SkipRODC:$SkipRODC -ExtendedForestInformation $ExtendedForestInformation -Extended

    # Lets create an output object
    $Output = [ordered] @{
        'All'           = [System.Collections.Generic.List[PSCustomObject]]::new()
        'DomainSIDs'    = $null
        'Statistics'    = [ordered] @{
            'TotalObjects'    = 0
            'TotalUsers'      = 0
            'TotalGroups'     = 0
            'TotalComputers'  = 0
            'EnabledObjects'  = 0
            'DisabledObjects' = 0
            'InternalSIDs'    = 0
            'ExternalSIDs'    = 0
            'UnknownSIDs'     = 0
        }
        'Trusts'        = [System.Collections.Generic.List[PSCustomObject]]::new()
        'DuplicateSIDs' = [System.Collections.Generic.List[PSCustomObject]]::new()
    }

    # Lets find out all trusts in the forest
    $Output['Trusts'] = Get-WinADTrust -Forest $Forest -Recursive -SkipValidation

    # Lets find out all SIDs that are in the forest and in trusts
    $DomainSIDs = [ordered]@{}
    $ForestDomainSIDs = [ordered]@{}
    $TrustDomainSIDs = [ordered]@{}

    # Add forest domains
    foreach ($Domain in $ForestInformation.DomainsExtended.Keys) {
        $SID = $ForestInformation.DomainsExtended[$Domain].DomainSID
        $DomainSIDs[$SID] = [PSCustomObject] @{
            Domain = $Domain
            Type   = 'Domain'
            SID    = $SID
        }
        $ForestDomainSIDs[$SID] = $Domain
    }

    # Add trusted domains
    foreach ($Trust in $Output['Trusts']) {
        if ($Trust.TrustTarget -in $ForestInformation.DomainsExtended.Keys) {
            continue
        }
        $SID = $Trust.DomainSID
        $DomainSIDs[$SID] = [PSCustomObject] @{
            Domain = $Trust.TrustTarget
            Type   = 'Trust'
            SID    = $SID
        }
        $TrustDomainSIDs[$SID] = $Trust.TrustTarget
    }

    # Lets get all objects with SIDHistory
    $AllUsers = foreach ($Domain in $ForestInformation.Domains) {
        $QueryServer = $ForestInformation['QueryServers'][$Domain].HostName[0]
        $getADObjectSplat = @{
            LDAPFilter = "(sidHistory=*)"
            Properties = 'sidHistory', 'userPrincipalName', 'WhenCreated', 'WhenChanged', 'userAccountControl', 'mail', 'sAMAccountName', 'lastLogonTimestamp', 'objectClass', 'distinguishedName', 'name', 'pwdLastSet'
            Server     = $QueryServer
        }

        $Objects = Get-ADObject @getADObjectSplat
        foreach ($Object in $Objects) {
            $SidDomains = [System.Collections.Generic.List[string]]::new()
            $SidHistoryValues = [System.Collections.Generic.List[string]]::new()
            $SidHistoryDomainsTranslated = [System.Collections.Generic.List[string]]::new()
            $SidHistoryInternal = [System.Collections.Generic.List[string]]::new()
            $SIDHistoryExternal = [System.Collections.Generic.List[string]]::new()
            $SIDHistoryUnknown = [System.Collections.Generic.List[string]]::new()

            foreach ($Sid in $Object.sidHistory) {
                $SidHistoryValues.Add($Sid.Value)
                $DomainSID = $Sid.AccountDomainSid.Value

                # Check if this is from an internal forest domain
                if ($ForestDomainSIDs.Contains($DomainSID)) {
                    $SIDHistoryInternal.Add($Sid.Value)
                    $Output['Statistics']['InternalSIDs']++
                }
                # Check if this is from a known trust
                elseif ($TrustDomainSIDs.Contains($DomainSID)) {
                    $SIDHistoryExternal.Add($Sid.Value)
                    $Output['Statistics']['ExternalSIDs']++
                }
                # Otherwise it's unknown
                else {
                    $SIDHistoryUnknown.Add($Sid.Value)
                    $Output['Statistics']['UnknownSIDs']++
                }

                if (-not $SidDomains.Contains($DomainSID)) {
                    $SidDomains.Add($DomainSID)
                }

                $DomainInternal = $DomainSIDs[$DomainSID].Domain
                if ($DomainInternal) {
                    if (-not $SidHistoryDomainsTranslated.Contains($DomainInternal)) {
                        $SidHistoryDomainsTranslated.Add($DomainInternal)
                    }
                } else {
                    if (-not $SidHistoryDomainsTranslated.Contains($DomainSID)) {
                        $SidHistoryDomainsTranslated.Add($DomainSID)
                    }
                }
            }

            $UAC = Convert-UserAccountControl -UserAccountControl $Object.UserAccountControl

            $LastLogonTime = if ($Object.lastLogonTimestamp) {
                [datetime]::FromFileTime($Object.lastLogonTimestamp)
            } else {
                $null
            }
            $LastLogonTimeDays = if ($LastLogonTime) {
                [math]::Round((New-TimeSpan -Start $LastLogonTime -End (Get-Date)).TotalDays, 0)
            } else {
                $null
            }

            $PasswordLastSet = if ($Object.pwdLastSet) {
                [datetime]::FromFileTime($Object.pwdLastSet)
            } else {
                $null
            }

            $PasswordLastSetDays = if ($PasswordLastSet) {
                [math]::Round((New-TimeSpan -Start $PasswordLastSet -End (Get-Date)).TotalDays, 0)
            } else {
                $null
            }

            $O = [PSCustomObject] @{
                Name               = $Object.Name
                UserPrincipalName  = $Object.UserPrincipalName
                Domain             = $Domain
                SamAccountName     = $Object.sAMAccountName
                ObjectClass        = $Object.ObjectClass
                Enabled            = if ($UAC -contains 'ACCOUNTDISABLE') {
                    $false 
                } else {
                    $true 
                }
                PasswordDays       = $PasswordLastSetDays
                LogonDays          = $LastLogonTimeDays
                Count              = $SidHistoryValues.Count
                SIDHistory         = $SidHistoryValues
                Domains            = $SidDomains
                DomainsExpanded    = $SidHistoryDomainsTranslated
                Internal           = $SIDHistoryInternal
                InternalCount      = $SIDHistoryInternal.Count
                External           = $SIDHistoryExternal
                ExternalCount      = $SIDHistoryExternal.Count
                Unknown            = $SIDHistoryUnknown
                UnknownCount       = $SIDHistoryUnknown.Count
                WhenCreated        = $Object.WhenCreated
                WhenChanged        = $Object.WhenChanged
                LastLogon          = $LastLogonTime
                PasswordLastSet    = $PasswordLastSet
                OrganizationalUnit = ConvertFrom-DistinguishedName -DistinguishedName $Object.DistinguishedName -ToOrganizationalUnit
                DistinguishedName  = $Object.DistinguishedName
            }
            if ($O.Enabled) {
                $Output['Statistics']['EnabledObjects']++
            } else {
                $Output['Statistics']['DisabledObjects']++
            }
            $Output['Statistics']['TotalObjects']++
            switch ($O.ObjectClass) {
                'user' {
                    $Output['Statistics']['TotalUsers']++
                }
                'group' {
                    $Output['Statistics']['TotalGroups']++
                }
                'computer' {
                    $Output['Statistics']['TotalComputers']++
                }
            }
            $O
            if ($All) {
                foreach ($Sid in $SidDomains) {
                    if (-not $Output[$Sid]) {
                        $Output[$Sid] = [System.Collections.Generic.List[PSCustomObject]]::new()
                    }
                    $Output[$Sid].Add($O)
                }
            }
        }
    }
    if ($All) {
        $Output['DomainSIDs'] = $DomainSIDs
        $Output['Statistics'] = $Output['Statistics']
        $Output['All'] = $AllUsers
        $Output
    } else {
        $AllUsers
    }
}
function Remove-EmptyValue { 
    <#
    .SYNOPSIS
    Removes empty values from a hashtable recursively.
 
    .DESCRIPTION
    This function removes empty values from a given hashtable. It can be used to clean up a hashtable by removing keys with null, empty string, empty array, or empty dictionary values. The function supports recursive removal of empty values.
 
    .PARAMETER Hashtable
    The hashtable from which empty values will be removed.
 
    .PARAMETER ExcludeParameter
    An array of keys to exclude from the removal process.
 
    .PARAMETER Recursive
    Indicates whether to recursively remove empty values from nested hashtables.
 
    .PARAMETER Rerun
    Specifies the number of times to rerun the removal process recursively.
 
    .PARAMETER DoNotRemoveNull
    If specified, null values will not be removed.
 
    .PARAMETER DoNotRemoveEmpty
    If specified, empty string values will not be removed.
 
    .PARAMETER DoNotRemoveEmptyArray
    If specified, empty array values will not be removed.
 
    .PARAMETER DoNotRemoveEmptyDictionary
    If specified, empty dictionary values will not be removed.
 
    .EXAMPLE
    $hashtable = @{
        'Key1' = '';
        'Key2' = $null;
        'Key3' = @();
        'Key4' = @{}
    }
    Remove-EmptyValue -Hashtable $hashtable -Recursive
 
    Description
    -----------
    This example removes empty values from the $hashtable recursively.
 
    .EXAMPLE
    $SplatDictionary = [ordered] @{
        Test = $NotExistingParameter
        Test1 = 'Existing Entry'
        Test2 = $null
        Test3 = ''
        Test5 = 0
        Test6 = 6
        Test7 = @{}
    }
 
    Remove-EmptyValue -Splat $SplatDictionary -Recursive -ExcludeParameter 'Test7'
 
    $SplatDictionary
 
    #>

    [alias('Remove-EmptyValues')]
    [CmdletBinding()]
    param(
        [alias('Splat', 'IDictionary')][Parameter(Mandatory)][System.Collections.IDictionary] $Hashtable,
        [string[]] $ExcludeParameter,
        [switch] $Recursive,
        [int] $Rerun,
        [switch] $DoNotRemoveNull,
        [switch] $DoNotRemoveEmpty,
        [switch] $DoNotRemoveEmptyArray,
        [switch] $DoNotRemoveEmptyDictionary
    )
    foreach ($Key in [string[]] $Hashtable.Keys) {
        if ($Key -notin $ExcludeParameter) {
            if ($Recursive) {
                if ($Hashtable[$Key] -is [System.Collections.IDictionary]) {
                    if ($Hashtable[$Key].Count -eq 0) {
                        if (-not $DoNotRemoveEmptyDictionary) {
                            $Hashtable.Remove($Key)
                        }
                    } else {
                        Remove-EmptyValue -Hashtable $Hashtable[$Key] -Recursive:$Recursive -ExcludeParameter $ExcludeParameter -DoNotRemoveNull:$DoNotRemoveNull -DoNotRemoveEmpty:$DoNotRemoveEmpty -DoNotRemoveEmptyArray:$DoNotRemoveEmptyArray -DoNotRemoveEmptyDictionary:$DoNotRemoveEmptyDictionary
                    }
                } else {
                    if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) {
                        $Hashtable.Remove($Key)
                    } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') {
                        $Hashtable.Remove($Key)
                    } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) {
                        $Hashtable.Remove($Key)
                    }
                }
            } else {
                if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) {
                    $Hashtable.Remove($Key)
                } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') {
                    $Hashtable.Remove($Key)
                } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) {
                    $Hashtable.Remove($Key)
                }
            }
        }
    }
    if ($Rerun) {
        for ($i = 0; $i -lt $Rerun; $i++) {
            Remove-EmptyValue -Hashtable $Hashtable -Recursive:$Recursive -ExcludeParameter $ExcludeParameter -DoNotRemoveNull:$DoNotRemoveNull -DoNotRemoveEmpty:$DoNotRemoveEmpty -DoNotRemoveEmptyArray:$DoNotRemoveEmptyArray -DoNotRemoveEmptyDictionary:$DoNotRemoveEmptyDictionary
        }
    }
}
function Set-LoggingCapabilities { 
    <#
    .SYNOPSIS
    Sets up logging capabilities by managing log files.
 
    .DESCRIPTION
    This function sets up logging capabilities by creating the necessary directories and managing the number of log files based on the specified maximum.
 
    .PARAMETER LogPath
    The path where the log files will be stored.
 
    .PARAMETER ScriptPath
    The path of the script that generates the logs.
 
    .PARAMETER LogMaximum
    The maximum number of log files to keep. Older files will be deleted if this limit is exceeded.
 
    .PARAMETER ShowTime
    Switch to include timestamps in the log entries.
 
    .PARAMETER TimeFormat
    The format of the timestamps in the log entries.
 
    .PARAMETER ParameterPSDefaultParameterValues
    The hashtable of default parameter values for the Write-Color function.
    If this parameter is not provided, the function will create a new hashtable.
    This will only work properly if the function is nested as private function in another module.
    It's advised to provide the hashtable from the parent function for this to work always.
 
    .EXAMPLE
    Set-LoggingCapabilities -LogPath "C:\Logs\log.log" -ScriptPath "C:\Scripts\script.ps1" -LogMaximum 10 -ShowTime -TimeFormat "yyyy-MM-dd HH:mm:ss" -ParameterPSDefaultParameterValues $Script:PSDefaultParameterValues
 
    .EXAMPLE
    Set-LoggingCapabilities -LogPath "C:\Logs\log.log" -ScriptPath "C:\Scripts\script.ps1" -LogMaximum 10 -ShowTime -TimeFormat "yyyy-MM-dd HH:mm:ss"
 
    .NOTES
    This function is used in:
    - CleanupMonster
    - PasswordSolution
    - SharePointEssentials
 
    And many other scripts.
    #>

    [CmdletBinding()]
    param(
        [Alias('Path', 'Log', 'Folder', 'LiteralPath', 'FilePath')][string] $LogPath,
        [string] $ScriptPath,
        [Alias('Maximum')][int] $LogMaximum,
        [switch] $ShowTime,
        [string] $TimeFormat,
        [System.Collections.IDictionary] $ParameterPSDefaultParameterValues
    )
    if (-not $ParameterPSDefaultParameterValues) {
        $Script:PSDefaultParameterValues = @{
            "Write-Color:LogFile"    = $LogPath
            "Write-Color:ShowTime"   = if ($PSBoundParameters.ContainsKey('ShowTime')) {
                $ShowTime.IsPresent 
            } else {
                $null 
            }
            "Write-Color:TimeFormat" = $TimeFormat
        }
    } else {
        $ParameterPSDefaultParameterValues["Write-Color:LogFile"] = $LogPath
        $ParameterPSDefaultParameterValues["Write-Color:ShowTime"] = if ($PSBoundParameters.ContainsKey('ShowTime')) {
            $ShowTime.IsPresent 
        } else {
            $null 
        }
        $ParameterPSDefaultParameterValues["Write-Color:TimeFormat"] = $TimeFormat
    }
    if ($LogPath) {
        try {
            $FolderPath = [io.path]::GetDirectoryName($LogPath)
            if (-not (Test-Path -LiteralPath $FolderPath)) {
                $null = New-Item -Path $FolderPath -ItemType Directory -Force -WhatIf:$false
            }
            if ($LogMaximum -gt 0) {
                if ($ScriptPath) {
                    $ScriptPathFolder = [io.path]::GetDirectoryName($ScriptPath)
                    if ($ScriptPathFolder -eq $FolderPath) {
                        Write-Color -Text '[i] ', "LogMaximum is set to ", $LogMaximum, " but log files are in the same folder as the script. Cleanup disabled." -Color Yellow, White, DarkCyan, White
                        return
                    }

                    $LogPathExtension = [io.path]::GetExtension($LogPath)

                    if ($LogPathExtension) {

                        $CurrentLogs = Get-ChildItem -LiteralPath $FolderPath -Filter "*$LogPathExtension" -ErrorAction Stop | Sort-Object -Property CreationTime -Descending | Select-Object -Skip $LogMaximum
                    } else {
                        $CurrentLogs = $null
                        Write-Color -Text '[i] ', "Log file has no extension (?!). Cleanup disabled." -Color Yellow, White, DarkCyan, White
                    }
                    if ($CurrentLogs) {
                        Write-Color -Text '[i] ', "Logs directory has more than ", $LogMaximum, " log files. Cleanup required..." -Color Yellow, DarkCyan, Red, DarkCyan
                        foreach ($Log in $CurrentLogs) {
                            try {
                                Remove-Item -LiteralPath $Log.FullName -Confirm:$false -WhatIf:$false
                                Write-Color -Text '[+] ', "Deleted ", "$($Log.FullName)" -Color Yellow, White, Green
                            } catch {
                                Write-Color -Text '[-] ', "Couldn't delete log file $($Log.FullName). Error: ', "$($_.Exception.Message) -Color Yellow, White, Red
                            }
                        }
                    }
                } else {
                    Write-Color -Text '[i] ', "LogMaximum is set to ", $LogMaximum, " but no script path detected. Most likely running interactively. Cleanup disabled." -Color Yellow, White, DarkCyan, White
                }
            } else {
                Write-Color -Text '[i] ', "LogMaximum is set to 0 (Unlimited). No log files will be deleted." -Color Yellow, DarkCyan
            }
        } catch {
            Write-Color -Text "[e] ", "Couldn't create the log directory. Error: $($_.Exception.Message)" -Color Yellow, Red
            $Script:PSDefaultParameterValues["Write-Color:LogFile"] = $null
        }
    } else {
        $Script:PSDefaultParameterValues["Write-Color:LogFile"] = $null
    }
    Remove-EmptyValue -Hashtable $Script:PSDefaultParameterValues
}
function Set-ReportingCapabilities { 
    <#
    .SYNOPSIS
    Sets up reporting capabilities by managing report files.
 
    .DESCRIPTION
    This function sets up reporting capabilities by creating the necessary directories and managing the number of report files based on the specified maximum.
 
    .PARAMETER ReportPath
    The path where the report files will be stored.
 
    .PARAMETER ScriptPath
    The path of the script that generates the reports.
 
    .PARAMETER ReportMaximum
    The maximum number of report files to keep. Older files will be deleted if this limit is exceeded.
 
    .EXAMPLE
    Set-ReportingCapabilities -ReportPath "C:\Reports\report.log" -ScriptPath "C:\Scripts\script.ps1" -ReportMaximum 10
 
    .NOTES
    This function is used in:
    - CleanupMonster
    - PasswordSolution
    - SharePointEssentials
 
    And many other scripts.
    #>

    [CmdletBinding()]
    param(
        [alias('Path', 'LiteralPath', 'FilePath')][string] $ReportPath,
        [string] $ScriptPath,
        [Alias('Maximum', 'Count')][int] $ReportMaximum
    )
    if ($ReportPath) {
        try {
            $FolderPath = [io.path]::GetDirectoryName($ReportPath)
            if (-not (Test-Path -LiteralPath $FolderPath -ErrorAction Stop)) {
                $null = New-Item -Path $FolderPath -ItemType Directory -Force -WhatIf:$false -ErrorAction Stop
            }
            if ($ReportMaximum -gt 0) {
                if ($ScriptPath) {
                    $ScriptPathFolder = [io.path]::GetDirectoryName($ScriptPath)
                    if ($ScriptPathFolder -eq $FolderPath) {
                        Write-Color -Text '[i] ', "ReportMaximum is set to ", $ReportMaximum, " but report files are in the same folder as the script. Cleanup disabled." -Color Yellow, White, DarkCyan, White
                        return
                    }
                }

                $ReportPathExtension = [io.path]::GetExtension($ReportPath)

                if ($ReportPathExtension) {

                    $CurrentReports = Get-ChildItem -LiteralPath $FolderPath -Filter "*$ReportPathExtension" -ErrorAction Stop | Sort-Object -Property CreationTime -Descending | Select-Object -Skip $ReportMaximum
                } else {
                    $CurrentReports = $null
                    Write-Color -Text '[i] ', "Report file has no extension (?!). Cleanup disabled." -Color Yellow, White, DarkCyan, White
                }
                if ($CurrentReports) {
                    Write-Color -Text '[i] ', "Reporting directory has more than ", $ReportMaximum, " report files. Cleanup required..." -Color Yellow, DarkCyan, Red, DarkCyan
                    foreach ($Report in $CurrentReports) {
                        try {
                            Remove-Item -LiteralPath $Report.FullName -Confirm:$false -WhatIf:$false
                            Write-Color -Text '[+] ', "Deleted ", "$($Report.FullName)" -Color Yellow, White, Green
                        } catch {
                            Write-Color -Text '[-] ', "Couldn't delete report file $($Report.FullName). Error: ', "$($_.Exception.Message) -Color Yellow, White, Red
                        }
                    }
                }
            } else {
                Write-Color -Text '[i] ', "ReportMaximum is set to 0 (Unlimited). No report files will be deleted." -Color Yellow, DarkCyan
            }
        } catch {
            Write-Color -Text "[e] ", "Couldn't create the reporting directory. Error: $($_.Exception.Message)" -Color Yellow, Red
        }
    }
}
function Write-Color { 
    <#
    .SYNOPSIS
    Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options.
 
    .DESCRIPTION
    Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options.
 
    It provides:
    - Easy manipulation of colors,
    - Logging output to file (log)
    - Nice formatting options out of the box.
    - Ability to use aliases for parameters
 
    .PARAMETER Text
    Text to display on screen and write to log file if specified.
    Accepts an array of strings.
 
    .PARAMETER Color
    Color of the text. Accepts an array of colors. If more than one color is specified it will loop through colors for each string.
    If there are more strings than colors it will start from the beginning.
    Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White
 
    .PARAMETER BackGroundColor
    Color of the background. Accepts an array of colors. If more than one color is specified it will loop through colors for each string.
    If there are more strings than colors it will start from the beginning.
    Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White
 
    .PARAMETER StartTab
    Number of tabs to add before text. Default is 0.
 
    .PARAMETER LinesBefore
    Number of empty lines before text. Default is 0.
 
    .PARAMETER LinesAfter
    Number of empty lines after text. Default is 0.
 
    .PARAMETER StartSpaces
    Number of spaces to add before text. Default is 0.
 
    .PARAMETER LogFile
    Path to log file. If not specified no log file will be created.
 
    .PARAMETER DateTimeFormat
    Custom date and time format string. Default is yyyy-MM-dd HH:mm:ss
 
    .PARAMETER LogTime
    If set to $true it will add time to log file. Default is $true.
 
    .PARAMETER LogRetry
    Number of retries to write to log file, in case it can't write to it for some reason, before skipping. Default is 2.
 
    .PARAMETER Encoding
    Encoding of the log file. Default is Unicode.
 
    .PARAMETER ShowTime
    Switch to add time to console output. Default is not set.
 
    .PARAMETER NoNewLine
    Switch to not add new line at the end of the output. Default is not set.
 
    .PARAMETER NoConsoleOutput
    Switch to not output to console. Default all output goes to console.
 
    .EXAMPLE
    Write-Color -Text "Red ", "Green ", "Yellow " -Color Red,Green,Yellow
 
    .EXAMPLE
    Write-Color -Text "This is text in Green ",
                      "followed by red ",
                      "and then we have Magenta... ",
                      "isn't it fun? ",
                      "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan
 
    .EXAMPLE
    Write-Color -Text "This is text in Green ",
                      "followed by red ",
                      "and then we have Magenta... ",
                      "isn't it fun? ",
                      "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan -StartTab 3 -LinesBefore 1 -LinesAfter 1
 
    .EXAMPLE
    Write-Color "1. ", "Option 1" -Color Yellow, Green
    Write-Color "2. ", "Option 2" -Color Yellow, Green
    Write-Color "3. ", "Option 3" -Color Yellow, Green
    Write-Color "4. ", "Option 4" -Color Yellow, Green
    Write-Color "9. ", "Press 9 to exit" -Color Yellow, Gray -LinesBefore 1
 
    .EXAMPLE
    Write-Color -LinesBefore 2 -Text "This little ","message is ", "written to log ", "file as well." `
                -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" -TimeFormat "yyyy-MM-dd HH:mm:ss"
    Write-Color -Text "This can get ","handy if ", "want to display things, and log actions to file ", "at the same time." `
                -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt"
 
    .EXAMPLE
    Write-Color -T "My text", " is ", "all colorful" -C Yellow, Red, Green -B Green, Green, Yellow
    Write-Color -t "my text" -c yellow -b green
    Write-Color -text "my text" -c red
 
    .EXAMPLE
    Write-Color -Text "TestujÄ™ czy siÄ™ Å‚adnie zapisze, czy bÄ™dÄ… problemy" -Encoding unicode -LogFile 'C:\temp\testinggg.txt' -Color Red -NoConsoleOutput
 
    .NOTES
    Understanding Custom date and time format strings: https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings
    Project support: https://github.com/EvotecIT/PSWriteColor
    Original idea: Josh (https://stackoverflow.com/users/81769/josh)
 
    #>

    [alias('Write-Colour')]
    [CmdletBinding()]
    param (
        [alias ('T')] [String[]]$Text,
        [alias ('C', 'ForegroundColor', 'FGC')] [ConsoleColor[]]$Color = [ConsoleColor]::White,
        [alias ('B', 'BGC')] [ConsoleColor[]]$BackGroundColor = $null,
        [alias ('Indent')][int] $StartTab = 0,
        [int] $LinesBefore = 0,
        [int] $LinesAfter = 0,
        [int] $StartSpaces = 0,
        [alias ('L')] [string] $LogFile = '',
        [Alias('DateFormat', 'TimeFormat')][string] $DateTimeFormat = 'yyyy-MM-dd HH:mm:ss',
        [alias ('LogTimeStamp')][bool] $LogTime = $true,
        [int] $LogRetry = 2,
        [ValidateSet('unknown', 'string', 'unicode', 'bigendianunicode', 'utf8', 'utf7', 'utf32', 'ascii', 'default', 'oem')][string]$Encoding = 'Unicode',
        [switch] $ShowTime,
        [switch] $NoNewLine,
        [alias('HideConsole')][switch] $NoConsoleOutput
    )
    if (-not $NoConsoleOutput) {
        $DefaultColor = $Color[0]
        if ($null -ne $BackGroundColor -and $BackGroundColor.Count -ne $Color.Count) {
            Write-Error "Colors, BackGroundColors parameters count doesn't match. Terminated."
            return
        }
        if ($LinesBefore -ne 0) {
            for ($i = 0; $i -lt $LinesBefore; $i++) {
                Write-Host -Object "`n" -NoNewline 
            } 
        } # Add empty line before
        if ($StartTab -ne 0) {
            for ($i = 0; $i -lt $StartTab; $i++) {
                Write-Host -Object "`t" -NoNewline 
            } 
        }  # Add TABS before text
        if ($StartSpaces -ne 0) {
            for ($i = 0; $i -lt $StartSpaces; $i++) {
                Write-Host -Object ' ' -NoNewline 
            } 
        }  # Add SPACES before text
        if ($ShowTime) {
            Write-Host -Object "[$([datetime]::Now.ToString($DateTimeFormat))] " -NoNewline 
        } # Add Time before output
        if ($Text.Count -ne 0) {
            if ($Color.Count -ge $Text.Count) {
                # the real deal coloring
                if ($null -eq $BackGroundColor) {
                    for ($i = 0; $i -lt $Text.Length; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline 
                    }
                } else {
                    for ($i = 0; $i -lt $Text.Length; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline 
                    }
                }
            } else {
                if ($null -eq $BackGroundColor) {
                    for ($i = 0; $i -lt $Color.Length ; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline 
                    }
                    for ($i = $Color.Length; $i -lt $Text.Length; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -NoNewline 
                    }
                } else {
                    for ($i = 0; $i -lt $Color.Length ; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline 
                    }
                    for ($i = $Color.Length; $i -lt $Text.Length; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -BackgroundColor $BackGroundColor[0] -NoNewline 
                    }
                }
            }
        }
        if ($NoNewLine -eq $true) {
            Write-Host -NoNewline 
        } else {
            Write-Host 
        } # Support for no new line
        if ($LinesAfter -ne 0) {
            for ($i = 0; $i -lt $LinesAfter; $i++) {
                Write-Host -Object "`n" -NoNewline 
            } 
        }  # Add empty line after
    }
    if ($Text.Count -and $LogFile) {
        # Save to file
        $TextToFile = ""
        for ($i = 0; $i -lt $Text.Length; $i++) {
            $TextToFile += $Text[$i]
        }
        $Saved = $false
        $Retry = 0
        Do {
            $Retry++
            try {
                if ($LogTime) {
                    "[$([datetime]::Now.ToString($DateTimeFormat))] $TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false
                } else {
                    "$TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false
                }
                $Saved = $true
            } catch {
                if ($Saved -eq $false -and $Retry -eq $LogRetry) {
                    Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Tried ($Retry/$LogRetry))"
                } else {
                    Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Retrying... ($Retry/$LogRetry)"
                }
            }
        } Until ($Saved -eq $true -or $Retry -ge $LogRetry)
    }
}
function Convert-UserAccountControl { 
    <#
    .SYNOPSIS
    Converts the UserAccountControl flags to their corresponding names.
 
    .DESCRIPTION
    This function takes a UserAccountControl value and converts it into a human-readable format by matching the flags to their corresponding names.
 
    .PARAMETER UserAccountControl
    Specifies the UserAccountControl value to be converted.
 
    .PARAMETER Separator
    Specifies the separator to use when joining the converted flags. If not provided, the flags will be returned as a list.
 
    .EXAMPLE
    Convert-UserAccountControl -UserAccountControl 66048
    Outputs: "DONT_EXPIRE_PASSWORD, PASSWORD_EXPIRED"
 
    .EXAMPLE
    Convert-UserAccountControl -UserAccountControl 512 -Separator ', '
    Outputs: "NORMAL_ACCOUNT"
 
    #>

    [cmdletBinding()]
    param(
        [alias('UAC')][int] $UserAccountControl,
        [string] $Separator
    )
    $UserAccount = [ordered] @{
        "SCRIPT"                         = 1
        "ACCOUNTDISABLE"                 = 2
        "HOMEDIR_REQUIRED"               = 8
        "LOCKOUT"                        = 16
        "PASSWD_NOTREQD"                 = 32
        "ENCRYPTED_TEXT_PWD_ALLOWED"     = 128
        "TEMP_DUPLICATE_ACCOUNT"         = 256
        "NORMAL_ACCOUNT"                 = 512
        "INTERDOMAIN_TRUST_ACCOUNT"      = 2048
        "WORKSTATION_TRUST_ACCOUNT"      = 4096
        "SERVER_TRUST_ACCOUNT"           = 8192
        "DONT_EXPIRE_PASSWORD"           = 65536
        "MNS_LOGON_ACCOUNT"              = 131072
        "SMARTCARD_REQUIRED"             = 262144
        "TRUSTED_FOR_DELEGATION"         = 524288
        "NOT_DELEGATED"                  = 1048576
        "USE_DES_KEY_ONLY"               = 2097152
        "DONT_REQ_PREAUTH"               = 4194304
        "PASSWORD_EXPIRED"               = 8388608
        "TRUSTED_TO_AUTH_FOR_DELEGATION" = 16777216
        "PARTIAL_SECRETS_ACCOUNT"        = 67108864
    }
    $Output = foreach ($_ in $UserAccount.Keys) {
        $binaryAnd = $UserAccount[$_] -band $UserAccountControl
        if ($binaryAnd -ne "0") {
            $_
        }
    }
    if ($Separator) {
        $Output -join $Separator
    } else {
        $Output
    }
}
function Copy-DictionaryManual { 
    <#
    .SYNOPSIS
    Copies a dictionary recursively, handling nested dictionaries and lists.
 
    .DESCRIPTION
    This function copies a dictionary recursively, handling nested dictionaries and lists. It creates a deep copy of the input dictionary, ensuring that modifications to the copied dictionary do not affect the original dictionary.
 
    .PARAMETER Dictionary
    The dictionary to be copied.
 
    .EXAMPLE
    $originalDictionary = @{
        'Key1' = 'Value1'
        'Key2' = @{
            'NestedKey1' = 'NestedValue1'
        }
    }
    $copiedDictionary = Copy-DictionaryManual -Dictionary $originalDictionary
 
    This example demonstrates how to copy a dictionary with nested values.
 
    #>

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

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

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

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

    $clone
}
function Get-GitHubLatestRelease { 
    <#
    .SYNOPSIS
    Gets one or more releases from GitHub repository
 
    .DESCRIPTION
    Gets one or more releases from GitHub repository
 
    .PARAMETER Url
    Url to github repository
 
    .EXAMPLE
    Get-GitHubLatestRelease -Url "https://api.github.com/repos/evotecit/Testimo/releases" | Format-Table
 
    .NOTES
    General notes
    #>

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

    $Responds = Test-Connection -ComputerName $URl.Host -Quiet -Count 1
    if ($Responds) {
        Try {
            [Array] $JsonOutput = (Invoke-WebRequest -Uri $Url -ErrorAction Stop | ConvertFrom-Json)
            foreach ($JsonContent in $JsonOutput) {
                [PSCustomObject] @{
                    PublishDate = [DateTime]  $JsonContent.published_at
                    CreatedDate = [DateTime] $JsonContent.created_at
                    PreRelease  = [bool] $JsonContent.prerelease
                    Version     = [version] ($JsonContent.name -replace 'v', '')
                    Tag         = $JsonContent.tag_name
                    Branch      = $JsonContent.target_commitish
                    Errors      = ''
                }
            }
        } catch {
            [PSCustomObject] @{
                PublishDate = $null
                CreatedDate = $null
                PreRelease  = $null
                Version     = $null
                Tag         = $null
                Branch      = $null
                Errors      = $_.Exception.Message
            }
        }
    } else {
        [PSCustomObject] @{
            PublishDate = $null
            CreatedDate = $null
            PreRelease  = $null
            Version     = $null
            Tag         = $null
            Branch      = $null
            Errors      = "No connection (ping) to $($Url.Host)"
        }
    }
    $ProgressPreference = 'Continue'
}
function Get-WinADTrust { 
    <#
    .SYNOPSIS
    Retrieves trust relationships within an Active Directory forest.
 
    .DESCRIPTION
    This cmdlet retrieves and displays trust relationships within an Active Directory forest. It can be used to identify the trust relationships between domains and forests, including the type of trust, direction, and other properties. The cmdlet can also recursively explore trust relationships across multiple forests.
 
    .PARAMETER Forest
    Specifies the target forest to retrieve trust relationships from. If not specified, the current forest is used.
 
    .PARAMETER Recursive
    Indicates that the cmdlet should recursively explore trust relationships across multiple forests.
 
    .PARAMETER Nesting
    This parameter is used internally to track the nesting level of recursive calls. It should not be used directly.
 
    .PARAMETER UniqueTrusts
    This parameter is used internally to keep track of unique trust relationships encountered during recursive exploration. It should not be used directly.
 
    .PARAMETER SkipValidation
    Indicates that the cmdlet should skip the validation of trust relationships. By default, the cmdlet validates trust relationships by checking the existence of the "Domain Admins" group in the trusted domain.
 
    .EXAMPLE
    Get-WinADTrust -Recursive
    This example retrieves all trust relationships within the current forest and recursively explores trust relationships across multiple forests.
 
    .NOTES
    This cmdlet is designed to provide detailed information about trust relationships within an Active Directory environment. It can be used for auditing, troubleshooting, and planning purposes.
    #>

    [alias('Get-WinADTrusts')]
    [cmdletBinding()]
    param(
        [string] $Forest,
        [switch] $Recursive,
        [Parameter(DontShow)][int] $Nesting = -1,
        [Parameter(DontShow)][System.Collections.IDictionary] $UniqueTrusts,
        [switch] $SkipValidation
    )
    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
            }
            if (-not $SkipValidation) {
                $TrustStatus = Test-DomainTrust -Domain $Trust.Details.SourceName -TrustedDomain $Trust.Details.TargetName
                $GroupExists = Get-WinADObject -Identity 'S-1-5-32-544' -DomainName $Trust.Details.TargetName
            } else {
                $TrustStatus = [PSCustomObject] @{
                    TrustStatus   = 'Validation skipped'
                    TrustSourceDC = ''
                    TrustTargetDC = ''
                }
                $GroupExists = $null
            }
            [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
                'CreatedDaysAgo'          = ((Get-Date) - $TrustObject[$Trust.Details.TargetName].WhenCreated).Days
                'ModifiedDaysAgo'         = ((Get-Date) - $TrustObject[$Trust.Details.TargetName].WhenChanged).Days
                'NetBiosName'             = if ($Trust.Details.TrustedDomainInformation.NetBiosName) {
                    $Trust.Details.TrustedDomainInformation.NetBiosName 
                } else {
                    $TrustObject[$Trust.Details.TargetName].TrustPartnerNetBios 
                }
                'DomainSID'               = if ($Trust.Details.TrustedDomainInformation.DomainSid) {
                    $Trust.Details.TrustedDomainInformation.DomainSid 
                } else {
                    $TrustObject[$Trust.Details.TargetName].ObjectSID 
                }
                'Status'                  = if ($null -ne $Trust.Details.TrustedDomainInformation.Status) {
                    $Trust.Details.TrustedDomainInformation.Status.ToString() 
                } else {
                    'Internal' 
                }
                '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 ($null -eq $GroupExists) {
                    'Skipped' 
                } elseif ($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 -SkipValidation:$SkipValidation.IsPresent
                }
            }
        }
    }
}
function Test-ComputerPort { 
    <#
    .SYNOPSIS
    Tests the connectivity of a computer on specified TCP and UDP ports.
 
    .DESCRIPTION
    The Test-ComputerPort function tests the connectivity of a computer on specified TCP and UDP ports. It checks if the specified ports are open and reachable on the target computer.
 
    .PARAMETER ComputerName
    Specifies the name of the computer to test the port connectivity.
 
    .PARAMETER PortTCP
    Specifies an array of TCP ports to test connectivity.
 
    .PARAMETER PortUDP
    Specifies an array of UDP ports to test connectivity.
 
    .PARAMETER Timeout
    Specifies the timeout value in milliseconds for the connection test. Default is 5000 milliseconds.
 
    .EXAMPLE
    Test-ComputerPort -ComputerName "Server01" -PortTCP 80,443 -PortUDP 53 -Timeout 3000
    Tests the connectivity of Server01 on TCP ports 80 and 443, UDP port 53 with a timeout of 3000 milliseconds.
 
    .EXAMPLE
    Test-ComputerPort -ComputerName "Server02" -PortTCP 3389 -PortUDP 123
    Tests the connectivity of Server02 on TCP port 3389, UDP port 123 with the default timeout of 5000 milliseconds.
    #>

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

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

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

        if ($TemporaryProgress) {
            $Global:ProgressPreference = $TemporaryProgress
        }
    }
}
function Test-WinRM { 
    <#
    .SYNOPSIS
    Tests the WinRM connectivity on the specified computers.
 
    .DESCRIPTION
    The Test-WinRM function tests the WinRM connectivity on the specified computers and returns the status of the connection.
 
    .PARAMETER ComputerName
    Specifies the names of the computers to test WinRM connectivity on.
 
    .EXAMPLE
    Test-WinRM -ComputerName "Server01", "Server02"
    Tests the WinRM connectivity on Server01 and Server02.
 
    .EXAMPLE
    Test-WinRM -ComputerName "Server03"
    Tests the WinRM connectivity on Server03.
 
    #>

    [CmdletBinding()]
    param (
        [alias('Server')][string[]] $ComputerName
    )
    $Output = foreach ($Computer in $ComputerName) {
        $Test = [PSCustomObject] @{
            Output       = $null
            Status       = $null
            ComputerName = $Computer
        }
        try {
            $Test.Output = Test-WSMan -ComputerName $Computer -ErrorAction Stop
            $Test.Status = $true
        } catch {
            $Test.Status = $false
        }
        $Test
    }
    $Output
}
function Get-WinADDomain { 
    <#
    .SYNOPSIS
    Retrieves information about a specified Active Directory domain.
 
    .DESCRIPTION
    This function retrieves detailed information about the specified Active Directory domain.
    It queries the domain to gather information such as domain controllers, domain name, and other domain-related details.
 
    .PARAMETER Domain
    Specifies the target domain to retrieve information from.
 
    .EXAMPLE
    Get-WinADDomain -Domain "example.com"
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory domain.
    #>

    [cmdletBinding()]
    param(
        [string] $Domain
    )
    try {
        if ($Domain) {
            $Type = [System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Domain
            $Context = [System.DirectoryServices.ActiveDirectory.DirectoryContext]::new($Type, $Domain)
            $DomainInformation = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($Context)
        } else {
            $DomainInformation = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain()
        }
    } catch {
        Write-Warning "Get-WinADDomain - Can't get $Domain information, error: $($_.Exception.Message.Replace([System.Environment]::NewLine,''))"
    }
    $DomainInformation
}
function Get-WinADForest { 
    <#
    .SYNOPSIS
    Retrieves information about a specified Active Directory forest.
 
    .DESCRIPTION
    This function retrieves detailed information about the specified Active Directory forest.
    It queries the forest to gather information such as domain controllers, sites, and other forest-related details.
 
    .PARAMETER Forest
    Specifies the target forest to retrieve information from.
 
    .EXAMPLE
    Get-WinADForest -Forest "example.com"
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory forest.
    #>

    [cmdletBinding()]
    param(
        [string] $Forest
    )
    try {
        if ($Forest) {
            $Type = [System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Forest
            $Context = [System.DirectoryServices.ActiveDirectory.DirectoryContext]::new($Type, $Forest)
            $ForestInformation = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($Context)
        } else {
            $ForestInformation = ([System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest())
        }
    } catch {
        Write-Warning "Get-WinADForest - Can't get $Forest information, error: $($_.Exception.Message.Replace([System.Environment]::NewLine,''))"
    }
    $ForestInformation
}
function Get-WinADObject { 
    <#
    .SYNOPSIS
    Gets Active Directory Object
 
    .DESCRIPTION
    Returns Active Directory Object (Computers, Groups, Users or ForeignSecurityPrincipal) using ADSI
 
    .PARAMETER Identity
    Identity of an object. It can be SamAccountName, SID, DistinguishedName or multiple other options
 
    .PARAMETER DomainName
    Choose domain name the objects resides in. This is optional for most objects
 
    .PARAMETER Credential
    Parameter description
 
    .PARAMETER IncludeGroupMembership
    Queries for group members when object is a group
 
    .PARAMETER IncludeAllTypes
    Allows functions to return all objects types and not only Computers, Groups, Users or ForeignSecurityPrincipal
 
    .EXAMPLE
    Get-WinADObject -Identity 'TEST\Domain Admins' -Verbose
    Get-WinADObject -Identity 'EVOTEC\Domain Admins' -Verbose
    Get-WinADObject -Identity 'Domain Admins' -DomainName 'DC=AD,DC=EVOTEC,DC=PL' -Verbose
    Get-WinADObject -Identity 'Domain Admins' -DomainName 'ad.evotec.pl' -Verbose
    Get-WinADObject -Identity 'CN=Domain Admins,CN=Users,DC=ad,DC=evotec,DC=pl'
    Get-WinADObject -Identity 'CN=Domain Admins,CN=Users,DC=ad,DC=evotec,DC=xyz'
 
    .NOTES
    General notes
    #>

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

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

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


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

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

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

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

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

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

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

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

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

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

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

                if ($Cache) {
                    $Script:CacheObjectsWinADObject[$TemporaryName] = [PSCustomObject] $ReturnObject
                    $Script:CacheObjectsWinADObject[$TemporaryName]
                } else {
                    [PSCustomObject] $ReturnObject
                }
            }
        }
    }
}
function Get-WinADTrustObject { 
    <#
    .SYNOPSIS
    Retrieves trust relationship information for a specified domain in Active Directory.
 
    .DESCRIPTION
    This function retrieves trust relationship information for the specified domain in Active Directory. It provides details about the trust type, trust direction, trust attributes, and security identifier.
 
    .PARAMETER Identity
    Specifies the domain identity for which trust relationship information is to be retrieved.
 
    .PARAMETER AsHashTable
    Indicates whether the output should be returned as a hashtable.
 
    .NOTES
    Author: Your Name
    Date: Current Date
    Version: 1.0
    #>

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

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

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

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


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

        $TrustObject = [PSCustomObject] @{
            #Name = [string] $Trust.properties.name # {ad.evotec.xyz}
            TrustSource                  = $TrustSource
            TrustPartner                 = [string] $Trust.properties.trustpartner           # {ad.evotec.xyz}
            TrustPartnerNetBios          = [string] $Trust.properties.flatname               # {EVOTEC}
            TrustDirection               = $TrustD.ToString()         # {3}
            TrustType                    = $TrustT.ToString()             # {2}
            TrustAttributes              = $TrustAttributes       # {32}
            TrustDirectionText           = $TrustDirection[$TrustD.ToString()]
            TrustTypeText                = $TrustType[$TrustT.ToString()]
            WhenCreated                  = [DateTime] $Trust.properties.whencreated[0]         # {26.07.2018 10:59:52}
            WhenChanged                  = [DateTime] $Trust.properties.whenchanged[0]            # {14.08.2020 22:23:14}
            ObjectSID                    = $ObjectSID
            Distinguishedname            = [string] $Trust.properties.distinguishedname      # {CN=ad.evotec.xyz,CN=System,DC=ad,DC=evotec,DC=pl}
            IsCriticalSystemObject       = [bool]::Parse($Trust.properties.iscriticalsystemobject[0]) # {True}
            ObjectGuid                   = [guid]::new($Trust.properties.objectguid[0])
            ObjectCategory               = [string] $Trust.properties.objectcategory         # {CN=Trusted-Domain,CN=Schema,CN=Configuration,DC=ad,DC=evotec,DC=xyz}
            ObjectClass                  = ([array] $Trust.properties.objectclass)[-1]           # {top, leaf, trustedDomain}
            UsnCreated                   = [string] $Trust.properties.usncreated             # {14149}
            UsnChanged                   = [string] $Trust.properties.usnchanged             # {4926091}
            ShowInAdvancedViewOnly       = [bool]::Parse($Trust.properties.showinadvancedviewonly) # {True}
            TrustPosixOffset             = [string] $Trust.properties.trustposixoffset       # {-2147483648}
            msDSTrustForestTrustInfo     = $msDSTrustForestTrustInfo
            msDSSupportedEncryptionTypes = if ($Trust.properties.'msds-supportedencryptiontypes') {
                Get-ADEncryptionTypes -Value ([int] $Trust.properties.'msds-supportedencryptiontypes'[0]) 
            } else {
                $null 
            }
            #SecurityIdentifier = [string] $Trust.properties.securityidentifier # {1 4 0 0 0 0 0 5 21 0 0 0 113 37 225 50 27 133 23 171 67 175 144 188}
            #InstanceType = $Trust.properties.instancetype # {4}
            #AdsPath = [string] $Trust.properties.adspath # {LDAP://CN=ad.evotec.xyz,CN=System,DC=ad,DC=evotec,DC=pl}
            #CN = [string] $Trust.properties.cn # {ad.evotec.xyz}
            #ObjectGuid = $Trust.properties.objectguid # {193 58 187 220 218 30 146 77 162 218 90 74 159 98 153 219}
            #dscorepropagationdata = $Trust.properties.dscorepropagationdata # {01.01.1601 00:00:00}
        }
        if ($AsHashTable) {
            $Summary[$TrustObject.trustpartner] = $TrustObject
        } else {
            $TrustObject
        }
    }
    if ($AsHashTable) {
        $Summary
    }
}
function Test-DomainTrust { 
    <#
    .SYNOPSIS
    Test the trust relationship between two domains.
 
    .DESCRIPTION
    This function tests the trust relationship between the specified domain and a trusted domain.
 
    .PARAMETER Domain
    Specifies the domain to test the trust relationship for.
 
    .PARAMETER TrustedDomain
    Specifies the trusted domain to test the trust relationship against.
 
    .EXAMPLE
    Test-DomainTrust -Domain "contoso.com" -TrustedDomain "fabrikam.com"
    Tests the trust relationship between the "contoso.com" domain and the "fabrikam.com" trusted domain.
 
    .NOTES
    Author: Your Name
    Date: Date
    Version: 1.0
    #>

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

    $PropertiesTrustWMI = @(
        'FlatName',
        'SID',
        'TrustAttributes',
        'TrustDirection',
        'TrustedDCName',
        'TrustedDomain',
        'TrustIsOk',
        'TrustStatus',
        'TrustStatusString', # TrustIsOk/TrustStatus are covered by this
        'TrustType'
    )
    $getCimInstanceSplat = @{
        ClassName    = 'Microsoft_DomainTrustStatus'
        Namespace    = 'root\MicrosoftActiveDirectory'
        ComputerName = $DomainPDC
        ErrorAction  = 'SilentlyContinue'
        Property     = $PropertiesTrustWMI
        Verbose      = $false
    }
    if ($TrustedDomain) {
        $getCimInstanceSplat['Filter'] = "TrustedDomain = `"$TrustedDomain`""
    }
    $TrustStatatuses = Get-CimInstance @getCimInstanceSplat
    if ($TrustStatatuses) {
        foreach ($Status in $TrustStatatuses) {
            [PSCustomObject] @{
                'TrustSource'     = $DomainInformation.Name
                'TrustPartner'    = $Status.TrustedDomain
                'TrustAttributes' = if ($Status.TrustAttributes) {
                    Get-ADTrustAttributes -Value $Status.TrustAttributes 
                } else {
                    'Error - needs fixing' 
                }
                'TrustStatus'     = if ($null -ne $Status) {
                    $Status.TrustStatusString 
                } else {
                    'N/A' 
                }
                'TrustSourceDC'   = if ($null -ne $Status) {
                    $Status.PSComputerName 
                } else {
                    '' 
                }
                'TrustTargetDC'   = if ($null -ne $Status) {
                    $Status.TrustedDCName.Replace('\\', '') 
                } else {
                    '' 
                }
                #'TrustOK' = if ($null -ne $Status) { $Status.TrustIsOK } else { $false }
                #'TrustStatusInt' = if ($null -ne $Status) { $Status.TrustStatus } else { -1 }
            }
        }
    } else {
        [PSCustomObject] @{
            'TrustSource'     = $DomainInformation.Name
            'TrustPartner'    = $TrustedDomain
            'TrustAttributes' = 'Error - needs fixing'
            'TrustStatus'     = 'N/A'
            'TrustSourceDC'   = ''
            'TrustTargetDC'   = ''
            #'TrustOK' = $false
            #'TrustStatusInt' = -1
        }
    }
}
function Convert-Identity { 
    <#
    .SYNOPSIS
    Small command that tries to resolve any given object
 
    .DESCRIPTION
    Small command that tries to resolve any given object - be it SID, DN, FSP or Netbiosname
 
    .PARAMETER Identity
    Type to resolve in form of Identity, DN, SID
 
    .PARAMETER SID
    Allows to pass SID directly, rather then going thru verification process
 
    .PARAMETER Name
    Allows to pass Name directly, rather then going thru verification process
 
    .PARAMETER Force
    Allows to clear cache, useful when you want to force refresh
 
    .EXAMPLE
    $Identity = @(
        'S-1-5-4'
        'S-1-5-4'
        'S-1-5-11'
        'S-1-5-32-549'
        'S-1-5-32-550'
        'S-1-5-32-548'
        'S-1-5-64-10'
        'S-1-5-64-14'
        'S-1-5-64-21'
        'S-1-5-18'
        'S-1-5-19'
        'S-1-5-32-544'
        'S-1-5-20-20-10-51' # Wrong SID
        'S-1-5-21-853615985-2870445339-3163598659-512'
        'S-1-5-21-3661168273-3802070955-2987026695-512'
        'S-1-5-21-1928204107-2710010574-1926425344-512'
        'CN=Test Test 2,OU=Users,OU=Production,DC=ad,DC=evotec,DC=pl'
        'Test Local Group'
        'przemyslaw.klys@evotec.pl'
        'test2'
        'NT AUTHORITY\NETWORK'
        'NT AUTHORITY\SYSTEM'
        'S-1-5-21-853615985-2870445339-3163598659-519'
        'TEST\some'
        'EVOTECPL\Domain Admins'
        'NT AUTHORITY\INTERACTIVE'
        'INTERACTIVE'
        'EVOTEC\Domain Admins'
        'EVOTECPL\Domain Admins'
        'Test\Domain Admins'
        'CN=S-1-5-21-1928204107-2710010574-1926425344-512,CN=ForeignSecurityPrincipals,DC=ad,DC=evotec,DC=xyz' # Valid
        'CN=S-1-5-21-1928204107-2710010574-512,CN=ForeignSecurityPrincipals,DC=ad,DC=evotec,DC=xyz' # not valid
        'CN=S-1-5-21-1928204107-2710010574-1926425344-512,CN=ForeignSecurityPrincipals,DC=ad,DC=evotec,DC=xyz' # cached
    )
 
    $TestOutput = Convert-Identity -Identity $Identity -Verbose
 
    Output:
 
    Name SID DomainName Type Error
    ---- --- ---------- ---- -----
    NT AUTHORITY\INTERACTIVE S-1-5-4 WellKnownGroup
    NT AUTHORITY\INTERACTIVE S-1-5-4 WellKnownGroup
    NT AUTHORITY\Authenticated Users S-1-5-11 WellKnownGroup
    BUILTIN\Server Operators S-1-5-32-549 WellKnownGroup
    BUILTIN\Print Operators S-1-5-32-550 WellKnownGroup
    BUILTIN\Account Operators S-1-5-32-548 WellKnownGroup
    NT AUTHORITY\NTLM Authentication S-1-5-64-10 WellKnownGroup
    NT AUTHORITY\SChannel Authentication S-1-5-64-14 WellKnownGroup
    NT AUTHORITY\Digest Authentication S-1-5-64-21 WellKnownGroup
    NT AUTHORITY\SYSTEM S-1-5-18 WellKnownAdministrative
    NT AUTHORITY\NETWORK SERVICE S-1-5-19 WellKnownGroup
    BUILTIN\Administrators S-1-5-32-544 WellKnownAdministrative
    S-1-5-20-20-10-51 S-1-5-20-20-10-51 Unknown Exception calling "Translate" with "1" argument(s): "Some or all identity references could not be translated."
    EVOTEC\Domain Admins S-1-5-21-853615985-2870445339-3163598659-512 ad.evotec.xyz Administrative
    EVOTECPL\Domain Admins S-1-5-21-3661168273-3802070955-2987026695-512 ad.evotec.pl Administrative
    TEST\Domain Admins S-1-5-21-1928204107-2710010574-1926425344-512 test.evotec.pl Administrative
    EVOTECPL\TestingAD S-1-5-21-3661168273-3802070955-2987026695-1111 ad.evotec.pl NotAdministrative
    EVOTEC\Test Local Group S-1-5-21-853615985-2870445339-3163598659-3610 ad.evotec.xyz NotAdministrative
    EVOTEC\przemyslaw.klys S-1-5-21-853615985-2870445339-3163598659-1105 ad.evotec.xyz NotAdministrative
    test2 Unknown Exception calling "Translate" with "1" argument(s): "Some or all identity references could not be translated."
    NT AUTHORITY\NETWORK S-1-5-2 WellKnownGroup
    NT AUTHORITY\SYSTEM S-1-5-18 WellKnownAdministrative
    EVOTEC\Enterprise Admins S-1-5-21-853615985-2870445339-3163598659-519 ad.evotec.xyz Administrative
    TEST\some S-1-5-21-1928204107-2710010574-1926425344-1106 test.evotec.pl NotAdministrative
    EVOTECPL\Domain Admins S-1-5-21-3661168273-3802070955-2987026695-512 ad.evotec.pl Administrative
    NT AUTHORITY\INTERACTIVE S-1-5-4 WellKnownGroup
    NT AUTHORITY\INTERACTIVE S-1-5-4 WellKnownGroup
    EVOTEC\Domain Admins S-1-5-21-853615985-2870445339-3163598659-512 ad.evotec.xyz Administrative
    EVOTECPL\Domain Admins S-1-5-21-3661168273-3802070955-2987026695-512 ad.evotec.pl Administrative
    TEST\Domain Admins S-1-5-21-1928204107-2710010574-1926425344-512 test.evotec.pl Administrative
    TEST\Domain Admins S-1-5-21-1928204107-2710010574-1926425344-512 test.evotec.pl Administrative
    S-1-5-21-1928204107-2710010574-512 S-1-5-21-1928204107-2710010574-512 Unknown Exception calling "Translate" with "1" argument(s): "Some or all identity references could not be translated."
    TEST\Domain Admins S-1-5-21-1928204107-2710010574-1926425344-512 test.evotec.pl Administrative
 
    .NOTES
    General notes
    #>

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

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

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

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

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

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

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

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

                    Write-Verbose "Convert-Identity - Processing $Ident (Other)"
                    try {
                        $SIDValue = ([System.Security.Principal.NTAccount] $Ident).Translate([System.Security.Principal.SecurityIdentifier]).Value
                        $Script:GlobalCacheSidConvert[$Ident] = ConvertFrom-SID -SID $SIDValue
                    } catch {
                        $Script:GlobalCacheSidConvert[$Ident] = [PSCustomObject] @{
                            Name       = $Ident
                            SID        = $null
                            DomainName = ''
                            Type       = 'Unknown'
                            Error      = $_.Exception.Message -replace [environment]::NewLine, ' '
                        }
                    }
                    $Script:GlobalCacheSidConvert[$Ident]
                }
            }
        } else {
            if ($SID) {
                foreach ($S in $SID) {
                    if ($Script:GlobalCacheSidConvert[$S]) {
                        $Script:GlobalCacheSidConvert[$S]
                    } else {
                        $Script:GlobalCacheSidConvert[$S] = ConvertFrom-SID -SID $S
                        $Script:GlobalCacheSidConvert[$S]
                    }
                }
            } else {
                foreach ($Ident in $Name) {
                    if ($Script:GlobalCacheSidConvert[$Ident]) {
                        $Script:GlobalCacheSidConvert[$Ident]
                    } else {
                        $Script:GlobalCacheSidConvert[$Ident] = ([System.Security.Principal.NTAccount] $Ident).Translate([System.Security.Principal.SecurityIdentifier]).Value
                        $Script:GlobalCacheSidConvert[$Ident]
                    }
                }
            }
        }
    }
    End {
    }
}
function Convert-TrustForestTrustInfo { 
    <#
    .SYNOPSIS
    Converts the binary data of forest trust information into a readable format.
 
    .DESCRIPTION
    This function takes the binary data of forest trust information and converts it into a readable format, providing details about the trust status, domain names, SIDs, and creation timestamps.
 
    .PARAMETER msDSTrustForestTrustInfo
    The binary data containing forest trust information.
 
    .EXAMPLE
    Convert-TrustForestTrustInfo -msDSTrustForestTrustInfo $binaryData
    Converts the binary forest trust information into a readable format.
 
    .NOTES
    Author: Your Name
    Date: Current Date
    Version: 1.0
    #>

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

    if ($msDSTrustForestTrustInfo) {
        $Read = Get-ForestTrustInfo -Byte $msDSTrustForestTrustInfo
        $ForestTrustDomainInfo = [ordered]@{}
        [Array] $Records = foreach ($Record in $Read.Records) {
            if ($Record.RecordType -ne 'ForestTrustDomainInfo') {
                # ForestTrustTopLevelName, ForestTrustTopLevelNameEx
                if ($Record.RecordType -eq 'ForestTrustTopLevelName') {
                    $Type = 'Included'
                } else {
                    $Type = 'Excluded'
                }
                [PSCustomObject] @{
                    DnsName     = $null
                    NetbiosName = $null
                    Sid         = $null
                    Type        = $Type
                    Suffix      = $Record.ForestTrustData
                    Status      = $Flags["$($Record.Flags)"]
                    StatusFlag  = $Record.Flags
                    WhenCreated = $Record.Timestamp
                }
            } else {
                $ForestTrustDomainInfo['DnsName'] = $Record.ForestTrustData.DnsName
                $ForestTrustDomainInfo['NetbiosName'] = $Record.ForestTrustData.NetbiosName
                $ForestTrustDomainInfo['Sid'] = $Record.ForestTrustData.Sid
            }
        }
        foreach ($Record in $Records) {
            $Record.DnsName = $ForestTrustDomainInfo['DnsName']
            $Record.NetbiosName = $ForestTrustDomainInfo['NetbiosName']
            $Record.Sid = $ForestTrustDomainInfo['Sid']
        }
        $Records
    }
}
function ConvertFrom-NetbiosName { 
    <#
    .SYNOPSIS
    Converts a NetBIOS name to its corresponding domain name and object name.
 
    .DESCRIPTION
    This function takes a NetBIOS name in the format 'Domain\Object' and converts it to the corresponding domain name and object name.
 
    .PARAMETER Identity
    Specifies the NetBIOS name(s) to convert.
 
    .EXAMPLE
    'TEST\Domain Admins', 'EVOTEC\Domain Admins', 'EVOTECPL\Domain Admins' | ConvertFrom-NetbiosName
    Converts the NetBIOS names 'TEST\Domain Admins', 'EVOTEC\Domain Admins', and 'EVOTECPL\Domain Admins' to their corresponding domain names and object names.
 
    .EXAMPLE
    ConvertFrom-NetbiosName -Identity 'TEST\Domain Admins', 'EVOTEC\Domain Admins', 'EVOTECPL\Domain Admins'
    Converts the NetBIOS names 'TEST\Domain Admins', 'EVOTEC\Domain Admins', and 'EVOTECPL\Domain Admins' to their corresponding domain names and object names.
 
    #>

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

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

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

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

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

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

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

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

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

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

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

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

                $DN = 'DC=' + $ADObject[0].Replace('.', ',DC=')
            }
            $DN
        }
    }
}
function Get-ADEncryptionTypes { 
    <#
    .SYNOPSIS
    Retrieves the supported encryption types based on the specified value.
 
    .DESCRIPTION
    This function returns the list of encryption types supported by Active Directory based on the provided value. Each encryption type is represented by a string.
 
    .PARAMETER Value
    Specifies the integer value representing the encryption types to retrieve.
 
    .EXAMPLE
    Get-ADEncryptionTypes -Value 24
 
    Retrieves the following encryption types:
    - AES128-CTS-HMAC-SHA1-96
    - AES256-CTS-HMAC-SHA1-96
 
    .NOTES
    This function is designed to provide information about encryption types supported by Active Directory.
    #>

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

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

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

    [String[]]$TrustAttributes = @(
        Foreach ($V in $Value) {
            if ([int32]$V -band 0x00000001) {
                "Non Transitive" 
            } 
            if ([int32]$V -band 0x00000002) {
                "UpLevel Only" 
            } 
            if ([int32]$V -band 0x00000004) {
                "Quarantined Domain" 
            } 
            if ([int32]$V -band 0x00000008) {
                "Forest Transitive" 
            } 
            if ([int32]$V -band 0x00000010) {
                "Cross Organization" 
            } 
            if ([int32]$V -band 0x00000020) {
                "Within Forest" 
            } 
            if ([int32]$V -band 0x00000040) {
                "Treat as External" 
            } 
            if ([int32]$V -band 0x00000080) {
                "Uses RC4 Encryption" 
            } 
            if ([int32]$V -band 0x00000200) {
                "No TGT DELEGATION" 
            } 
            if ([int32]$V -band 0x00000800) {
                "Enable TGT DELEGATION" 
            } 
            if ([int32]$V -band 0x00000400) {
                "PIM Trust" 
            } 
        }
    )
    return $TrustAttributes
}
function Get-ForestTrustInfo { 
    <#
    .SYNOPSIS
    Retrieves and processes forest trust information from an array of bytes.
 
    .DESCRIPTION
    This function retrieves and processes forest trust information from the provided array of bytes.
 
    .PARAMETER Byte
    An array of bytes containing the forest trust information to be processed.
 
    .EXAMPLE
    Get-ForestTrustInfo -Byte $ByteData
    Retrieves and processes forest trust information from the specified array of bytes.
 
    .NOTES
    Author: Chris Dent
    #>

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

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

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

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

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

    enum RecordType {
        ForestTrustTopLevelName
        ForestTrustTopLevelNameEx
        ForestTrustDomainInfo
    }


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

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

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

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

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

    $record
}
function Assert-InitialSettings {
    [cmdletbinding()]
    param(
        [System.Collections.IDictionary] $DisableOnlyIf,
        [System.Collections.IDictionary] $MoveOnlyIf,
        [System.Collections.IDictionary] $DeleteOnlyIf
    )

    $AzureRequired = $false
    $IntuneRequired = $false
    $JamfRequired = $false

    if ($DisableOnlyIf) {
        if ($null -ne $DisableOnlyIf.LastSyncAzureMoreThan -or $null -ne $DisableOnlyIf.LastSeenAzureMoreThan) {
            $AzureRequired = $true
        }
        if ($null -ne $DisableOnlyIf.LastContactJamfMoreThan) {
            $JamfRequired = $true
        }
        if ($null -ne $DisableOnlyIf.LastSeenIntuneMoreThan) {
            $IntuneRequired = $true
        }
    }
    if ($MoveOnlyIf) {
        if ($null -ne $MoveOnlyIf.LastSyncAzureMoreThan -or $null -ne $MoveOnlyIf.LastSeenAzureMoreThan) {
            $AzureRequired = $true
        }
        if ($null -ne $MoveOnlyIf.LastContactJamfMoreThan) {
            $JamfRequired = $true
        }
        if ($null -ne $MoveOnlyIf.LastSeenIntuneMoreThan) {
            $IntuneRequired = $true
        }
    }
    if ($DeleteOnlyIf) {
        if ($null -ne $DeleteOnlyIf.LastSyncAzureMoreThan -or $null -ne $DeleteOnlyIf.LastSeenAzureMoreThan) {
            $AzureRequired = $true
        }
        if ($null -ne $DeleteOnlyIf.LastContactJamfMoreThan) {
            $JamfRequired = $true
        }
        if ($null -ne $DeleteOnlyIf.LastSeenIntuneMoreThan) {
            $IntuneRequired = $true
        }
    }

    if ($AzureRequired -or $IntuneRequired) {
        $ModuleAvailable = Get-Module -Name GraphEssentials -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1
        if (-not $ModuleAvailable) {
            Write-Color -Text "[e] ", "'GraphEssentials' module is required but not available. Terminating." -Color Yellow, Red
            return $false
        }
        $ModuleVersion = [version]'0.0.46'
        if ($ModuleAvailable.Version -lt $ModuleVersion) {
            Write-Color -Text "[e] ", "'GraphEssentials' module is outdated. Please update to the latest version minimum '$ModuleVersion'. Terminating." -Color Yellow, Red
            return $false
        }
    }
    if ($JamfRequired) {
        $ModuleAvailable = Get-Module -Name PowerJamf -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1
        if (-not $ModuleAvailable) {
            Write-Color -Text "[e] ", "'PowerJamf' module is required but not available. Terminating." -Color Yellow, Red
            return $false
        }
        $ModuleVersion = [version]'0.3.1'
        if ($ModuleAvailable.Version -lt $ModuleVersion) {
            Write-Color -Text "[e] ", "'PowerJamf' module is outdated. Please update to the latest version minimum '$ModuleVersion'. Terminating." -Color Yellow, Red
            return $false
        }
    }
}
function Convert-ListProcessed {
    <#
    .SYNOPSIS
    This function converts old format of writting down pending list with DN to new format with SamAccountName with domain.
 
    .DESCRIPTION
    This function converts old format of writting down pending list with DN to new format with SamAccountName with domain.
    The old way would probably break if someone would move computer after disabling which would cause the computer to be removed from the list.
 
    .PARAMETER FileImport
    Hashtable with PendingDeletion and History keys.
 
    .EXAMPLE
    $FileImport = Import-Clixml -LiteralPath $DataStorePath -ErrorAction Stop
    # convert old format to new format
    $FileImport = Convert-ListProcessed -FileImport $FileImport
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $FileImport
    )
    if (-not $FileImport.Contains('PendingDeletion') -and $FileImport.PendingDeletion.Keys.Count -eq 0) {
        return $FileImport
    }
    if ($FileImport.PendingDeletion.Keys[0] -like "*@*") {
        # Write-Color -Text "[i] ", "List is already converted. Terminating." -Color Yellow, Green
        return $FileImport
    }
    Write-Color -Text "[i] ", "Converting list to new format." -Color Yellow, Green
    foreach ($Key in [string[]] $FileImport.PendingDeletion.Keys) {
        $DomainName = ConvertFrom-DistinguishedName -DistinguishedName $FileImport.PendingDeletion[$Key].DistinguishedName -ToDomainCN
        $NewKey = -join ($FileImport.PendingDeletion[$Key].SamAccountName, "@", $DomainName)
        $FileImport.PendingDeletion[$NewKey] = $FileImport.PendingDeletion[$Key]
        $null = $FileImport.PendingDeletion.Remove($Key)
    }
    Write-Color -Text "[i] ", "List converted." -Color Yellow, Green
    $FileImport
}
function ConvertTo-PreparedComputer {
    [CmdletBinding()]
    param(
        [Microsoft.ActiveDirectory.Management.ADComputer[]] $Computers,
        [System.Collections.IDictionary] $AzureInformationCache,
        [System.Collections.IDictionary] $JamfInformationCache,
        [switch] $IncludeAzureAD,
        [switch] $IncludeIntune,
        [switch] $IncludeJamf
    )

    foreach ($Computer in $Computers) {
        if ($IncludeAzureAD) {
            $AzureADComputer = $AzureInformationCache['AzureAD']["$($Computer.Name)"]
            $DataAzureAD = [ordered] @{
                'AzureLastSeen'     = $AzureADComputer.LastSeen
                'AzureLastSeenDays' = $AzureADComputer.LastSeenDays
                'AzureLastSync'     = $AzureADComputer.LastSynchronized
                'AzureLastSyncDays' = $AzureADComputer.LastSynchronizedDays
                'AzureOwner'        = $AzureADComputer.OwnerDisplayName
                'AzureOwnerStatus'  = $AzureADComputer.OwnerEnabled
                'AzureOwnerUPN'     = $AzureADComputer.OwnerUserPrincipalName
            }
        }
        if ($IncludeIntune) {
            # data was requested from Intune
            $IntuneComputer = $AzureInformationCache['Intune']["$($Computer.Name)"]
            $DataIntune = [ordered] @{
                'IntuneLastSeen'     = $IntuneComputer.LastSeen
                'IntuneLastSeenDays' = $IntuneComputer.LastSeenDays
                'IntuneUser'         = $IntuneComputer.UserDisplayName
                'IntuneUserUPN'      = $IntuneComputer.UserPrincipalName
                'IntuneUserEmail'    = $IntuneComputer.EmailAddress
            }
        }
        if ($IncludeJamf) {
            $JamfComputer = $JamfInformationCache["$($Computer.Name)"]
            $DataJamf = [ordered] @{
                JamfLastContactTime     = $JamfComputer.lastContactTime
                JamfLastContactTimeDays = $JamfComputer.lastContactTimeDays
                JamfCapableUsers        = $JamfComputer.mdmCapableCapableUsers
            }
        }
        $LastLogonDays = if ($null -ne $Computer.LastLogonDate) {
            - $($Computer.LastLogonDate - $Today).Days
        } else {
            $null
        }
        $PasswordLastChangedDays = if ($null -ne $Computer.PasswordLastSet) {
            - $($Computer.PasswordLastSet - $Today).Days
        } else {
            $null
        }


        $DataStart = [ordered] @{
            'DNSHostName'                     = $Computer.DNSHostName
            'SamAccountName'                  = $Computer.SamAccountName
            'DomainName'                      = ConvertFrom-DistinguishedName -DistinguishedName $Computer.DistinguishedName -ToDomainCN
            'Enabled'                         = $Computer.Enabled
            'Action'                          = 'Not required'
            'ActionStatus'                    = $null
            'ActionDate'                      = $null
            'ActionComment'                   = $null
            'OperatingSystem'                 = $Computer.OperatingSystem
            'OperatingSystemVersion'          = $Computer.OperatingSystemVersion
            'OperatingSystemLong'             = ConvertTo-OperatingSystem -OperatingSystem $Computer.OperatingSystem -OperatingSystemVersion $Computer.OperatingSystemVersion
            'LastLogonDate'                   = $Computer.LastLogonDate
            'LastLogonDays'                   = $LastLogonDays
            'PasswordLastSet'                 = $Computer.PasswordLastSet
            'PasswordLastChangedDays'         = $PasswordLastChangedDays
            'ProtectedFromAccidentalDeletion' = $Computer.ProtectedFromAccidentalDeletion
        }
        $DataEnd = [ordered] @{
            'PasswordExpired'            = $Computer.PasswordExpired
            'LogonCount'                 = $Computer.logonCount
            'ManagedBy'                  = $Computer.ManagedBy
            'DistinguishedName'          = $Computer.DistinguishedName
            'OrganizationalUnit'         = ConvertFrom-DistinguishedName -DistinguishedName $Computer.DistinguishedName -ToOrganizationalUnit
            'Description'                = $Computer.Description
            'WhenCreated'                = $Computer.WhenCreated
            'WhenChanged'                = $Computer.WhenChanged
            'ServicePrincipalName'       = $Computer.servicePrincipalName #-join [System.Environment]::NewLine
            'DistinguishedNameAfterMove' = $null
            'TimeOnPendingList'          = $null
            'TimeToLeavePendingList'     = $null
        }
        if ($IncludeAzureAD -and $IncludeIntune -and $IncludeJamf) {
            $Data = $DataStart + $DataAzureAD + $DataIntune + $DataJamf + $DataEnd
        } elseif ($IncludeAzureAD -and $IncludeIntune) {
            $Data = $DataStart + $DataAzureAD + $DataIntune + $DataEnd
        } elseif ($IncludeAzureAD -and $IncludeJamf) {
            $Data = $DataStart + $DataAzureAD + $DataJamf + $DataEnd
        } elseif ($IncludeIntune -and $IncludeJamf) {
            $Data = $DataStart + $DataIntune + $DataJamf + $DataEnd
        } elseif ($IncludeAzureAD) {
            $Data = $DataStart + $DataAzureAD + $DataEnd
        } elseif ($IncludeIntune) {
            $Data = $DataStart + $DataIntune + $DataEnd
        } elseif ($IncludeJamf) {
            $Data = $DataStart + $DataJamf + $DataEnd
        } else {
            $Data = $DataStart + $DataEnd
        }
        [PSCustomObject] $Data
    }
}
function Disable-WinADComputer {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [bool] $Success,
        [switch] $WhatIfDisable,
        [switch] $DontWriteToEventLog,
        [PSCustomObject] $Computer,
        [string] $Server
    )
    if ($Success) {
        if ($Computer.Enabled -eq $true) {
            Write-Color -Text "[i] Disabling computer ", $Computer.SamAccountName, ' DN: ', $Computer.DistinguishedName, ' Enabled: ', $Computer.Enabled, ' Operating System: ', $Computer.OperatingSystem, ' LastLogon: ', $Computer.LastLogonDate, " / " , $Computer.LastLogonDays , ' days, PasswordLastSet: ', $Computer.PasswordLastSet, " / ", $Computer.PasswordLastChangedDays, " days" -Color Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green
            try {
                if ($Computer.DistinguishedNameAfterMove) {
                    $DN = $Computer.DistinguishedNameAfterMove
                } else {
                    $DN = $Computer.DistinguishedName
                }

                Disable-ADAccount -Identity $DN -Server $Server -WhatIf:$WhatIfDisable -ErrorAction Stop
                Write-Color -Text "[+] Disabling computer ", $DN, " (WhatIf: $WhatIfDisable) successful." -Color Yellow, Green, Yellow
                if (-not $DontWriteToEventLog) {
                    Write-Event -ID 10 -LogName 'Application' -EntryType Information -Category 1000 -Source 'CleanupComputers' -Message "Disabling computer $($Computer.SamAccountName) successful." -AdditionalFields @('Disable', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDisable) -WarningAction SilentlyContinue -WarningVariable warnings
                }
                foreach ($W in $Warnings) {
                    Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red
                }
                $Success = $true
            } catch {
                $Computer.ActionComment = $_.Exception.Message
                $Success = $false
                Write-Color -Text "[-] Disabling computer ", $DN, " (WhatIf: $WhatIfDisable) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow
                if (-not $DontWriteToEventLog) {
                    Write-Event -ID 10 -LogName 'Application' -EntryType Error -Category 1001 -Source 'CleanupComputers' -Message "Disabling computer $($Computer.SamAccountName) failed. Error: $($_.Exception.Message)" -AdditionalFields @('Disable', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDisable, $($_.Exception.Message)) -WarningAction SilentlyContinue -WarningVariable warnings
                }
                foreach ($W in $Warnings) {
                    Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red
                }
            }
        } else {
            Write-Color -Text "[i] Computer ", $Computer.SamAccountName, " is already disabled." -Color Yellow, Green, Yellow
        }
    }
    $Success
}
function Get-ADComputersToProcess {
    [CmdletBinding()]
    param(
        [parameter(Mandatory)][ValidateSet('Disable', 'Move', 'Delete')][string] $Type,
        [Array] $Computers,
        [alias('DeleteOnlyIf', 'DisableOnlyIf', 'MoveOnlyIf')][System.Collections.IDictionary] $ActionIf,
        [Array] $Exclusions,
        [System.Collections.IDictionary] $DomainInformation,
        [System.Collections.IDictionary] $ProcessedComputers,
        [System.Collections.IDictionary] $AzureInformationCache,
        [System.Collections.IDictionary] $JamfInformationCache,
        [switch] $IncludeAzureAD,
        [switch] $IncludeIntune,
        [switch] $IncludeJamf
    )
    Write-Color -Text "[i] ", "Applying following rules to $Type action: " -Color Yellow, Cyan, Green
    foreach ($Key in $ActionIf.Keys) {
        if ($null -eq $ActionIf[$Key] -or $ActionIf[$Key].Count -eq 0) {
            Write-Color -Text " [>] ", $($Key), " is ", 'Not Set' -Color Yellow, Cyan, Yellow
        } else {
            if ($Key -in 'LastLogonDateMoreThan', 'LastLogonDateOlderThan') {
                Write-Color -Text " [>] ", $($Key), " is ", $($ActionIf[$Key]), " or ", "Never logged on" -Color Yellow, Cyan, Green
            } elseif ($Key -in 'PasswordLastSetMoreThan', 'PasswordLastSetOlderThan') {
                Write-Color -Text " [>] ", $($Key), " is ", $($ActionIf[$Key]), " or ", "Never changed" -Color Yellow, Cyan, Green
            } elseif ($Key -in 'LastSeenAzureMoreThan', 'LastSeenIntuneMoreThan', 'LastSyncAzureMoreThan', 'LastContactJamfMoreThan') {
                Write-Color -Text " [>] ", $($Key), " is ", $($ActionIf[$Key]), " or ", "Never synced/seen" -Color Yellow, Cyan, Green
            } else {
                Write-Color -Text " [>] ", $($Key), " is ", $($ActionIf[$Key]) -Color Yellow, Cyan, Green
            }
        }
    }
    $Count = 0
    $Today = Get-Date

    # Let's cache the destination move, if we have them
    $CachedDestinationMove = @{}
    if ($ActionIf.MoveTargetOrganizationalUnit -is [string]) {
        $Domain = ConvertFrom-DistinguishedName -DistinguishedName $ActionIf.MoveTargetOrganizationalUnit -ToDomainCN
        $CachedDestinationMove[$ActionIf.MoveTargetOrganizationalUnit] = $Domain
    } elseif ($ActionIf.MoveTargetOrganizationalUnit -is [System.Collections.IDictionary]) {
        foreach ($Domain in $ActionIf.MoveTargetOrganizationalUnit.Keys) {
            $OU = $ActionIf.MoveTargetOrganizationalUnit[$Domain]
            $CachedDestinationMove[$OU] = $Domain
        }
    }

    Write-Color -Text "[i] ", "Looking for computers to $Type" -Color Yellow, Cyan, Green
    :SkipComputer foreach ($Computer in $Computers) {
        if ($Type -eq 'Delete') {
            # actions to happen only if we are deleting computers
            if ($null -ne $ActionIf.ListProcessedMoreThan) {
                # if more then 0 this means computer has to be on list of disabled computers for that number of days.
                if ($ProcessedComputers.Count -gt 0) {
                    $FullComputerName = "$($Computer.SamAccountName)@$($Computer.DomainName)"
                    $FoundComputer = $ProcessedComputers[$FullComputerName]
                    if ($FoundComputer) {
                        if ($FoundComputer.ActionDate -is [DateTime]) {
                            $TimeSpan = New-TimeSpan -Start $FoundComputer.ActionDate -End $Today
                            # Lets calculate how many days it's been on the list
                            $ProcessedComputers[$FullComputerName].TimeToLeavePendingList = $ActionIf.ListProcessedMoreThan - $TimeSpan.Days
                            if ($TimeSpan.Days -gt $ActionIf.ListProcessedMoreThan) {

                            } else {
                                continue SkipComputer
                            }
                        } else {
                            continue SkipComputer
                        }
                    } else {
                        continue SkipComputer
                    }
                } else {
                    # ListProcessed doesn't have members, and it's part of requirement
                    break
                }
            }
        }
        if ($Type -eq 'Disable') {
            # actions to happen only if we are disabling computers
            if ($ProcessedComputers.Count -gt 0) {
                $FullComputerName = "$($Computer.SamAccountName)@$($Computer.DomainName)"
                $FoundComputer = $ProcessedComputers[$FullComputerName]
                if ($FoundComputer) {
                    if ($Computer.Enabled -eq $true) {
                        # We checked and it seems the computer has been enabled since it was added to list, we remove it from the list and reprocess
                        Write-Color -Text "[*] Removing computer from pending list (computer is enabled) ", $FoundComputer.SamAccountName, " ($($FoundComputer.DistinguishedName))" -Color DarkYellow, Green, DarkYellow
                        $ProcessedComputers.Remove($FullComputerName)
                    } elseif ($ActionIf.DisableAndMove -and $Computer.Enabled -eq $false) {
                        if ($ActionIf.MoveTargetOrganizationalUnit) {
                            if (-not $CachedDestinationMove[$Computer.OrganizationalUnit]) {
                                # We checked and it seems the computer has been moved since it was added to list, we remove it from the list and reprocess
                                Write-Color -Text "[*] Removing computer from pending list (computer is moved out of pending deletion OU) ", $FoundComputer.SamAccountName, " ($($FoundComputer.DistinguishedName))" -Color DarkYellow, Green, DarkYellow
                                $ProcessedComputers.Remove($FullComputerName)
                            } else {
                                # We checked and it seems the computer is in place where it's supposed to, we skip to next computer
                                continue SkipComputer
                            }
                        } else {
                            # We checked and it seems the computer is in place where it's supposed to, we skip to next computer
                            continue SkipComputer
                        }
                    } else {
                        # we skip adding to disabled because it's already on the list for removing
                        continue SkipComputer
                    }
                }
            }
        }

        # rest of actions are same for all types
        foreach ($PartialExclusion in $Exclusions) {
            if ($Computer.DistinguishedName -like "$PartialExclusion") {
                $Computer.'Action' = 'ExcludedByFilter'
                continue SkipComputer
            }
            if ($Computer.SamAccountName -like "$PartialExclusion") {
                $Computer.'Action' = 'ExcludedByFilter'
                continue SkipComputer
            }
            if ($Computer.DNSHostName -like "$PartialExclusion") {
                $Computer.'Action' = 'ExcludedByFilter'
                continue SkipComputer
            }
        }
        if ($ActionIf.IncludeSystems.Count -gt 0) {
            $FoundInclude = $false
            foreach ($Include in $ActionIf.IncludeSystems) {
                if ($Computer.OperatingSystem -like $Include) {
                    $FoundInclude = $true
                    break
                }
            }
            # If not found in includes we need to skip the computer
            if (-not $FoundInclude) {
                $Computer.'Action' = 'ExcludedBySetting'
                continue SkipComputer
            }
        }
        if ($ActionIf.ExcludeServicePrincipalName.Count -gt 0) {
            foreach ($ExcludeSPN in $ActionIf.ExcludeServicePrincipalName) {
                if ($Computer.servicePrincipalName -like "$ExcludeSPN") {
                    $Computer.'Action' = 'ExcludedBySetting'
                    continue SkipComputer
                }
            }
        }
        if ($ActionIf.IncludeServicePrincipalName.Count -gt 0) {
            $FoundInclude = $false
            foreach ($IncludeSPN in $ActionIf.IncludeServicePrincipalName) {
                if ($Computer.servicePrincipalName -like "$IncludeSPN") {
                    $FoundInclude = $true
                    break
                }
            }
            # If not found in includes we need to skip the computer
            if (-not $FoundInclude) {
                $Computer.'Action' = 'ExcludedBySetting'
                continue SkipComputer
            }
        }
        if ($ActionIf.ExcludeSystems.Count -gt 0) {
            foreach ($Exclude in $ActionIf.ExcludeSystems) {
                if ($Computer.OperatingSystem -like $Exclude) {
                    $Computer.'Action' = 'ExcludedBySetting'
                    continue SkipComputer
                }
            }
        }
        if ($ActionIf.NoServicePrincipalName -eq $true) {
            # action computer only if it has no service principal names defined
            if ($Computer.servicePrincipalName.Count -gt 0) {
                $Computer.'Action' = 'ExcludedBySetting'
                continue SkipComputer
            }
        } elseif ($ActionIf.NoServicePrincipalName -eq $false) {
            # action computer only if it has service principal names defined
            if ($Computer.servicePrincipalName.Count -eq 0) {
                $Computer.'Action' = 'ExcludedBySetting'
                continue SkipComputer
            }
        }
        if ($ActionIf.RequireWhenCreatedMoreThan) {
            # This runs only if more than 0
            if ($Computer.WhenCreated) {
                # We ignore empty

                $TimeToCompare = ($Computer.WhenCreated).AddDays($ActionIf.RequireWhenCreatedMoreThan)
                if ($TimeToCompare -gt $Today) {
                    continue SkipComputer
                }
            }
        }
        if ($ActionIf.IsEnabled -eq $true) {
            # action computer only if it's Enabled
            if ($Computer.Enabled -eq $false) {
                continue SkipComputer
            }
        } elseif ($ActionIf.IsEnabled -eq $false) {
            # action computer only if it's Disabled
            if ($Computer.Enabled -eq $true) {
                continue SkipComputer
            }
        }

        if ($ActionIf.LastLogonDateMoreThan) {
            # This runs only if more than 0
            if ($Computer.LastLogonDate) {
                # We ignore empty

                $TimeToCompare = ($Computer.LastLogonDate).AddDays($ActionIf.LastLogonDateMoreThan)
                if ($TimeToCompare -gt $Today) {
                    continue SkipComputer
                }
            }
        }
        if ($ActionIf.PasswordLastSetMoreThan) {
            # This runs only if more than 0
            if ($Computer.PasswordLastSet) {
                # We ignore empty

                $TimeToCompare = ($Computer.PasswordLastSet).AddDays($ActionIf.PasswordLastSetMoreThan)
                if ($TimeToCompare -gt $Today) {
                    continue SkipComputer
                }
            }
        }

        if ($ActionIf.PasswordLastSetOlderThan) {
            # This runs only if not null
            if ($Computer.PasswordLastSet) {
                # We ignore empty

                if ($ActionIf.PasswordLastSetOlderThan -le $Computer.PasswordLastSet) {
                    continue SkipComputer
                }
            }
        }
        if ($ActionIf.LastLogonDateOlderThan) {
            # This runs only if not null
            if ($Computer.LastLogonDate) {
                # We ignore empty

                if ($ActionIf.LastLogonDateOlderThan -le $Computer.LastLogonDate) {
                    continue SkipComputer
                }
            }
        }
        if ($IncludeAzureAD) {
            if ($null -ne $ActionIf.LastSeenAzureMoreThan -and $null -ne $Computer.AzureLastSeenDays) {
                if ($Computer.AzureLastSeenDays -le $ActionIf.LastSeenAzureMoreThan) {
                    continue SkipComputer
                }

            }
            if ($null -ne $ActionIf.LastSyncAzureMoreThan -and $null -ne $Computer.AzureLastSyncDays) {
                if ($Computer.AzureLastSyncDays -le $ActionIf.LastSyncAzureMoreThan) {
                    continue SkipComputer
                }
            }
        }
        if ($IncludeIntune) {
            if ($null -ne $ActionIf.LastSeenIntuneMoreThan -and $null -ne $Computer.IntuneLastSeenDays) {
                if ($Computer.IntuneLastSeenDays -le $ActionIf.LastSeenIntuneMoreThan) {
                    continue SkipComputer
                }
            }
        }
        if ($IncludeJamf) {
            if ($null -ne $ActionIf.LastContactJamfMoreThan -and $null -ne $Computer.JamfLastContactTimeDays) {
                if ($Computer.JamfLastContactTimeDays -le $ActionIf.LastContactJamfMoreThan) {
                    continue SkipComputer
                }
            }
        }
        $Computer.'Action' = $Type
        $Count++
    }
    $Count
}
function Get-InitialADComputers {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Report,
        [System.Collections.IDictionary] $ForestInformation,
        [object] $Filter,
        [object] $SearchBase,
        [string[]] $Properties,
        [bool] $Disable,
        [bool] $Delete,
        [bool] $Move,
        [System.Collections.IDictionary] $DisableOnlyIf,
        [System.Collections.IDictionary] $DeleteOnlyIf,
        [System.Collections.IDictionary] $MoveOnlyIf,
        [Array] $Exclusions,
        [System.Collections.IDictionary] $ProcessedComputers,
        [nullable[int]] $SafetyADLimit,
        [System.Collections.IDictionary] $AzureInformationCache,
        [System.Collections.IDictionary] $JamfInformationCache,
        [object] $TargetServers
    )
    $AllComputers = [ordered] @{}

    $AzureRequired = $false
    $IntuneRequired = $false
    $JamfRequired = $false

    if ($DisableOnlyIf) {
        if ($null -ne $DisableOnlyIf.LastSyncAzureMoreThan -or $null -ne $DisableOnlyIf.LastSeenAzureMoreThan) {
            $AzureRequired = $true
        }
        if ($null -ne $DisableOnlyIf.LastContactJamfMoreThan) {
            $JamfRequired = $true
        }
        if ($null -ne $DisableOnlyIf.LastSeenIntuneMoreThan) {
            $IntuneRequired = $true
        }
    }
    if ($MoveOnlyIf) {
        if ($null -ne $MoveOnlyIf.LastSyncAzureMoreThan -or $null -ne $MoveOnlyIf.LastSeenAzureMoreThan) {
            $AzureRequired = $true
        }
        if ($null -ne $MoveOnlyIf.LastContactJamfMoreThan) {
            $JamfRequired = $true
        }
        if ($null -ne $MoveOnlyIf.LastSeenIntuneMoreThan) {
            $IntuneRequired = $true
        }
    }
    if ($DeleteOnlyIf) {
        if ($null -ne $DeleteOnlyIf.LastSyncAzureMoreThan -or $null -ne $DeleteOnlyIf.LastSeenAzureMoreThan) {
            $AzureRequired = $true
        }
        if ($null -ne $DeleteOnlyIf.LastContactJamfMoreThan) {
            $JamfRequired = $true
        }
        if ($null -ne $DeleteOnlyIf.LastSeenIntuneMoreThan) {
            $IntuneRequired = $true
        }
    }

    if ($TargetServers) {
        # User provided target servers/server. If there is only one we assume user wants to use it for all domains (hopefully just one domain)
        # If there are multiple we assume user wants to use different servers for different domains using hashtable/dictionary
        # If there is no server for a domain we will use the default server, as detected
        if ($TargetServers -is [string]) {
            $TargetServer = $TargetServers
        }
        if ($TargetServers -is [System.Collections.IDictionary]) {
            $TargetServerDictionary = $TargetServers[$Domain]
        }
    }

    $CountDomains = 0
    foreach ($Domain in $ForestInformation.Domains) {
        $CountDomains++
        $Report["$Domain"] = [ordered] @{ }
        $Server = $ForestInformation['QueryServers'][$Domain].HostName[0]
        if (-not $Server) {
            Write-Color "[e] ", "No server found for domain $Domain" -Color Yellow, Red
            continue
        }
        if ($TargetServer) {
            Write-Color -Text "Overwritting target server for domain ", $Domain, ": ", $TargetServer -Color Yellow, Magenta
            $Server = $TargetServer
        } elseif ($TargetServerDictionary) {
            if ($TargetServerDictionary[$Domain]) {
                Write-Color -Text "Overwritting target server for domain ", $Domain, ": ", $TargetServerDictionary[$Domain] -Color Yellow, Magenta
                $Server = $TargetServerDictionary[$Domain]
            }
        }
        $DomainInformation = $ForestInformation.DomainsExtended[$Domain]
        $Report["$Domain"]['Server'] = $Server
        Write-Color "[i] Getting all computers for domain ", $Domain, " [", $CountDomains, "/", $ForestInformation.Domains.Count, "]", " from ", $Server -Color Yellow, Magenta, Yellow, Magenta, Yellow, Magenta, Yellow, Yellow, Magenta

        if ($Filter) {
            if ($Filter -is [string]) {
                $FilterToUse = $Filter
            } elseif ($Filter -is [System.Collections.IDictionary]) {
                $FilterToUse = $Filter[$Domain]
            } else {
                Write-Color "[e] ", "Filter must be a string or a hashtable/ordereddictionary" -Color Yellow, Red
                return $false
            }
        } else {
            $FilterToUse = "*"
        }

        if ($SearchBase) {
            if ($SearchBase -is [string]) {
                $SearchBaseToUse = $SearchBase
            } elseif ($SearchBase -is [System.Collections.IDictionary]) {
                $SearchBaseToUse = $SearchBase[$Domain]
            } else {
                Write-Color "[e] ", "SearchBase must be a string or a hashtable/ordereddictionary" -Color Yellow, Red
                return $false
            }
        } else {
            $SearchBaseToUse = $DomainInformation.DistinguishedName
        }

        $getADComputerSplat = @{
            Filter      = $FilterToUse
            Server      = $Server
            Properties  = $Properties
            ErrorAction = 'Stop'
        }
        if ($SearchBaseToUse) {
            $getADComputerSplat.SearchBase = $SearchBaseToUse
        }
        try {
            [Array] $Computers = Get-ADComputer @getADComputerSplat
        } catch {
            if ($_.Exception.Message -like "*distinguishedName must belong to one of the following partition*") {
                Write-Color "[e] ", "Error getting computers for domain $($Domain): ", $_.Exception.Message -Color Yellow, Red
                Write-Color "[e] ", "Please check if the distinguishedName for SearchBase is correct for the domain. If you have multiple domains please use Hashtable/Dictionary to provide relevant data or using IncludeDomains/ExcludeDomains functionality" -Color Yellow, Red
            } else {
                Write-Color "[e] ", "Error getting computers for domain $($Domain): ", $_.Exception.Message -Color Yellow, Red
            }
            return $false
        }
        foreach ($Computer in $Computers) {
            # we will be using it later to just check if computer exists in AD
            $DomainName = ConvertFrom-DistinguishedName -DistinguishedName $Computer.DistinguishedName -ToDomainCN
            $ComputerFullName = -join ($Computer.SamAccountName, "@", $DomainName)
            # initially we used DN, but DN changes when moving so it wouldn't work
            $AllComputers[$ComputerFullName] = $Computer
        }
        $Report["$Domain"]['Computers'] = @(
            $convertToPreparedComputerSplat = @{
                Computers             = $Computers
                AzureInformationCache = $AzureInformationCache
                JamfInformationCache  = $JamfInformationCache
                IncludeAzureAD        = $AzureRequired
                IncludeJamf           = $JamfRequired
                IncludeIntune         = $IntuneRequired
            }

            ConvertTo-PreparedComputer @convertToPreparedComputerSplat
        )
        Write-Color "[i] ", "Computers found for domain $Domain`: ", $($Computers.Count) -Color Yellow, Cyan, Green
        if ($Disable) {
            Write-Color "[i] ", "Processing computers to disable for domain $Domain" -Color Yellow, Cyan, Green
            $getADComputersToDisableSplat = @{
                Computers             = $Report["$Domain"]['Computers']
                DisableOnlyIf         = $DisableOnlyIf
                Exclusions            = $Exclusions
                DomainInformation     = $DomainInformation
                ProcessedComputers    = $ProcessedComputers
                AzureInformationCache = $AzureInformationCache
                JamfInformationCache  = $JamfInformationCache
                IncludeAzureAD        = $AzureRequired
                IncludeJamf           = $JamfRequired
                IncludeIntune         = $IntuneRequired
                Type                  = 'Disable'
            }
            $Report["$Domain"]['ComputersToBeDisabled'] = Get-ADComputersToProcess @getADComputersToDisableSplat
            #Write-Color "[i] ", "Computers to be disabled for domain $Domain`: ", $($Report["$Domain"]['ComputersToBeDisabled'].Count) -Color Yellow, Cyan, Green
        }
        if ($Move) {
            Write-Color "[i] ", "Processing computers to move for domain $Domain" -Color Yellow, Cyan, Green
            $getADComputersToDeleteSplat = @{
                Computers             = $Report["$Domain"]['Computers']
                MoveOnlyIf            = $MoveOnlyIf
                Exclusions            = $Exclusions
                DomainInformation     = $DomainInformation
                ProcessedComputers    = $ProcessedComputers
                AzureInformationCache = $AzureInformationCache
                JamfInformationCache  = $JamfInformationCache
                IncludeAzureAD        = $AzureRequired
                IncludeJamf           = $JamfRequired
                IncludeIntune         = $IntuneRequired
                Type                  = 'Move'
            }
            $Report["$Domain"]['ComputersToBeMoved'] = Get-ADComputersToProcess @getADComputersToDeleteSplat
            #Write-Color "[i] ", "Computers to be moved for domain $Domain`: ", $($Report["$Domain"]['ComputersToBeMoved'].Count) -Color Yellow, Cyan, Green
        }
        if ($Delete) {
            Write-Color "[i] ", "Processing computers to delete for domain $Domain" -Color Yellow, Cyan, Green
            $getADComputersToDeleteSplat = @{
                Computers             = $Report["$Domain"]['Computers']
                DeleteOnlyIf          = $DeleteOnlyIf
                Exclusions            = $Exclusions
                DomainInformation     = $DomainInformation
                ProcessedComputers    = $ProcessedComputers
                AzureInformationCache = $AzureInformationCache
                JamfInformationCache  = $JamfInformationCache
                IncludeAzureAD        = $AzureRequired
                IncludeJamf           = $JamfRequired
                IncludeIntune         = $IntuneRequired
                Type                  = 'Delete'
            }
            $Report["$Domain"]['ComputersToBeDeleted'] = Get-ADComputersToProcess @getADComputersToDeleteSplat
            #Write-Color "[i] ", "Computers to be deleted for domain $Domain`: ", $($Report["$Domain"]['ComputersToBeDeleted'].Count) -Color Yellow, Cyan, Green
        }
    }
    if ($null -ne $SafetyADLimit -and $AllComputers.Count -lt $SafetyADLimit) {
        Write-Color "[e] ", "Only ", $($AllComputers.Count), " computers found in AD, this is less than the safety limit of ", $SafetyADLimit, ". Terminating!" -Color Yellow, Cyan, Red, Cyan
        return $false
    }
    $AllComputers
}
function Get-InitialGraphComputers {
    [CmdletBinding()]
    param(
        [nullable[int]] $SafetyAzureADLimit,
        [nullable[int]] $SafetyIntuneLimit,
        [nullable[int]] $DeleteLastSeenAzureMoreThan,
        [nullable[int]] $DeleteLastSeenIntuneMoreThan,
        [nullable[int]] $DeleteLastSyncAzureMoreThan,
        [nullable[int]] $DisableLastSeenAzureMoreThan,
        [nullable[int]] $DisableLastSeenIntuneMoreThan,
        [nullable[int]] $DisableLastSyncAzureMoreThan,
        [nullable[int]] $MoveLastSeenAzureMoreThan,
        [nullable[int]] $MoveLastSeenIntuneMoreThan,
        [nullable[int]] $MoveLastSyncAzureMoreThan
    )

    $AzureInformationCache = [ordered] @{
        AzureAD = [ordered] @{}
        Intune  = [ordered] @{}
    }

    if ($PSBoundParameters.ContainsKey('DisableLastSeenAzureMoreThan') -or
        $PSBoundParameters.ContainsKey('DisableLastSyncAzureMoreThan') -or
        $PSBoundParameters.ContainsKey('DeleteLastSeenAzureMoreThan') -or
        $PSBoundParameters.ContainsKey('DeleteLastSyncAzureMoreThan') -or
        $PSBoundParameters.ContainsKey('MoveLastSeenAzureMoreThan') -or
        $PSBoundParameters.ContainsKey('MoveLastSyncAzureMoreThan')) {
        Write-Color "[i] ", "Getting all computers from AzureAD" -Color Yellow, Cyan, Green

        [Array] $Devices = Get-MyDevice -Synchronized -WarningAction SilentlyContinue -WarningVariable WarningVar
        if ($WarningVar) {
            Write-Color "[e] ", "Error getting computers from AzureAD: ", $WarningVar, " Terminating!" -Color Yellow, Red, Yellow, Red
            return $false
        }
        if ($Devices.Count -eq 0) {
            Write-Color "[e] ", "No computers found in AzureAD, terminating! Please disable Azure AD integration or fix connectivity." -Color Yellow, Red
            return $false
        }
        foreach ($Device in $Devices) {
            $AzureInformationCache.AzureAD[$Device.Name] = $Device
        }

        if ($null -ne $SafetyAzureADLimit -and $Devices.Count -lt $SafetyAzureADLimit) {
            Write-Color "[e] ", "Only ", $($Devices.Count), " computers found in AzureAD, this is less than the safety limit of ", $SafetyAzureADLimit, ". Terminating!" -Color Yellow, Cyan, Red, Cyan
            return $false
        }
        Write-Color "[i] ", "Synchronized Computers found in AzureAD`: ", $($Devices.Count) -Color Yellow, Cyan, Green
    }
    if ($PSBoundParameters.ContainsKey('DisableLastSeenIntuneMoreThan') -or
        $PSBoundParameters.ContainsKey('DeleteLastSeenIntuneMoreThan') -or
        $PSBoundParameters.ContainsKey('MoveLastSeenIntuneMoreThan')) {
        Write-Color "[i] ", "Getting all computers from Intune" -Color Yellow, Cyan, Green

        [Array] $DevicesIntune = Get-MyDeviceIntune -WarningAction SilentlyContinue -WarningVariable WarningVar -Synchronized
        if ($WarningVar) {
            Write-Color "[e] ", "Error getting computers from Intune: ", $WarningVar, " Terminating!" -Color Yellow, Red, Yellow, Red
            return $false
        }
        if ($DevicesIntune.Count -eq 0) {
            Write-Color "[e] ", "No computers found in Intune, terminating! Please disable Intune integration or fix connectivity." -Color Yellow, Red
            return $false
        }

        foreach ($device in $DevicesIntune) {
            $AzureInformationCache.Intune[$Device.Name] = $device
        }

        if ($null -ne $SafetyIntuneLimit -and $DevicesIntune.Count -lt $SafetyIntuneLimit) {
            Write-Color "[e] ", "Only ", $($DevicesIntune.Count), " computers found in Intune, this is less than the safety limit of ", $SafetyIntuneLimit, ". Terminating!" -Color Yellow, Cyan, Red, Cyan
            return $false
        }

        Write-Color "[i] ", "Synchronized Computers found in Intune`: ", $($DevicesIntune.Count) -Color Yellow, Cyan, Green
    }

    $AzureInformationCache
}
function Get-InitialJamfComputers {
    [CmdletBinding()]
    param(
        [bool] $DisableLastContactJamfMoreThan,
        [bool] $MoveLastContactJamfMoreThan,
        [bool] $DeleteLastContactJamfMoreThan,
        [nullable[int]] $SafetyJamfLimit
    )
    $JamfCache = [ordered] @{}
    if ($PSBoundParameters.ContainsKey('DisableLastContactJamfMoreThan') -or
        $PSBoundParameters.ContainsKey('DeleteLastContactJamfMoreThan') -or
        $PSBoundParameters.ContainsKey('MoveLastContactJamfMoreThan')
    ) {
        Write-Color "[i] ", "Getting all computers from Jamf" -Color Yellow, Cyan, Green
        [Array] $Jamf = Get-JamfDevice -WarningAction SilentlyContinue -WarningVariable WarningVar
        if ($WarningVar) {
            Write-Color "[e] ", "Error getting computers from Jamf: ", $WarningVar, " Terminating!" -Color Yellow, Red, Yellow, Red
            return $false
        }
        if ($Jamf.Count -eq 0) {
            Write-Color "[e] ", "No computers found in Jamf, terminating! Please disable Jamf integration or fix connectivity." -Color Yellow, Red
            return $false
        } else {
            Write-Color "[i] ", "Computers found in Jamf`: ", $($Jamf.Count) -Color Yellow, Cyan, Green
        }

        if ($null -ne $SafetyJamfLimit -and $Jamf.Count -lt $SafetyJamfLimit) {
            Write-Color "[e] ", "Only ", $($Jamf.Count), " computers found in Jamf, this is less than the safety limit of ", $SafetyJamfLimit, ". Terminating!" -Color Yellow, Cyan, Red, Cyan
            return $false
        }

        foreach ($device in $Jamf) {
            $JamfCache[$Device.Name] = $device
        }
    }
    $JamfCache
}
function Import-ComputersData {
    [CmdletBinding()]
    param(
        [string] $DataStorePath,
        [System.Collections.IDictionary] $Export
    )

    $ProcessedComputers = [ordered] @{ }
    $Today = Get-Date
    try {
        if ($DataStorePath -and (Test-Path -LiteralPath $DataStorePath -ErrorAction Stop)) {
            $FileImport = Import-Clixml -LiteralPath $DataStorePath -ErrorAction Stop
            # convert old format to new format
            $FileImport = Convert-ListProcessed -FileImport $FileImport

            if ($FileImport.PendingDeletion) {
                if ($FileImport.PendingDeletion.GetType().Name -notin 'Hashtable', 'OrderedDictionary') {
                    Write-Color -Text "[e] ", "Incorrect XML format. PendingDeletion is not a hashtable/ordereddictionary. Terminating." -Color Yellow, Red
                    return $false
                }
            }
            if ($FileImport.History) {
                if ($FileImport.History.GetType().Name -ne 'ArrayList') {
                    Write-Color -Text "[e] ", "Incorrect XML format. History is not a ArrayList. Terminating." -Color Yellow, Red
                    return $False
                }
            }
            $ProcessedComputers = $FileImport.PendingDeletion
            foreach ($ComputerFullName in $ProcessedComputers.Keys) {
                $Computer = $ProcessedComputers[$ComputerFullName]
                if ($Computer.PSObject.Properties.Name -notcontains 'TimeOnPendingList') {
                    $TimeOnPendingList = if ($Computer.ActionDate) {
                        - $($Computer.ActionDate - $Today).Days
                    } else {
                        $null
                    }
                    # We need to add this property to the object, as it may not exist on the old exports
                    Add-Member -MemberType NoteProperty -Name 'TimeOnPendingList' -Value $TimeOnPendingList -Force -InputObject $Computer
                    Add-Member -MemberType NoteProperty -Name 'TimeToLeavePendingList' -Value $null -Force -InputObject $Computer
                } else {
                    $TimeOnPendingList = if ($Computer.ActionDate) {
                        - $($Computer.ActionDate - $Today).Days
                    } else {
                        $null
                    }
                    $Computer.TimeOnPendingList = $TimeOnPendingList
                }
            }
            $Export['History'] = $FileImport.History
        }
        if (-not $ProcessedComputers) {
            $ProcessedComputers = [ordered] @{ }
        }
    } catch {
        Write-Color -Text "[e] ", "Couldn't read the list or wrong format. Error: $($_.Exception.Message)" -Color Yellow, Red
        return $false
    }
    $ProcessedComputers
}
function Import-SIDHistory {
    [CmdletBinding()]
    param(
        [string] $DataStorePath,
        [System.Collections.IDictionary] $Export
    )
    try {
        if ($DataStorePath -and (Test-Path -LiteralPath $DataStorePath -ErrorAction Stop)) {
            $FileImport = Import-Clixml -LiteralPath $DataStorePath -ErrorAction Stop
            if ($FileImport.History) {
                $Export.History = $FileImport.History
            }
        }
    } catch {
        Write-Color -Text "[e] ", "Couldn't read the list or wrong format. Error: $($_.Exception.Message)" -Color Yellow, Red
        return $false
    }

    $Export
}
function Move-WinADComputer {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [bool] $Success,
        [bool] $DisableAndMove,
        [System.Collections.IDictionary] $OrganizationalUnit,
        [PSCustomObject] $Computer,
        [switch] $WhatIfDisable,
        [switch] $DontWriteToEventLog,
        [string] $Server
    )
    if ($Success -and $DisableAndMove) {
        # we only move if we successfully disabled the computer
        if ($OrganizationalUnit[$Domain]) {
            if ($Computer.OrganizationalUnit -eq $OrganizationalUnit[$Domain]) {
                Write-Color -Text "[i] Computer ", $Computer.DistinguishedName, " is already in the correct OU." -Color Yellow, Green, Yellow
            } else {
                try {
                    $MovedObject = Move-ADObject -Identity $Computer.DistinguishedName -WhatIf:$WhatIfDisable -Server $Server -ErrorAction Stop -Confirm:$false -TargetPath $OrganizationalUnit[$Domain] -PassThru
                    Write-Color -Text "[+] Moving computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfDisable.IsPresent)) successful." -Color Yellow, Green, Yellow
                    if (-not $DontWriteToEventLog) {
                        Write-Event -ID 11 -LogName 'Application' -EntryType Warning -Category 1000 -Source 'CleanupComputers' -Message "Moving computer $($Computer.SamAccountName) successful." -AdditionalFields @('Move', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDisable) -WarningAction SilentlyContinue -WarningVariable warnings
                    }
                    foreach ($W in $Warnings) {
                        Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red
                    }
                    $Computer.DistinguishedNameAfterMove = $MovedObject.DistinguishedName
                    $Success = $true
                } catch {
                    $Success = $false
                    Write-Color -Text "[-] Moving computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfDisable.IsPresent)) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow
                    if (-not $DontWriteToEventLog) {
                        Write-Event -ID 11 -LogName 'Application' -EntryType Error -Category 1000 -Source 'CleanupComputers' -Message "Moving computer $($Computer.SamAccountName) failed." -AdditionalFields @('Move', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDisable, $($_.Exception.Message)) -WarningAction SilentlyContinue -WarningVariable warnings
                    }
                    foreach ($W in $Warnings) {
                        Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red
                    }
                    $Computer.ActionComment = $Computer.ActionComment + [System.Environment]::NewLine + $_.Exception.Message
                }
            }
        }
    }
    $Success
}
function New-ADComputersStatistics {
    [CmdletBinding()]
    param(
        [Array] $ComputersToProcess
    )
    $Statistics = [ordered] @{
        All                          = $ComputersToProcess.Count

        ToMove                       = 0
        ToMoveComputerWorkstation    = 0
        ToMoveComputerServer         = 0
        ToMoveComputerUnknown        = 0

        ToDisable                    = 0
        ToDisableComputerUnknown     = 0
        ToDisableComputerWorkstation = 0
        ToDisableComputerServer      = 0

        ToDelete                     = 0
        ToDeleteComputerWorkstation  = 0
        ToDeleteComputerServer       = 0
        ToDeleteComputerUnknown      = 0

        TotalWindowsServers          = 0
        TotalWindowsWorkstations     = 0
        TotalMacOS                   = 0
        TotalLinux                   = 0
        TotalUnknown                 = 0

        Delete                       = [ordered] @{
            LastLogonDays           = [ordered ]@{}
            PasswordLastChangedDays = [ordered] @{}
            Systems                 = [ordered] @{}
        }
        Move                         = [ordered] @{
            LastLogonDays           = [ordered] @{}
            PasswordLastChangedDays = [ordered] @{}
            Systems                 = [ordered] @{}
        }
        Disable                      = [ordered] @{
            LastLogonDays           = [ordered] @{}
            PasswordLastChangedDays = [ordered] @{}
            Systems                 = [ordered] @{}
        }
        'Not required'               = [ordered] @{
            LastLogonDays           = [ordered] @{}
            PasswordLastChangedDays = [ordered] @{}
            Systems                 = [ordered] @{}
        }
        'ExcludedBySetting'          = [ordered] @{
            LastLogonDays           = [ordered] @{}
            PasswordLastChangedDays = [ordered] @{}
            Systems                 = [ordered] @{}
        }
        'ExcludedByFilter'           = [ordered] @{
            LastLogonDays           = [ordered] @{}
            PasswordLastChangedDays = [ordered] @{}
            Systems                 = [ordered] @{}
        }

    }
    foreach ($Computer in $ComputersToProcess) {
        if ($Computer.OperatingSystem -like "Windows Server*") {
            $Statistics.TotalWindowsServers++
        } elseif ($Computer.OperatingSystem -notlike "Windows Server*" -and $Computer.OperatingSystem -like "Windows*") {
            $Statistics.TotalWindowsWorkstations++
        } elseif ($Computer.OperatingSystem -like "Mac*") {
            $Statistics.TotalMacOS++
        } elseif ($Computer.OperatingSystem -like "Linux*") {
            $Statistics.TotalLinux++
        } else {
            $Statistics.TotalUnknown++
        }
        if ($Computer.Action -eq 'Disable') {
            $Statistics.ToDisable++
            if ($Computer.OperatingSystem -like "Windows Server*") {
                $Statistics.ToDisableComputerServer++
            } elseif ($Computer.OperatingSystem -notlike "Windows Server*" -and $Computer.OperatingSystem -like "Windows*") {
                $Statistics.ToDisableComputerWorkstation++
            } else {
                $Statistics.ToDisableComputerUnknown++
            }
        } elseif ($Computer.Action -eq 'Move') {
            $Statistics.ToMove++
            if ($Computer.OperatingSystem -like "Windows Server*") {
                $Statistics.ToMoveComputerServer++
            } elseif ($Computer.OperatingSystem -notlike "Windows Server*" -and $Computer.OperatingSystem -like "Windows*") {
                $Statistics.ToMoveComputerWorkstation++
            } else {
                $Statistics.ToMoveComputerUnknown++
            }
        } elseif ($Computer.Action -eq 'Delete') {
            $Statistics.ToDelete++
            if ($Computer.OperatingSystem -like "Windows Server*") {
                $Statistics.ToDeleteComputerServer++
            } elseif ($Computer.OperatingSystem -notlike "Windows Server*" -and $Computer.OperatingSystem -like "Windows*") {
                $Statistics.ToDeleteComputerWorkstation++
            } else {
                $Statistics.ToDeleteComputerUnknown++
            }
        }
        if ($Computer.OperatingSystem) {
            $Statistics[$Computer.Action]['Systems'][$Computer.OperatingSystem]++
        } else {
            $Statistics[$Computer.Action]['Systems']['Unknown']++
        }
        if ($Computer.LastLogonDays -gt 720) {
            $Statistics[$Computer.Action]['LastLogonDays']['Over 720 days']++
        } elseif ($Computer.LastLogonDays -gt 360) {
            $Statistics[$Computer.Action]['LastLogonDays']['Over 360 days']++
        } elseif ($Computer.LastLogonDays -gt 180) {
            $Statistics[$Computer.Action]['LastLogonDays']['Over 180 days']++
        } elseif ($Computer.LastLogonDays -gt 90) {
            $Statistics[$Computer.Action]['LastLogonDays']['Over 90 days']++
        } elseif ($Computer.LastLogonDays -gt 30) {
            $Statistics[$Computer.Action]['LastLogonDays']['Over 30 days']++
        } else {
            $Statistics[$Computer.Action]['LastLogonDays']['Under 30 days']++
        }
        if ($Computer.PasswordLastChangedDays -gt 720) {
            $Statistics[$Computer.Action]['PasswordLastChangedDays']['Over 720 days']++
        } elseif ($Computer.PasswordLastChangedDays -gt 360) {
            $Statistics[$Computer.Action]['PasswordLastChangedDays']['Over 360 days']++
        } elseif ($Computer.PasswordLastChangedDays -gt 180) {
            $Statistics[$Computer.Action]['PasswordLastChangedDays']['Over 180 days']++
        } elseif ($Computer.PasswordLastChangedDays -gt 90) {
            $Statistics[$Computer.Action]['PasswordLastChangedDays']['Over 90 days']++
        } elseif ($Computer.PasswordLastChangedDays -gt 30) {
            $Statistics[$Computer.Action]['PasswordLastChangedDays']['Over 30 days']++
        } else {
            $Statistics[$Computer.Action]['PasswordLastChangedDays']['Under 30 days']++
        }
    }
    $Statistics
}
function New-EmailBodyComputers {
    [CmdletBinding()]
    param(
        [Array] $CurrentRun
    )

    Write-Color -Text "[i] ", "Generating email body" -Color Yellow, White

    [Array] $DisabledObjects = $CurrentRun | Where-Object { $_.Action -eq 'Disable' }
    [Array] $DeletedObjects = $CurrentRun | Where-Object { $_.Action -eq 'Delete' }

    $EmailBody = EmailBody -EmailBody {
        EmailText -Text "Hello,"

        EmailText -LineBreak

        EmailText -Text "This is an automated email from Automations run on ", $Env:COMPUTERNAME, " on ", (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), " by ", $Env:UserName -Color None, Green, None, Green, None, Green -FontWeight normal, bold, normal, bold, normal, bold

        EmailText -LineBreak

        EmailText -Text "Following is a summary for the computer object cleanup:" -FontWeight bold
        EmailList {
            EmailListItem -Text "Objects actioned: ", $Output.CurrentRun.Count -Color None, Green -FontWeight normal, bold
            EmailListItem -Text "Objects deleted: ", $DeletedObjects.Count -Color None, Salmon -FontWeight normal, bold
            EmailListItem -Text "Objects disabled: ", $DisabledObjects.Count -Color None, Orange -FontWeight normal, bold
        }

        EmailText -Text "Following objects were actioned:" -LineBreak -FontWeight bold -Color Salmon
        EmailTable -DataTable $Output.CurrentRun -HideFooter {
            New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Delete' -BackGroundColor PinkLace -Inline
            New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Disable' -BackGroundColor EnergyYellow -Inline
            New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'True' -BackGroundColor LightGreen -Inline
            New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'False' -BackGroundColor Salmon -Inline
            New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Whatif' -BackGroundColor LightBlue -Inline
        }

        EmailText -LineBreak

        EmailText -Text "Regards,"
        EmailText -Text "Automations Team" -FontWeight bold
    }

    Write-Color -Text "[i] ", "Email body generated" -Color Yellow, White

    $EmailBody
}
function New-EmailBodySidHistory {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Export
    )

    $EmailBody = EmailBody -EmailBody {
        EmailText -Text "Hello,"

        EmailText -LineBreak

        EmailText -Text "This is an automated email from Automations run on ", $Env:COMPUTERNAME, " on ", (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), " by ", $Env:UserName -Color None, Green, None, Green, None, Green -FontWeight normal, bold, normal, bold, normal, bold

        EmailText -LineBreak

        New-HTMLText -Text "The following table lists all actions that were taken on given objects while removing SID History. The following statistics provide insights into processed SID history in the forest:" -FontSize 10pt

        $Enabled = $Export.CurrentRun | Where-Object { $_.Enabled }
        $Disabled = $Export.CurrentRun | Where-Object { -not $_.Enabled }

        EmailList {
            EmailListItem -Text "$($Enabled.Count)", " enabled objects" -FontWeight normal, bold
            EmailListItem -Text "$($Disabled.Count)", " disabled objects" -FontWeight normal, bold
            EmailListItem -Text "Processed ", $($Export.ProcessedObjects), " total objects" -FontWeight normal, bold, normal
            EmailListItem -Text "Processed ", $($Export.ProcessedSIDs), " total SID history values" -FontWeight normal, bold, normal
        } -FontSize 10pt

        EmailText -Text "Following objects were actioned:" -LineBreak -FontWeight bold -Color Salmon

        EmailTable -DataTable $Export.CurrentRun {
            EmailTableCondition -Name 'Enabled' -ComparisonType bool -Operator eq -Value $true -BackgroundColor MintGreen -FailBackgroundColor Salmon -Inline
            EmailTableCondition -Name 'SIDBeforeCount' -ComparisonType number -Operator gt -Value 0 -BackgroundColor LightCoral -FailBackgroundColor LightGreen -Inline
            EmailTableCondition -Name 'SIDAfterCount' -ComparisonType number -Operator eq -Value 0 -BackgroundColor LightGreen -FailBackgroundColor Salmon -Inline

            EmailTableCondition -Name 'Action' -ComparisonType string -Value 'RemoveAll' -BackgroundColor LightPink -Inline
            EmailTableCondition -Name 'Action' -ComparisonType string -Value 'RemovePerSID' -BackgroundColor LightCoral -Inline

            EmailTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Success' -BackgroundColor LightGreen -Inline
            EmailTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Failed' -BackgroundColor Salmon -Inline
            EmailTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'WhatIf' -BackgroundColor LightBlue -Inline
        } -HideFooter -PrettifyObject

        EmailText -LineBreak

        EmailText -Text "Regards,"
        EmailText -Text "Automations Team" -FontWeight bold
    }
    $EmailBody
}
function New-HTMLProcessedComputers {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Export,
        [System.Collections.IDictionary] $DisableOnlyIf,
        [System.Collections.IDictionary] $DeleteOnlyIf,
        [System.Collections.IDictionary] $MoveOnlyIf,
        [Array] $ComputersToProcess,
        [string] $FilePath,
        [switch] $Online,
        [switch] $ShowHTML,
        [string] $LogFile,
        [switch] $Disable,
        [switch] $Delete,
        [switch] $Move,
        [switch] $ReportOnly
    )

    New-HTML {
        New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey -BackgroundColor BlizzardBlue
        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 "Cleanup Monster - $($Export['Version'])" -Color Blue
                } -JustifyContent flex-end -Invisible
            }
        }
        if (-not $ReportOnly) {
            New-HTMLTab -Name 'Devices Current Run' {
                New-HTMLSection {
                    [Array] $ListAll = $($Export.CurrentRun)
                    New-HTMLPanel {
                        New-HTMLToast -TextHeader 'Total in this run' -Text "Actions (disable & delete): $($ListAll.Count)" -BarColorLeft MintGreen -IconSolid info-circle -IconColor MintGreen
                    } -Invisible

                    [Array] $ListDisabled = $($($Export.CurrentRun | Where-Object { $_.Action -eq 'Disable' }))
                    New-HTMLPanel {
                        New-HTMLToast -TextHeader 'Disable' -Text "Computers disabled: $($ListDisabled.Count)" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel
                    } -Invisible

                    [Array] $ListMoved = $($($Export.CurrentRun | Where-Object { $_.Action -eq 'Move' }))
                    New-HTMLPanel {
                        New-HTMLToast -TextHeader 'Move' -Text "Computers moved: $($ListMoved.Count)" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel
                    } -Invisible

                    [Array] $ListDeleted = $($($Export.CurrentRun | Where-Object { $_.Action -eq 'Delete' }))
                    New-HTMLPanel {
                        New-HTMLToast -TextHeader 'Delete' -Text "Computers deleted: $($ListDeleted.Count)" -BarColorLeft OrangeRed -IconSolid info-circle -IconColor OrangeRed
                    } -Invisible
                } -Invisible
                New-HTMLTable -DataTable $Export.CurrentRun -Filtering -ScrollX {
                    New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Delete' -BackgroundColor PinkLace
                    New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Move' -BackgroundColor Yellow
                    New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Disable' -BackgroundColor EnergyYellow
                    New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'True' -BackgroundColor LightGreen
                    New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'False' -BackgroundColor Salmon
                    New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Whatif' -BackgroundColor LightBlue
                    New-HTMLTableCondition -Name 'ProtectedFromAccidentalDeletion' -ComparisonType string -Value $false -BackgroundColor LightBlue -FailBackgroundColor Salmon
                } -WarningAction SilentlyContinue
            }
            New-HTMLTab -Name 'Devices History' {
                New-HTMLSection {
                    [Array] $ListAll = $($Export.History)
                    New-HTMLPanel {
                        New-HTMLToast -TextHeader 'Total History' -Text "Actions (disable & move & delete): $($ListAll.Count)" -BarColorLeft MintGreen -IconSolid info-circle -IconColor MintGreen
                    } -Invisible

                    [Array] $ListDisabled = $($($Export.History | Where-Object { $_.Action -eq 'Disable' }))
                    New-HTMLPanel {
                        New-HTMLToast -TextHeader 'Disabled History' -Text "Computers disabled so far: $($ListDisabled.Count)" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel
                    } -Invisible

                    [Array] $ListMoved = $($($Export.History | Where-Object { $_.Action -eq 'Move' }))
                    New-HTMLPanel {
                        New-HTMLToast -TextHeader 'Moved History' -Text "Computers moved so far: $($ListMoved.Count)" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel
                    } -Invisible

                    [Array] $ListDeleted = $($($Export.History | Where-Object { $_.Action -eq 'Delete' }))
                    New-HTMLPanel {
                        New-HTMLToast -TextHeader 'Deleted History' -Text "Computers deleted so far: $($ListDeleted.Count)" -BarColorLeft OrangeRed -IconSolid info-circle -IconColor OrangeRed
                    } -Invisible

                } -Invisible
                New-HTMLTable -DataTable $Export.History -Filtering -ScrollX {
                    New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Delete' -BackgroundColor PinkLace
                    New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Move' -BackgroundColor Yellow
                    New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Disable' -BackgroundColor EnergyYellow
                    New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'True' -BackgroundColor LightGreen
                    New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'False' -BackgroundColor Salmon
                    New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Whatif' -BackgroundColor LightBlue
                } -WarningAction SilentlyContinue -AllProperties
            }
            New-HTMLTab -Name 'Devices Pending' {
                New-HTMLTable -DataTable $Export.PendingDeletion.Values -Filtering -ScrollX {
                    New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Delete' -BackgroundColor PinkLace
                    New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Move' -BackgroundColor Yellow
                    New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Disable' -BackgroundColor EnergyYellow
                    New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'True' -BackgroundColor LightGreen
                    New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'False' -BackgroundColor Salmon
                    New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Whatif' -BackgroundColor LightBlue
                    New-HTMLTableCondition -Name 'ProtectedFromAccidentalDeletion' -ComparisonType string -Value $false -BackgroundColor LightBlue -FailBackgroundColor Salmon
                } -WarningAction SilentlyContinue -AllProperties
            }
        }
        New-HTMLTab -Name 'Devices' {
            New-HTMLSection {
                New-HTMLPanel {
                    New-HTMLToast -TextHeader 'Total' -Text "Computers Total: $($ComputersToProcess.Count)" -BarColorLeft MintGreen -IconSolid info-circle -IconColor MintGreen
                } -Invisible
                New-HTMLPanel {
                    New-HTMLToast -TextHeader 'To disable' -Text "Computers to be disabled: $($Export.Statistics.ToDisable)" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel
                } -Invisible
                New-HTMLPanel {
                    New-HTMLToast -TextHeader 'To move' -Text "Computers to be moved: $($Export.Statistics.ToMove)" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel
                } -Invisible
                New-HTMLPanel {
                    New-HTMLToast -TextHeader 'To delete' -Text "Computers to be deleted: $($Export.Statistics.ToDelete)" -BarColorLeft OrangeRed -IconSolid info-circle -IconColor OrangeRed
                } -Invisible
            } -Invisible
            New-HTMLSection -HeaderText 'General statistics' -CanCollapse {
                New-HTMLPanel {
                    New-HTMLChart {
                        New-ChartPie -Name 'To be disabled' -Value $Export.Statistics.ToDisable
                        New-ChartPie -Name 'To be moved' -Value $Export.Statistics.ToMove
                        New-ChartPie -Name 'To be deleted' -Value $Export.Statistics.ToDelete
                    } -Title "Computers to be disabled or deleted"
                }
                if ($Export.Statistics.ToDisableComputerWorkstation -or $Export.Statistics.ToDisableComputerServer -or $Export.Statistics.ToDisableComputerUnknown) {
                    New-HTMLPanel {
                        New-HTMLChart {
                            New-ChartPie -Name "Disable workstations" -Value $Export.Statistics.ToDisableComputerWorkstation
                            New-ChartPie -Name "Disable servers" -Value $Export.Statistics.ToDisableComputerServer
                            New-ChartPie -Name "Disable unknown" -Value $Export.Statistics.ToDisableComputerUnknown
                        } -Title "Computers to be disabled by type"
                    }
                }
                if ($Export.Statistics.ToDeleteComputerWorkstation -or $Export.Statistics.ToDeleteComputerServer -or $Export.Statistics.ToDeleteComputerUnknown) {
                    New-HTMLPanel {
                        New-HTMLChart {
                            New-ChartPie -Name "Delete workstations" -Value $Export.Statistics.ToDeleteComputerWorkstation
                            New-ChartPie -Name "Delete servers" -Value $Export.Statistics.ToDeleteComputerServer
                            New-ChartPie -Name "Delete unknown" -Value $Export.Statistics.ToDeleteComputerUnknown
                        } -Title "Computers to be deleted by type"
                    }
                }
            }
            New-HTMLText -LineBreak
            New-HTMLHeading -Heading h3 -HeadingText "Full list of computers that will be processed if there are no limits to processing. "
            New-HTMLText -LineBreak
            New-HTMLSection -Invisible {
                New-HTMLPanel {
                    if ($Disable) {
                        New-HTMLText -Text "Computers will be disabled (and moved) only if: " -FontWeight bold
                        New-HTMLList {
                            foreach ($Key in $DisableOnlyIf.Keys) {
                                $newHTMLListItemSplat = @{
                                    Text       = @(
                                        if ($null -eq $DisableOnlyIf[$Key] -or $DisableOnlyIf[$Key].Count -eq 0) {
                                            $($Key), " is ", 'Not Set'
                                            $ColorInUse = 'Cinnabar'
                                        } else {
                                            if ($Key -in 'LastLogonDateMoreThan', 'LastLogonDateOlderThan') {
                                                $($Key), " is ", $($DisableOnlyIf[$Key]), " or ", "Never logged on"
                                            } elseif ($Key -in 'PasswordLastSetMoreThan', 'PasswordLastSetOlderThan') {
                                                $($Key), " is ", $($DisableOnlyIf[$Key]), " or ", "Never changed"
                                            } elseif ($Key -in 'LastSeenAzureMoreThan', 'LastSeenIntuneMoreThan', 'LastSyncAzureMoreThan', 'LastContactJamfMoreThan') {
                                                $($Key), " is ", $($DisableOnlyIf[$Key]), " or ", "Never synced/seen"
                                            } elseif ($Key -in 'MoveTargetOrganizationalUnit') {
                                                if ($DisableOnlyIf[$Key] -is [string]) {
                                                    $($Key), " is ", $MoveOnlyIf[$Key]
                                                } elseif ($DisableOnlyIf[$Key] -is [System.Collections.IDictionary]) {
                                                    $Key
                                                }
                                            } else {
                                                $($Key), " is ", $($DisableOnlyIf[$Key])
                                            }
                                            $ColorInUse = 'Apple'
                                        }
                                    )
                                    FontWeight = 'bold', 'normal', 'bold', 'normal', 'bold'
                                    Color      = $ColorInUse, 'None', 'CornflowerBlue', 'None', 'CornflowerBlue'

                                }
                                if ($Key -eq 'MoveTargetOrganizationalUnit') {
                                    $newHTMLListItemSplat.NestedListItems = {
                                        New-HTMLList {
                                            foreach ($MoveKey in $DisableOnlyIf[$Key].Keys) {
                                                New-HTMLListItem -Text @(
                                                    $MoveKey, " to ", $DisableOnlyIf[$Key][$MoveKey]
                                                ) -FontWeight 'bold', 'normal', 'bold', 'normal', 'bold' -Color $ColorInUse, 'None', 'CornflowerBlue', 'None', 'CornflowerBlue'
                                            }
                                        }
                                    }
                                }
                                New-HTMLListItem @newHTMLListItemSplat
                            }
                        }
                    } else {
                        New-HTMLText -Text "Computers will not be disabled, as the disable functionality was not enabled. " -FontWeight bold
                    }
                }
                New-HTMLPanel {
                    if ($Move) {
                        New-HTMLText -Text "Computers will be moved only if: " -FontWeight bold
                        New-HTMLList {
                            foreach ($Key in $MoveOnlyIf.Keys) {
                                $newHTMLListItemSplat = @{
                                    Text       = @(
                                        if ($null -eq $MoveOnlyIf[$Key] -or $MoveOnlyIf[$Key].Count -eq 0) {
                                            $($Key), " is ", 'Not Set'
                                            $ColorInUse = 'Cinnabar'
                                        } else {
                                            if ($Key -in 'LastLogonDateMoreThan', 'LastLogonDateOlderThan') {
                                                $($Key), " is ", $($MoveOnlyIf[$Key]), " or ", "Never logged on"
                                            } elseif ($Key -in 'PasswordLastSetMoreThan', 'PasswordLastSetOlderThan') {
                                                $($Key), " is ", $($MoveOnlyIf[$Key]), " or ", "Never changed"
                                            } elseif ($Key -in 'LastSeenAzureMoreThan', 'LastSeenIntuneMoreThan', 'LastSyncAzureMoreThan', 'LastContactJamfMoreThan') {
                                                $($Key), " is ", $($MoveOnlyIf[$Key]), " or ", "Never synced/seen"
                                            } elseif ($Key -in 'TargetOrganizationalUnit') {
                                                if ($MoveOnlyIf[$Key] -is [string]) {
                                                    $($Key), " is ", $MoveOnlyIf[$Key]
                                                } elseif ($MoveOnlyIf[$Key] -is [System.Collections.IDictionary]) {
                                                    $Key
                                                }
                                            } else {
                                                $($Key), " is ", $($MoveOnlyIf[$Key])
                                            }
                                            $ColorInUse = 'Apple'
                                        }
                                    )
                                    FontWeight = 'bold', 'normal', 'bold', 'normal', 'bold'
                                    Color      = $ColorInUse, 'None', 'CornflowerBlue', 'None', 'CornflowerBlue'
                                }
                                if ($Key -eq 'TargetOrganizationalUnit') {
                                    $newHTMLListItemSplat.NestedListItems = {
                                        New-HTMLList {
                                            foreach ($MoveKey in $MoveOnlyIf[$Key].Keys) {
                                                New-HTMLListItem -Text @(
                                                    $MoveKey, " to ", $MoveOnlyIf[$Key][$MoveKey]
                                                ) -FontWeight 'bold', 'normal', 'bold', 'normal', 'bold' -Color $ColorInUse, 'None', 'CornflowerBlue', 'None', 'CornflowerBlue'
                                            }
                                        }
                                    }
                                }
                                New-HTMLListItem @newHTMLListItemSplat
                            }
                        }
                    } else {
                        New-HTMLText -Text "Computers will not be moved, as the move functionality was not enabled. " -FontWeight bold
                    }
                }
                New-HTMLPanel {
                    if ($Delete) {
                        New-HTMLText -Text "Computers will be deleted only if: " -FontWeight bold
                        New-HTMLList {
                            foreach ($Key in $DeleteOnlyIf.Keys) {
                                New-HTMLListItem -Text @(
                                    if ($null -eq $DeleteOnlyIf[$Key] -or $DeleteOnlyIf[$Key].Count -eq 0) {
                                        $($Key), " is ", 'Not Set'
                                        $ColorInUse = 'Cinnabar'
                                    } else {
                                        if ($Key -in 'LastLogonDateMoreThan', 'LastLogonDateOlderThan') {
                                            $($Key), " is ", $($DeleteOnlyIf[$Key]), " or ", "Never logged on"
                                        } elseif ($Key -in 'PasswordLastSetMoreThan', 'PasswordLastSetOlderThan') {
                                            $($Key), " is ", $($DeleteOnlyIf[$Key]), " or ", "Never changed"
                                        } elseif ($Key -in 'LastSeenAzureMoreThan', 'LastSeenIntuneMoreThan', 'LastSyncAzureMoreThan', 'LastContactJamfMoreThan') {
                                            $($Key), " is ", $($DeleteOnlyIf[$Key]), " or ", "Never synced/seen"
                                        } else {
                                            $($Key), " is ", $($DeleteOnlyIf[$Key])
                                        }
                                        $ColorInUse = 'Apple'
                                    }
                                ) -FontWeight bold, normal, bold, normal, bold -Color $ColorInUse, None, CornflowerBlue, None, CornflowerBlue
                            }
                        }
                    } else {
                        New-HTMLText -Text "Computers will not be deleted, as the delete functionality was not enabled. " -FontWeight bold
                    }
                }
            }

            New-HTMLTable -DataTable $ComputersToProcess -Filtering -ScrollX {
                New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Delete' -BackgroundColor PinkLace
                New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Move' -BackgroundColor Yellow
                New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Disable' -BackgroundColor EnergyYellow
                New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'True' -BackgroundColor LightGreen
                New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'False' -BackgroundColor Salmon
                New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Whatif' -BackgroundColor LightBlue
                New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'ExcludedByFilter' -BackgroundColor LightBlue
                New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'ExcludedBySetting' -BackgroundColor LightPink
                New-HTMLTableCondition -Name 'ProtectedFromAccidentalDeletion' -ComparisonType string -Value $false -BackgroundColor LightBlue -FailBackgroundColor Salmon
            } -WarningAction SilentlyContinue -ExcludeProperty 'TimeOnPendingList', 'TimeToLeavePendingList', 'DistinguishedNameAfterMove'
        }
        try {
            if ($LogFile -and (Test-Path -LiteralPath $LogFile -ErrorAction Stop)) {
                $LogContent = Get-Content -Raw -LiteralPath $LogFile -ErrorAction Stop
                New-HTMLTab -Name 'Log' {
                    New-HTMLCodeBlock -Code $LogContent -Style generic
                }
            }
        } catch {
            Write-Color -Text "[e] ", "Couldn't read the log file. Skipping adding log to HTML. Error: $($_.Exception.Message)" -Color Yellow, Red
        }
    } -FilePath $FilePath -Online:$Online.IsPresent -ShowHTML:$ShowHTML.IsPresent
}
function New-HTMLProcessedSIDHistory {
    [CmdletBinding()]
    param(
        $Export,
        [System.Collections.IDictionary] $ForestInformation,
        [System.Collections.IDictionary] $Output,
        [string] $FilePath,
        [switch] $HideHTML,
        [switch] $Online,
        [string] $LogPath,
        [System.Collections.IDictionary] $Configuration
    )
    New-HTML {
        New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow
        New-HTMLTableOption -DataStore JavaScript -ArrayJoin -ArrayJoinString ", " -BoolAsString
        New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey

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

            New-HTMLText -Text "Overview of cleanup process for the SID History in the forest ", $($ForestInformation.Forest) -Color None, None -FontSize 14pt -FontWeight normal, bold -Alignment center

            New-HTMLSection -HeaderText "SID History Report for $($ForestInformation.Forest)" {
                New-HTMLPanel {
                    New-HTMLText -Text @(
                        "This report provides an overview of the SID history in the forest along with the current and history deletion status of SID history values as configured in the script. ",
                        "The report is divided into three tabs: Overview, Current Deletion Status, and History Deletion Status. ",
                        "The following report shows 3 tabs:"
                    ) -FontSize 10pt

                    New-HTMLList {
                        New-HTMLListItem -Text "Overview", " - ", "provides an overview of the SID history in the forest" -FontWeight bold, normal, normal
                        New-HTMLListItem -Text "Current Deletion Status", " - ", "shows the current deletion status of SID history values for given day (this report only)" -FontWeight bold, normal, normal
                        New-HTMLListItem -Text "History Deletion Status", " - ", "shows the history deletion status of SID history values over time" -FontWeight bold, normal, normal
                    } -FontSize 10pt

                    New-HTMLText -Text "The following statistics provide insights into the SID history in the forest:" -FontSize 10pt

                    New-HTMLList {
                        New-HTMLListItem -Text "$($Output.All.Count)", " objects with SID history values" -Color BlueViolet, None -FontWeight bold, normal
                        New-HTMLListItem -Text "$($Output.Statistics.TotalUsers)", " users with SID history values" -Color BlueViolet, None -FontWeight bold, normal
                        New-HTMLListItem -Text "$($Output.Statistics.TotalGroups)", " groups with SID history values" -Color BlueViolet, None -FontWeight bold, normal
                        New-HTMLListItem -Text "$($Output.Statistics.TotalComputers)", " computers with SID history values" -Color BlueViolet, None -FontWeight bold, normal
                        New-HTMLListItem -Text "$($Output.Statistics.EnabledObjects)", " enabled objects with SID history values" -Color BlueViolet, None -FontWeight bold, normal
                        New-HTMLListItem -Text "$($Output.Statistics.DisabledObjects)", " disabled objects with SID history values" -Color Salmon, None -FontWeight bold, normal
                        New-HTMLListItem -Text "$($Output.Keys.Count - 2)", " different domains with SID history values" -Color BlueViolet, None -FontWeight bold, normal
                    } -LineBreak -FontSize 10pt


                    New-HTMLText -Text "The following statistics provide insights into the SID history categories:" -FontSize 10pt

                    New-HTMLList {
                        # Add statistics for the three SID history categories
                        New-HTMLListItem -Text "$($Output.Statistics.InternalSIDs)", " SID history values from internal forest domains" -Color ForestGreen, None -FontWeight bold, normal
                        New-HTMLListItem -Text "$($Output.Statistics.ExternalSIDs)", " SID history values from external trusted domains" -Color DodgerBlue, None -FontWeight bold, normal
                        New-HTMLListItem -Text "$($Output.Statistics.UnknownSIDs)", " SID history values from unknown domains (deleted or broken trusts)" -Color Crimson, None -FontWeight bold, normal
                    } -FontSize 10pt
                }
                New-HTMLPanel {
                    New-HTMLText -Text "The following table lists all domains in the forest and active trusts, and their respective domain SID values, along with their types." -FontSize 10pt
                    New-HTMLList {
                        foreach ($SID in $Output.DomainSIDs.Keys) {
                            $DomainSID = $Output.DomainSIDs[$SID]
                            New-HTMLListItem -Text "Domain ", $($DomainSID.Domain), ", SID: ", $($DomainSID.SID), ", Type: ", $($DomainSID.Type) -Color None, BlueViolet, None, BlueViolet, None, BlueViolet -FontWeight normal, bold, normal, bold, normal, bold
                        }
                    } -FontSize 10pt
                }
            }

        }
        [Array] $DomainNames = foreach ($Key in $Output.Keys) {
            if ($Key -in @('Statistics', 'Trusts', 'DomainSIDs', 'DuplicateSIDs')) {
                continue
            }
            $Key
        }

        New-HTMLTab -Name 'Overview' {
            foreach ($Domain in $DomainNames) {
                [Array] $Objects = $Output[$Domain]
                $EnabledObjects = $Objects | Where-Object { $_.Enabled }
                $DisabledObjects = $Objects | Where-Object { -not $_.Enabled }
                $Types = $Objects | Group-Object -Property ObjectClass -NoElement


                if ($Domain -eq 'All') {
                    $Name = 'All'
                } else {
                    if ($Output.DomainSIDs[$Domain]) {
                        $DomainName = $Output.DomainSIDs[$Domain].Domain
                        $Name = "$DomainName ($($Objects.Count))"
                    } else {
                        $Name = "$Domain ($($Objects.Count))"
                    }
                }

                New-HTMLTab -Name $Name {
                    New-HTMLSection -HeaderText "Domain $Domain" {
                        New-HTMLPanel -Invisible {
                            New-HTMLText -Text "Overview for ", $Domain -Color Blue, BattleshipGrey -FontSize 10pt
                            New-HTMLList {
                                New-HTMLListItem -Text "$($Objects.Count)", " objects with SID history values" -Color BlueViolet, None -FontWeight bold, normal
                                New-HTMLListItem -Text "$($EnabledObjects.Count)", " enabled objects with SID history values" -Color Green, None -FontWeight bold, normal
                                New-HTMLListItem -Text "$($DisabledObjects.Count)", " disabled objects with SID history values" -Color Salmon, None -FontWeight bold, normal

                                # Calculate SID history categories for this domain
                                $InternalSIDsForDomain = ($Objects | ForEach-Object { $_.InternalCount }) | Measure-Object -Sum | Select-Object -ExpandProperty Sum
                                $ExternalSIDsForDomain = ($Objects | ForEach-Object { $_.ExternalCount }) | Measure-Object -Sum | Select-Object -ExpandProperty Sum
                                $UnknownSIDsForDomain = ($Objects | ForEach-Object { $_.UnknownCount }) | Measure-Object -Sum | Select-Object -ExpandProperty Sum

                                New-HTMLListItem -Text "$InternalSIDsForDomain", " SID history values from internal forest domains" -Color ForestGreen, None -FontWeight bold, normal
                                New-HTMLListItem -Text "$ExternalSIDsForDomain", " SID history values from external trusted domains" -Color DodgerBlue, None -FontWeight bold, normal
                                New-HTMLListItem -Text "$UnknownSIDsForDomain", " SID history values from unknown domains" -Color Crimson, None -FontWeight bold, normal

                                New-HTMLListItem -Text "Object types:" {
                                    New-HTMLList {
                                        foreach ($Type in $Types) {
                                            New-HTMLListItem -Text "$($Type.Count)", " ", $Type.Name, " objects with SID history values" -Color BlueViolet, None, BlueViolet, None -FontWeight bold, normal, bold, normal
                                        }
                                    }
                                } -FontSize 10pt
                            } -FontSize 10pt
                        }
                        New-HTMLPanel -Invisible {
                            New-HTMLText -Text 'Explanation to table columns:' -FontSize 10pt
                            New-HTMLList {
                                New-HTMLListItem -Text "Domain", " - ", "this column shows the domain of the object" -FontWeight bold, normal, normal
                                New-HTMLListItem -Text "ObjectClass", " - ", "this column shows the object class of the object (user, device, group)" -FontWeight bold, normal, normal
                                New-HTMLListItem -Text "Internal", " - ", "this column shows SIDs from domains within the current forest" -FontWeight bold, normal, normal
                                New-HTMLListItem -Text "External", " - ", "this column shows SIDs from domains that are trusted by the current forest" -FontWeight bold, normal, normal
                                New-HTMLListItem -Text "Unknown", " - ", "this column shows SIDs from domains that no longer exist or have broken trusts" -FontWeight bold, normal, normal
                                New-HTMLListItem -Text "Enabled", " - ", "this column shows if the object is enabled" -FontWeight bold, normal, normal
                                New-HTMLListItem -Text "SIDHistory", " - ", "this column shows the SID history values of the object" -FontWeight bold, normal, normal
                                New-HTMLListItem -Text "Domains", " - ", "this column shows the domains of the SID history values" -FontWeight bold, normal, normal
                                New-HTMLListItem -Text "DomainsExpanded", " - ", "this column shows the expanded domains of the SID history values (if possible), including SID if not possible to expand" -FontWeight bold, normal, normal
                            } -FontSize 10pt
                        }
                    }
                    New-HTMLTable -DataTable $Objects -Filtering {
                        New-HTMLTableCondition -Name 'Enabled' -ComparisonType bool -Operator eq -Value $true -BackgroundColor MintGreen -FailBackgroundColor Salmon
                        New-HTMLTableCondition -Name 'InternalCount' -ComparisonType number -Operator gt -Value 0 -BackgroundColor ForestGreen
                        New-HTMLTableCondition -Name 'ExternalCount' -ComparisonType number -Operator gt -Value 0 -BackgroundColor DodgerBlue
                        New-HTMLTableCondition -Name 'UnknownCount' -ComparisonType number -Operator gt -Value 0 -BackgroundColor Crimson
                    } -ScrollX
                } -TextTransform uppercase
            }
        }
        New-HTMLTab -Name 'Current Deletion Status' {
            New-HTMLSection -HeaderText "SID History Report" {
                New-HTMLPanel -Invisible {
                    New-HTMLText -Text "The following table lists all actions that were taken on given objects while removing SID History. The following statistics provide insights into processed SID history in the forest:" -FontSize 10pt

                    $Enabled = $Export.CurrentRun | Where-Object { $_.Enabled }
                    $Disabled = $Export.CurrentRun | Where-Object { -not $_.Enabled }

                    New-HTMLList {
                        New-HTMLListItem -Text "$($Enabled.Count)", " enabled objects" -FontWeight normal, bold
                        New-HTMLListItem -Text "$($Disabled.Count)", " disabled objects" -FontWeight normal, bold
                        New-HTMLListItem -Text "Processed ", $($Export.ProcessedObjects), " total objects" -FontWeight normal, bold, normal
                        New-HTMLListItem -Text "Processed ", $($Export.ProcessedSIDs), " total SID history values" -FontWeight normal, bold, normal
                    } -FontSize 10pt


                    New-HTMLText -Text "The following table lists all objects with SID history values and their current deletion status." -FontSize 10pt
                }
            }
            New-HTMLTable -DataTable $Export.CurrentRun -Filtering {
                New-HTMLTableCondition -Name 'Enabled' -ComparisonType bool -Operator eq -Value $true -BackgroundColor MintGreen -FailBackgroundColor Salmon
                New-HTMLTableCondition -Name 'SIDBeforeCount' -ComparisonType number -Operator gt -Value 0 -BackgroundColor LightCoral -FailBackgroundColor LightGreen
                New-HTMLTableCondition -Name 'SIDAfterCount' -ComparisonType number -Operator eq -Value 0 -BackgroundColor LightGreen -FailBackgroundColor Salmon

                New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'RemoveAll' -BackgroundColor LightPink
                New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'RemovePerSID' -BackgroundColor LightCoral

                New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Success' -BackgroundColor LightGreen
                New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Failed' -BackgroundColor Salmon
                New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'WhatIf' -BackgroundColor LightBlue
            } -ScrollX
        }
        New-HTMLTab -Name 'History Deletion Status' {
            New-HTMLSection -HeaderText "SID History Report" {
                New-HTMLPanel -Invisible {
                    New-HTMLText -Text "The following table lists all actions that were taken on given objects while removing SID History over time." -FontSize 10pt
                }
            }
            New-HTMLTable -DataTable $Export.History -Filtering {
                New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Success' -BackgroundColor LightGreen
                New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Failed' -BackgroundColor Salmon
                New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'WhatIf' -BackgroundColor LightBlue

                New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'RemoveAll' -BackgroundColor LightPink
                New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'RemovePerSID' -BackgroundColor LightCoral
            } -ScrollX
        }
        New-HTMLTab -Name "Configuration" {
            New-HTMLSection -HeaderText "Configuration" {
                New-HTMLPanel -Invisible {
                    New-HTMLText -Text "The following table lists all configuration settings used in the script." -FontSize 10pt
                }
            }
            New-HTMLTable -DataTable $Configuration -ScrollX
        }
        if ($LogPath) {
            $LogsContent = Get-Content -Path $LogPath -Raw -ErrorAction SilentlyContinue
            if ($LogsContent) {
                New-HTMLTab -Name 'Logs' {
                    New-HTMLCodeBlock -Code $LogsContent -Style generic
                }
            }
        }
    } -FilePath $FilePath -ShowHTML:(-not $HideHTML) -Online:$Online.IsPresent
}
function Remove-ADSIDHistory {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Array] $ObjectsToProcess,
        [System.Collections.IDictionary] $Export,
        [int] $RemoveLimitSID
    )
    Write-Color -Text "[i] ", "Starting process of removing SID History entries from ", $ObjectsToProcess.Count, " objects" -Color Yellow, White, Green

    $GlobalLimitSID = 0
    $ProcessedSIDs = 0
    $ProcessedObjects = 0

    $Export['CurrentRun'] = [System.Collections.Generic.List[PSCustomObject]]::new()

    :TopLoop foreach ($Item in $ObjectsToProcess) {
        $Object = $Item.Object
        $QueryServer = $Item.QueryServer

        $CurrentRunObject = [PSCustomObject] @{
            ObjectName     = $Object.Name
            ObjectDomain   = $Object.Domain
            Enabled        = $Object.Enabled
            SIDBefore      = $Object.SIDHistory -join ", "
            SIDBeforeCount = $Object.SIDHistory.Count
            Action         = $null
            ActionDate     = $null
            ActionStatus   = $null
            ActionError    = ''
            SIDRemoved     = @()
            SIDAfter       = @()
            SIDAfterCount  = 0
            ObjectDN       = $Object.DistinguishedName
        }

        $ProcessedObjects++

        if ($LimitPerSID) {
            # Process individual SIDs for this object
            foreach ($SID in $Object.SIDHistory) {
                $CurrentDate = Get-Date
                if ($PSCmdlet.ShouldProcess("$($Object.Name) ($($Object.Domain))", "Remove SID History entry $SID")) {
                    Write-Color -Text "[i] ", "Removing SID History entry $SID from ", $Object.Name -Color Yellow, White, Green
                    try {
                        Set-ADObject -Identity $Object.DistinguishedName -Remove @{ SIDHistory = $SID } -Server $QueryServer -ErrorAction Stop
                        $Result = [PSCustomObject]@{
                            ObjectName   = $Object.Name
                            ObjectDomain = $Object.Domain
                            Enabled      = $Object.Enabled
                            SID          = $SID
                            Action       = 'RemovePerSID'
                            ActionDate   = $CurrentDate
                            ActionStatus = 'Success'
                            ActionError  = ''
                            ObjectDN     = $Object.DistinguishedName
                        }
                        $CurrentRunObject.Action = 'RemovePerSID'
                        $CurrentRunObject.ActionDate = $CurrentDate
                        $CurrentRunObject.ActionStatus = 'Success'
                        $CurrentRunObject.ActionError = ''
                        $CurrentRunObject.SIDRemoved += $SID
                        Write-Color -Text "[+] ", "Removed SID History entry $SID from ", $Object.Name -Color Yellow, White, Green
                    } catch {
                        Write-Color -Text "[!] ", "Failed to remove SID History entry $SID from ", $Object.Name, " exception: ", $_.Exception.Message -Color Yellow, White, Red
                        $Result = [PSCustomObject]@{
                            ObjectName   = $Object.Name
                            ObjectDomain = $Object.Domain
                            SID          = $SID
                            Action       = 'RemovePerSID'
                            ActionDate   = $CurrentDate
                            ActionStatus = 'Failed'
                            ActionError  = $_.Exception.Message
                            ObjectDN     = $Object.DistinguishedName
                        }
                        $CurrentRunObject.Action = 'RemovePerSID'
                        $CurrentRunObject.ActionDate = $CurrentDate
                        $CurrentRunObject.ActionStatus = 'Failed'
                        $CurrentRunObject.ActionError = $_.Exception.Message
                    }
                } else {
                    Write-Color -Text "[i] ", "Would have removed SID History entry $SID from ", $Object.Name -Color Yellow, White, Green
                    $Result = [PSCustomObject]@{
                        ObjectName   = $Object.Name
                        ObjectDomain = $Object.Domain
                        SID          = $SID
                        Action       = 'RemovePerSID'
                        ActionDate   = Get-Date
                        ActionStatus = 'WhatIf'
                        ActionError  = ''
                        ObjectDN     = $Object.DistinguishedName
                    }
                    $CurrentRunObject.SIDRemoved += $SID
                    $CurrentRunObject.Action = 'RemovePerSID'
                    $CurrentRunObject.ActionDate = $CurrentDate
                    $CurrentRunObject.ActionStatus = 'WhatIf'
                    $CurrentRunObject.ActionError = ''
                }
                $null = $Export.History.Add($Result)

                try {
                    $RefreshedObject = Get-ADObject -Identity $Object.DistinguishedName -Properties SIDHistory -Server $QueryServer -ErrorAction Stop
                } catch {
                    Write-Color -Text "[!] ", "Failed to refresh object ", $Object.Name, " exception: ", $_.Exception.Message -Color Yellow, White, Red, Red
                    $RefreshedObject = $null
                }
                if ($RefreshedObject -and $RefreshedObject.SIDHistory) {
                    $CurrentRunObject.SIDAfter = $RefreshedObject.SIDHistory -join ", "
                } else {
                    $CurrentRunObject.SIDAfter = $null
                }

                $CurrentRunObject.SIDAfterCount = $RefreshedObject.SIDHistory.Count
                $Export.CurrentRun.Add($CurrentRunObject)
                $GlobalLimitSID++
                $ProcessedSIDs++

                if ($GlobalLimitSID -ge $RemoveLimitSID) {
                    Write-Color -Text "[i] ", "Reached SID limit of ", $RemoveLimitSID, ". Stopping processing." -Color Yellow, White, Green, White
                    break TopLoop
                }
            }
        } else {
            $CurrentDate = Get-Date
            # Process all SIDs for this object at once
            if ($PSCmdlet.ShouldProcess("$($Object.Name) ($($Object.Domain))", "Remove all SID History entries")) {
                Write-Color -Text "[i] ", "Removing all SID History entries from ", $Object.Name -Color Yellow, White, Green
                try {
                    Set-ADObject -Identity $Object.DistinguishedName -Clear SIDHistory -Server $QueryServer -ErrorAction Stop
                    $Result = [PSCustomObject]@{
                        ObjectName   = $Object.Name
                        ObjectDomain = $Object.Domain
                        SID          = $Object.SIDHistory -join ", "
                        Action       = 'RemoveAll'
                        ActionDate   = $CurrentDate
                        ActionStatus = 'Success'
                        ActionError  = ''
                        ObjectDN     = $Object.DistinguishedName
                    }
                    $CurrentRunObject.ActionStatus = 'Success'
                    $CurrentRunObject.ActionError = ''
                    Write-Color -Text "[+] ", "Removed all SID History entries from ", $Object.Name -Color Yellow, White, Green
                } catch {
                    Write-Color -Text "[!] ", "Failed to remove SID History entries from ", $Object.Name, " exception: ", $_.Exception.Message -Color Yellow, White, Red
                    $Result = [PSCustomObject]@{
                        ObjectName   = $Object.Name
                        ObjectDomain = $Object.Domain
                        SID          = $Object.SIDHistory -join ", "
                        Action       = 'RemoveAll'
                        ActionDate   = $CurrentDate
                        ActionStatus = 'Failed'
                        ActionError  = $_.Exception.Message
                        ObjectDN     = $Object.DistinguishedName
                    }
                    $CurrentRunObject.ActionStatus = 'Failed'
                    $CurrentRunObject.ActionError = $_.Exception.Message
                }
            } else {
                Write-Color -Text "[i] ", "Would have removed all SID History entries from ", $Object.Name -Color Yellow, White, Green
                $Result = [PSCustomObject]@{
                    ObjectName   = $Object.Name
                    ObjectDomain = $Object.Domain
                    SID          = $Object.SIDHistory -join ", "
                    Action       = 'RemoveAll'
                    ActionDate   = $CurrentDate
                    ActionStatus = 'WhatIf'
                    ActionError  = ''
                    ObjectDN     = $Object.DistinguishedName
                }
                $CurrentRunObject.ActionStatus = 'WhatIf'
                $CurrentRunObject.ActionError = ''
            }
            $CurrentRunObject.SIDRemoved += $Object.SIDHistory
            $CurrentRunObject.Action = 'RemoveAll'
            $CurrentRunObject.ActionDate = $CurrentDate
            $CurrentRunObject.ActionError = ''

            $null = $Export.History.Add($Result)

            try {
                $RefreshedObject = Get-ADObject -Identity $Object.DistinguishedName -Properties SIDHistory -Server $QueryServer -ErrorAction Stop
            } catch {
                Write-Color -Text "[!] ", "Failed to refresh object ", $Object.Name, " exception: ", $_.Exception.Message -Color Yellow, White, Red, Red
                $RefreshedObject = $null
            }
            if ($RefreshedObject -and $RefreshedObject.SIDHistory) {
                $CurrentRunObject.SIDAfter = $RefreshedObject.SIDHistory -join ", "
            } else {
                $CurrentRunObject.SIDAfter = $null
            }

            $CurrentRunObject.SIDAfterCount = $RefreshedObject.SIDHistory.Count
            $Export.CurrentRun.Add($CurrentRunObject)
            $ProcessedSIDs += $Object.SIDHistory.Count
        }
    }
    $Export['ProcessedObjects'] = $ProcessedObjects
    $Export['ProcessedSIDs'] = $ProcessedSIDs

    Write-Color -Text "[i] ", "Processed ", $ProcessedObjects, " objects out of ", $ObjectsToProcess.Count, " and removed ", $ProcessedSIDs, " SID History entries" -Color Yellow, White, Green, White, Green, White, Green, White
}
function Request-ADComputersDelete {
    [cmdletBinding(SupportsShouldProcess)]
    param(
        [System.Collections.IDictionary] $Report,
        [switch] $ReportOnly,
        [switch] $WhatIfDelete,
        [int] $DeleteLimit,
        [System.Collections.IDictionary] $ProcessedComputers,
        [DateTime] $Today,
        [switch] $DontWriteToEventLog
    )

    $CountDeleteLimit = 0
    # :top means name of the loop, so we can break it
    :topLoop foreach ($Domain in $Report.Keys) {
        foreach ($Computer in $Report["$Domain"]['Computers']) {
            $Server = $Report["$Domain"]['Server']
            if ($Computer.Action -ne 'Delete') {
                continue
            }
            if ($ReportOnly) {
                $Computer
            } else {
                Write-Color -Text "[i] Deleting computer ", $Computer.SamAccountName, ' DN: ', $Computer.DistinguishedName, ' Enabled: ', $Computer.Enabled, ' Operating System: ', $Computer.OperatingSystem, ' LastLogon: ', $Computer.LastLogonDate, " / " , $Computer.LastLogonDays , ' days, PasswordLastSet: ', $Computer.PasswordLastSet, " / ", $Computer.PasswordLastChangedDays, " days" -Color Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green
                try {
                    $Success = $true
                    Remove-ADObject -Identity $Computer.DistinguishedName -Recursive -WhatIf:$WhatIfDelete -Server $Server -ErrorAction Stop -Confirm:$false
                    Write-Color -Text "[+] Deleting computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfDelete.IsPresent)) successful." -Color Yellow, Green, Yellow
                    if (-not $DontWriteToEventLog) {
                        Write-Event -ID 12 -LogName 'Application' -EntryType Warning -Category 1000 -Source 'CleanupComputers' -Message "Deleting computer $($Computer.SamAccountName) successful." -AdditionalFields @('Delete', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDelete) -WarningAction SilentlyContinue -WarningVariable warnings
                    }
                    foreach ($W in $Warnings) {
                        Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red
                    }

                } catch {
                    $Success = $false
                    Write-Color -Text "[-] Deleting computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfDelete.IsPresent)) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow
                    if (-not $DontWriteToEventLog) {
                        Write-Event -ID 12 -LogName 'Application' -EntryType Error -Category 1000 -Source 'CleanupComputers' -Message "Deleting computer $($Computer.SamAccountName) failed." -AdditionalFields @('Delete', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDelete, $($_.Exception.Message)) -WarningAction SilentlyContinue -WarningVariable warnings
                    }
                    foreach ($W in $Warnings) {
                        Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red
                    }
                    $Computer.ActionComment = $_.Exception.Message
                }
                $Computer.ActionDate = $Today
                if ($WhatIfDelete.IsPresent) {
                    $Computer.ActionStatus = 'WhatIf'
                } else {
                    if ($Success) {
                        # lets remove computer from $ProcessedComputers
                        # but only if it's not WhatIf and only if it's successful
                        $ComputerOnTheList = -join ($Computer.SamAccountName, "@", $Domain)
                        $ProcessedComputers.Remove("$ComputerOnTheList")
                    }
                    $Computer.ActionStatus = $Success
                }
                # return computer to $ReportDeleted so we can see summary just in case
                $Computer
                $CountDeleteLimit++
                if ($DeleteLimit) {
                    if ($DeleteLimit -eq $CountDeleteLimit) {
                        break topLoop # this breaks top loop
                    }
                }
            }
        }
    }
}
function Request-ADComputersDisable {
    [cmdletbinding(SupportsShouldProcess)]
    param(
        [nullable[bool]] $Delete,
        [nullable[bool]] $Move,
        [nullable[bool]] $DisableAndMove,
        [System.Collections.IDictionary] $Report,
        [switch] $WhatIfDisable,
        [switch] $DisableModifyDescription,
        [switch] $DisableModifyAdminDescription,
        [int] $DisableLimit,
        [switch] $ReportOnly,
        [DateTime] $Today,
        [switch] $DontWriteToEventLog,
        [Object] $DisableMoveTargetOrganizationalUnit,
        [switch] $DoNotAddToPendingList,
        [ValidateSet(
            'DisableAndMove',
            'MoveAndDisable'
        )][string] $DisableAndMoveOrder = 'DisableAndMove'
    )

    if ($DisableAndMove -and $DisableMoveTargetOrganizationalUnit) {
        if ($DisableMoveTargetOrganizationalUnit -is [System.Collections.IDictionary]) {
            $OrganizationalUnit = $DisableMoveTargetOrganizationalUnit
        } elseif ($DisableMoveTargetOrganizationalUnit -is [string]) {
            $DomainCN = ConvertFrom-DistinguishedName -DistinguishedName $DisableMoveTargetOrganizationalUnit -ToDomainCN
            $OrganizationalUnit = [ordered] @{
                $DomainCN = $DisableMoveTargetOrganizationalUnit
            }
        } else {
            Write-Color -Text "[-] DisableMoveTargetOrganizationalUnit is not a string or hashtable. Skipping moving to proper OU." -Color Yellow, Red
            return
        }
    }

    $CountDisable = 0
    # :top means name of the loop, so we can break it
    :topLoop foreach ($Domain in $Report.Keys) {
        Write-Color "[i] ", "Starting process of disabling computers for domain $Domain" -Color Yellow, Green
        foreach ($Computer in $Report["$Domain"]['Computers']) {
            $Server = $Report["$Domain"]['Server']
            if ($Computer.Action -ne 'Disable') {
                continue
            }
            if ($ReportOnly) {
                $Computer
            } else {
                $Success = $true
                if ($DisableAndMoveOrder -eq 'DisableAndMove') {
                    $Success = Disable-WinADComputer -Success $Success -WhatIfDisable:$WhatIfDisable -DontWriteToEventLog:$DontWriteToEventLog -Computer $Computer -Server $Server
                    $Success = Move-WinADComputer -Success $Success -DisableAndMove $DisableAndMove -OrganizationalUnit $OrganizationalUnit -Computer $Computer -WhatIfDisable:$WhatIfDisable -DontWriteToEventLog:$DontWriteToEventLog -Server $Server
                } else {
                    $Success = Move-WinADComputer -Success $Success -DisableAndMove $DisableAndMove -OrganizationalUnit $OrganizationalUnit -Computer $Computer -WhatIfDisable:$WhatIfDisable -DontWriteToEventLog:$DontWriteToEventLog -Server $Server
                    $Success = Disable-WinADComputer -Success $Success -WhatIfDisable:$WhatIfDisable -DontWriteToEventLog:$DontWriteToEventLog -Computer $Computer -Server $Server
                }
                if ($Success) {
                    if ($DisableModifyDescription -eq $true) {
                        $DisableModifyDescriptionText = "Disabled by a script, LastLogon $($Computer.LastLogonDate) ($($DisableOnlyIf.LastLogonDateMoreThan)), PasswordLastSet $($Computer.PasswordLastSet) ($($DisableOnlyIf.PasswordLastSetMoreThan))"
                        try {
                            Set-ADComputer -Identity $Computer.DistinguishedName -Description $DisableModifyDescriptionText -WhatIf:$WhatIfDisable -ErrorAction Stop -Server $Server
                            Write-Color -Text "[+] ", "Setting description on disabled computer ", $Computer.DistinguishedName, " (WhatIf: $WhatIfDisable) successful. Set to: ", $DisableModifyDescriptionText -Color Yellow, Green, Yellow, Green, Yellow
                        } catch {
                            $Computer.ActionComment = $Computer.ActionComment + [System.Environment]::NewLine + $_.Exception.Message
                            Write-Color -Text "[-] ", "Setting description on disabled computer ", $Computer.DistinguishedName, " (WhatIf: $WhatIfDisable) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow
                        }
                    }
                    if ($DisableModifyAdminDescription) {
                        $DisableModifyAdminDescriptionText = "Disabled by a script, LastLogon $($Computer.LastLogonDate) ($($DisableOnlyIf.LastLogonDateMoreThan)), PasswordLastSet $($Computer.PasswordLastSet) ($($DisableOnlyIf.PasswordLastSetMoreThan))"
                        try {
                            Set-ADObject -Identity $Computer.DistinguishedName -Replace @{ AdminDescription = $DisableModifyAdminDescriptionText } -WhatIf:$WhatIfDisable -ErrorAction Stop -Server $Server
                            Write-Color -Text "[+] ", "Setting admin description on disabled computer ", $Computer.DistinguishedName, " (WhatIf: $WhatIfDisable) successful. Set to: ", $DisableModifyAdminDescriptionText -Color Yellow, Green, Yellow, Green, Yellow
                        } catch {
                            $Computer.ActionComment + [System.Environment]::NewLine + $_.Exception.Message
                            Write-Color -Text "[-] ", "Setting admin description on disabled computer ", $Computer.DistinguishedName, " (WhatIf: $WhatIfDisable) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow
                        }
                    }
                }

                # this is to store actual disabling time - we can't trust WhenChanged date
                $Computer.ActionDate = $Today
                if ($WhatIfDisable.IsPresent) {
                    $Computer.ActionStatus = 'WhatIf'
                } else {
                    $Computer.ActionStatus = $Success
                }

                # We add computer to pending list in all cases because otherwise we would be going in circles
                # if move or delete were not enabled
                # please use -DoNotAddToPendingList if you don't want to add computer to pending list
                if (-not $DoNotAddToPendingList) {
                    $FullComputerName = -join ($Computer.SamAccountName, '@', $Domain)
                    # Lets add computer to pending list, and lets set time how long it's there so it can be easily visible in reports
                    $Computer.TimeOnPendingList = 0
                    $ProcessedComputers[$FullComputerName] = $Computer
                }

                # return computer to $ReportDisabled so we can see summary just in case
                $Computer
                $CountDisable++
                if ($DisableLimit) {
                    if ($DisableLimit -eq $CountDisable) {
                        break topLoop # this breaks top loop
                    }
                }
            }
        }
    }
}
function Request-ADComputersMove {
    [cmdletBinding(SupportsShouldProcess)]
    param(
        [nullable[bool]] $Delete,
        [System.Collections.IDictionary] $Report,
        [switch] $ReportOnly,
        [switch] $WhatIfMove,
        [int] $MoveLimit,
        [System.Collections.IDictionary] $ProcessedComputers,
        [DateTime] $Today,
        [Object] $TargetOrganizationalUnit,
        [switch] $DontWriteToEventLog,
        [switch] $DoNotAddToPendingList
    )

    if ($TargetOrganizationalUnit -is [System.Collections.IDictionary]) {
        $OrganizationalUnit = $TargetOrganizationalUnit
    } elseif ($TargetOrganizationalUnit -is [string]) {
        $DomainCN = ConvertFrom-DistinguishedName -DistinguishedName $TargetOrganizationalUnit -ToDomainCN
        $OrganizationalUnit = [ordered] @{
            $DomainCN = $TargetOrganizationalUnit
        }
    } else {
        Write-Color -Text "[-] TargetOrganizationalUnit is not a string or hashtable. Skipping moving to proper OU." -Color Yellow, Red
        return
    }
    $CountMoveLimit = 0
    # :top means name of the loop, so we can break it
    :topLoop foreach ($Domain in $Report.Keys) {
        foreach ($Computer in $Report["$Domain"]['Computers']) {
            $Server = $Report["$Domain"]['Server']
            if ($Computer.Action -ne 'Move') {
                continue
            }
            if ($ReportOnly) {
                $Computer
            } else {
                if ($OrganizationalUnit[$Domain]) {
                    # we check if the computer is already in the correct OU
                    if ($Computer.OrganizationalUnit -eq $OrganizationalUnit[$Domain]) {
                        # this shouldn't really happen as we should have filtered it out earlier
                    } else {
                        Write-Color -Text "[i] Moving computer ", $Computer.SamAccountName, ' DN: ', $Computer.DistinguishedName, ' Enabled: ', $Computer.Enabled, ' Operating System: ', $Computer.OperatingSystem, ' LastLogon: ', $Computer.LastLogonDate, " / " , $Computer.LastLogonDays , ' days, PasswordLastSet: ', $Computer.PasswordLastSet, " / ", $Computer.PasswordLastChangedDays, " days" -Color Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green
                        try {
                            $MovedObject = Move-ADObject -Identity $Computer.DistinguishedName -WhatIf:$WhatIfMove -Server $Server -ErrorAction Stop -Confirm:$false -TargetPath $OrganizationalUnit[$Domain] -PassThru
                            $Computer.DistinguishedNameAfterMove = $MovedObject.DistinguishedName
                            Write-Color -Text "[+] Moving computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfMove.IsPresent)) successful." -Color Yellow, Green, Yellow
                            if (-not $DontWriteToEventLog) {
                                Write-Event -ID 11 -LogName 'Application' -EntryType Warning -Category 1000 -Source 'CleanupComputers' -Message "Moving computer $($Computer.SamAccountName) successful." -AdditionalFields @('Move', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfMove) -WarningAction SilentlyContinue -WarningVariable warnings
                            }
                            foreach ($W in $Warnings) {
                                Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red
                            }
                            if (-not $Delete) {
                                # lets remove computer from $ProcessedComputers
                                # we only remove it if Delete is not part of the removal process and move is the last step
                                if (-not $DoNotAddToPendingList) {
                                    $ComputerOnTheList = -join ($Computer.SamAccountName, "@", $Domain)
                                    $ProcessedComputers.Remove("$ComputerOnTheList")
                                }
                            }
                            $Success = $true
                        } catch {
                            $Success = $false
                            Write-Color -Text "[-] Moving computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfMove.IsPresent)) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow
                            if (-not $DontWriteToEventLog) {
                                Write-Event -ID 11 -LogName 'Application' -EntryType Error -Category 1000 -Source 'CleanupComputers' -Message "Moving computer $($Computer.SamAccountName) failed." -AdditionalFields @('Move', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfMove, $($_.Exception.Message)) -WarningAction SilentlyContinue -WarningVariable warnings
                            }
                            foreach ($W in $Warnings) {
                                Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red
                            }
                            $Computer.ActionComment = $_.Exception.Message
                        }
                        $Computer.ActionDate = $Today
                        if ($WhatIfMove.IsPresent) {
                            $Computer.ActionStatus = 'WhatIf'
                        } else {
                            $Computer.ActionStatus = $Success
                        }
                        # return computer to $ReportMoved so we can see summary just in case
                        $Computer
                        $CountMoveLimit++
                        if ($MoveLimit) {
                            if ($MoveLimit -eq $CountMoveLimit) {
                                break topLoop # this breaks top loop
                            }
                        }
                    }
                } else {
                    Write-Color -Text "[-] Moving computer ", $Computer.SamAccountName, " failed. TargetOrganizationalUnit for domain $Domain not found." -Color Yellow, Red, Yellow
                    if (-not $DontWriteToEventLog) {
                        Write-Event -ID 11 -LogName 'Application' -EntryType Error -Category 1000 -Source 'CleanupComputers' -Message "Moving computer $($Computer.SamAccountName) failed." -AdditionalFields @('Move', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfMove, "TargetOrganizationalUnit for domain $Domain not found.") -WarningAction SilentlyContinue -WarningVariable warnings
                    }
                    foreach ($W in $Warnings) {
                        Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red
                    }
                    $Computer.ActionComment = "TargetOrganizationalUnit for domain $Domain not found."
                    $Computer.ActionDate = $Today
                    $Computer.ActionStatus = $false
                    # return computer to $ReportMoved so we can see summary just in case
                    $Computer
                    $CountMoveLimit++
                    if ($MoveLimit) {
                        if ($MoveLimit -eq $CountMoveLimit) {
                            break topLoop # this breaks top loop
                        }
                    }
                }
            }
        }
    }
}
function Request-ADSIDHistory {
    [CmdletBinding()]
    param(
        [Array] $DomainNames,
        [System.Collections.IDictionary] $Output,
        [System.Collections.IDictionary] $Export,
        [System.Collections.IDictionary] $ForestInformation,
        [string[]] $IncludeOrganizationalUnit,
        [string[]] $ExcludeOrganizationalUnit,
        [string[]] $IncludeSIDHistoryDomain,
        [string[]] $ExcludeSIDHistoryDomain,
        [nullable[int]] $RemoveLimitObject,
        [ValidateSet('Internal', 'External', 'Unknown')][string[]] $IncludeType,
        [ValidateSet('Internal', 'External', 'Unknown')][string[]] $ExcludeType,
        [switch] $DisabledOnly,
        [switch] $DontWriteToEventLog,
        [bool] $LimitPerObject,
        [bool] $LimitPerSID,
        [System.Collections.Generic.List[PSCustomObject]] $ObjectsToProcess
    )
    $GlobalLimitObject = 0

    # Process each domain SID
    :TopLoop foreach ($Domain in $DomainNames) {
        [Array] $Objects = $Output[$Domain]

        # Skip if we've already hit our object limit
        if ($LimitPerObject -and $GlobalLimitObject -ge $RemoveLimitObject) {
            Write-Color -Text "[i] ", "Reached object limit of ", $RemoveLimitObject, ". Stopping processing." -Color Yellow, White, Green, White
            break TopLoop
        }

        # Check if this domain SID matches our type filters
        $DomainInfo = $Output.DomainSIDs[$Domain]

        if (-not $DomainInfo) {
            $DomainType = "Unknown"
        } else {
            $DomainType = $DomainInfo.Type
            if ($DomainType -eq 'Domain') {
                $DomainType = "Internal"
            } elseif ($DomainType -eq 'Trust') {
                $DomainType = "External"
            }
        }

        # Apply type filters
        if ($ExcludeType.Count -gt 0 -and $ExcludeType -contains $DomainType) {
            Write-Color -Text "[s] ", "Skipping ", $Domain, " as it's type ", $DomainType, " is excluded." -Color Yellow, White, Red, White, Red, White
            continue
        }

        if ($IncludeType.Count -gt 0 -and $IncludeType -notcontains $DomainType) {
            Write-Color -Text "[s] ", "Skipping ", $Domain, " as it's type ", $DomainType, " is not included." -Color Yellow, White, Red, White, Red, White
            continue
        }

        # Apply SID domain filters
        if ($IncludeSIDHistoryDomain -and $IncludeSIDHistoryDomain -notcontains $Domain) {
            Write-Color -Text "[s] ", "Skipping ", $Domain, " as it's not in the included SID history domains." -Color Yellow, White, Red, White
            continue
        }

        if ($ExcludeSIDHistoryDomain -and $ExcludeSIDHistoryDomain -contains $Domain) {
            Write-Color -Text "[s] ", "Skipping ", $Domain, " as it's in the excluded SID history domains." -Color Yellow, White, Red, White
            continue
        }

        # Display which domain we're processing
        $DomainDisplayName = if ($DomainInfo) { $DomainInfo.Domain } else { "Unknown" }
        Write-Color -Text "[i] ", "Processing domain SID ", $Domain, " (", $DomainDisplayName, " - ", $DomainType, ")" -Color Yellow, White, Green, White, Green, White, Green

        # Process each object in this domain
        foreach ($Object in $Objects) {
            $QueryServer = $ForestInformation['QueryServers'][$Object.Domain].HostName[0]

            if ($DisabledOnly) {
                if ($Object.Enabled) {
                    Write-Color -Text "[s] ", "Skipping ", $Object.Name, " as it is enabled and DisabledOnly filter is set." -Color Yellow, White, Red, White
                    continue
                }
            }

            # Check if we need to filter by OU
            if ($IncludeOrganizationalUnit -and $IncludeOrganizationalUnit -notcontains $Object.OrganizationalUnit) {
                continue
            }

            if ($ExcludeOrganizationalUnit -and $ExcludeOrganizationalUnit -contains $Object.OrganizationalUnit) {
                continue
            }

            Write-Color -Text "[i] ", "Processing ", $Object.Name, " (", $Object.ObjectClass, " in ", $Object.Domain, ", SID History Count: ", $Object.SIDHistory.Count, ")" -Color Yellow, White, Green, White, Green, White, Green, White, Green

            # Add to our collection of objects to process
            $ObjectsToProcess.Add(
                [PSCustomObject]@{
                    Object      = $Object
                    QueryServer = $QueryServer
                    Domain      = $Domain
                    DomainInfo  = $DomainInfo
                    DomainType  = $DomainType
                }
            )

            # Increment counter and check limits
            $GlobalLimitObject++
            if ($LimitPerObject -and $GlobalLimitObject -ge $RemoveLimitObject) {
                Write-Color -Text "[i] ", "Reached object limit of ", $RemoveLimitObject, ". Stopping object collection." -Color Yellow, White, Green, White
                break TopLoop
            }
        }
    }
}
function Invoke-ADComputersCleanup {
    <#
    .SYNOPSIS
    Active Directory Cleanup function that can disable or delete computers
    that have not been logged on for a certain amount of time.
 
    .DESCRIPTION
    Active Directory Cleanup function that can disable or delete computers
    that have not been logged on for a certain amount of time.
    It has many options to customize the cleanup process.
 
    .PARAMETER Forest
    Forest to use when connecting to Active Directory.
 
    .PARAMETER IncludeDomains
    List of domains to include in the process.
 
    .PARAMETER ExcludeDomains
    List of domains to exclude from the process.
 
    .PARAMETER Disable
    Enable the disable process, meaning the computers that meet the criteria will be disabled.
 
    .PARAMETER DisableAndMove
    Enable the disable and move process, meaning the computers that meet the criteria will be disabled and moved (in that order).
    This is useful if you want to disable computers first and then move them to a different OU right after.
    It's integral part of disabling process.
    If you want Move as a separate process, use Move settings.
 
    .PARAMETER DisableAndMoveOrder
    Order of the Disable and Move process. Default is 'DisableAndMove'.
    If you want to move computers first and then disable them, use 'MoveAndDisable'.
 
    .PARAMETER DisableIsEnabled
    Disable computer only if it's Enabled or only if it's Disabled.
    By default it will try to disable all computers that are either disabled or enabled.
    While counter-intuitive for already disabled computers,
    this is useful if you want preproceess computers for deletion and need to get them on the list.
 
    .PARAMETER DisableNoServicePrincipalName
    Disable computer only if it has a ServicePrincipalName or only if it doesn't have a ServicePrincipalName.
    By default it doesn't care if it has a ServicePrincipalName or not.
 
    .PARAMETER DisableLastLogonDateMoreThan
    Disable computer only if it has a LastLogonDate that is more than the specified number of days.
 
    .PARAMETER DisablePasswordLastSetMoreThan
    Disable computer only if it has a PasswordLastSet that is more than the specified number of days.
 
    .PARAMETER DisableRequireWhenCreatedMoreThan
    Disable computer only if it was created more than the specified number of days ago.
 
    .PARAMETER DisablePasswordLastSetOlderThan
    Disable computer only if it has a PasswordLastSet that is older than the specified date.
 
    .PARAMETER DisableLastLogonDateOlderThan
    Disable computer only if it has a LastLogonDate that is older than the specified date.
 
    .PARAMETER DisableLastSeenAzureMoreThan
    Disable computer only if it Last Seen in Azure is more than the specified number of days.
    Please note that you need to make connection to Azure using Connect-MgGraph with proper permissions first.
    Additionally you will need GraphEssentials PowerShell Module installed.
 
    .PARAMETER DisableLastSeenIntuneMoreThan
    Disable computer only if it Last Seen in Intune is more than the specified number of days.
    Please note that you need to make connection to Intune using Connect-MgGraph with proper permissions first.
    Additionally you will need GraphEssentials PowerShell Module installed.
 
    .PARAMETER DisableLastSyncAzureMoreThan
    Disable computer only if it Last Synced in Azure is more than the specified number of days.
    Please note that you need to make connection to Azure AD using Connect-MgGraph with proper permissions first.
    Additionally you will need GraphEssentials PowerShell Module installed.
 
    .PARAMETER DisableLastContactJamfMoreThan
    Disable computer only if it Last Contacted in Jamf is more than the specified number of days.
    Please note that you need to make connection to Jamf using PowerJamf PowerShell Module first.
    Additionally you will need PowerJamf PowerShell Module installed.
 
    .PARAMETER DisableExcludeSystems
    Disable computer only if it's not on the list of excluded operating systems.
    If you want to exclude Windows 10, you can specify 'Windows 10' or 'Windows 10*' or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'.
    You can also specify multiple operating systems by separating them with a comma.
    It's using the -like operator, so you can use wildcards.
    It's using OperatingSystem property of the computer object for comparison.
 
    .PARAMETER DisableIncludeSystems
    Disable computer only if it's on the list of included operating systems.
    If you want to include Windows 10, you can specify 'Windows 10' or 'Windows 10*'
    or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'.
    You can also specify multiple operating systems by separating them with a comma.
    It's using the -like operator, so you can use wildcards.
 
    .PARAMETER DisableExcludeServicePrincipalName
    Disable computer only if it's not on the list of excluded ServicePrincipalNames.
    You can also specify multiple ServicePrincipalNames by providing an array of entries.
    It's using the -like operator, so you can use wildcards.
 
    .PARAMETER DisableIncludeServicePrincipalName
    Disable computer only if it's on the list of included ServicePrincipalNames.
    You can also specify multiple ServicePrincipalNames by providing an array of entries.
    It's using the -like operator, so you can use wildcards.
 
    .PARAMETER DisableMoveTargetOrganizationalUnit
    Move computer to the specified OU after it's disabled.
    It can take a string with DistinguishedName, or hashtable with key being the domain, and value being the DistinguishedName.
    If you have a forest with multiple domains and want to move computers to different OUs based on their domain, you can use hashtable.
 
    .PARAMETER DisableDoNotAddToPendingList
    By default, computers that are disabled are added to the list of computers that will be actioned later (moved/deleted).
    If you want to disable computers, but not add them to the list of computers that will be actioned later (aka pending list), use this switch.
 
    .PARAMETER Delete
    Enable the delete process, meaning the computers that meet the criteria will be deleted.
 
    .PARAMETER DeleteIsEnabled
    Delete computer only if it's Enabled or only if it's Disabled.
    By default it will try to delete all computers that are either disabled or enabled.
 
    .PARAMETER DeleteNoServicePrincipalName
    Delete computer only if it has a ServicePrincipalName or only if it doesn't have a ServicePrincipalName.
    By default it doesn't care if it has a ServicePrincipalName or not.
 
    .PARAMETER DeleteLastLogonDateMoreThan
    Delete computer only if it has a LastLogonDate that is more than the specified number of days.
 
    .PARAMETER DeletePasswordLastSetMoreThan
    Delete computer only if it has a PasswordLastSet that is more than the specified number of days.
 
    .PARAMETER DeleteRequireWhenCreatedMoreThan
    Delete computer only if it was created more than the specified number of days ago.
 
    .PARAMETER DeleteListProcessedMoreThan
    Delete computer only if it has been processed by this script more than the specified number of days ago.
    This is useful if you want to delete computers that have been disabled for a certain amount of time.
    It uses XML file to store the list of processed computers, so please make sure to not remove it or it will start over.
 
    .PARAMETER DeletePasswordLastSetOlderThan
    Delete computer only if it has a PasswordLastSet that is older than the specified date.
 
    .PARAMETER DeleteLastLogonDateOlderThan
    Delete computer only if it has a LastLogonDate that is older than the specified date.
 
    .PARAMETER DeleteLastSeenAzureMoreThan
    Delete computer only if it Last Seen in Azure is more than the specified number of days.
    Please note that you need to make connection to Azure using Connect-MgGraph with proper permissions first.
    Additionally yopu will need GraphEssentials PowerShell Module installed.
 
    .PARAMETER DeleteLastSeenIntuneMoreThan
    Delete computer only if it Last Seen in Intune is more than the specified number of days.
    Please note that you need to make connection to Intune using Connect-MgGraph with proper permissions first.
    Additionally you will need GraphEssentials PowerShell Module installed.
 
    .PARAMETER DeleteLastSyncAzureMoreThan
    Delete computer only if it Last Synced in Azure is more than the specified number of days.
    Please note that you need to make connection to Azure AD using Connect-MgGraph with proper permissions first.
    Additionally you will need GraphEssentials PowerShell Module installed.
 
    .PARAMETER DeleteLastContactJamfMoreThan
    Delete computer only if it Last Contacted in Jamf is more than the specified number of days.
    Please note that you need to make connection to Jamf using PowerJamf PowerShell Module first.
    Additionally you will need PowerJamf PowerShell Module installed.
 
    .PARAMETER DeleteExcludeSystems
    Delete computer only if it's not on the list of excluded operating systems.
    If you want to exclude Windows 10, you can specify 'Windows 10' or 'Windows 10*'
    or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'.
    You can also specify multiple operating systems by separating them with a comma.
    It's using the -like operator, so you can use wildcards.
    It's using OperatingSystem property of the computer object for comparison.
 
    .PARAMETER DeleteIncludeSystems
    Delete computer only if it's on the list of included operating systems.
    If you want to include Windows 10, you can specify 'Windows 10' or 'Windows 10*'
    or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'.
    You can also specify multiple operating systems by separating them with a comma.
    It's using the -like operator, so you can use wildcards.
 
    .PARAMETER DeleteExcludeServicePrincipalName
    Delete computer only if it's not on the list of excluded ServicePrincipalNames.
    You can also specify multiple ServicePrincipalNames by providing an array of entries.
    It's using the -like operator, so you can use wildcards.
 
    .PARAMETER DeleteIncludeServicePrincipalName
    Delete computer only if it's on the list of included ServicePrincipalNames.
    You can also specify multiple ServicePrincipalNames by providing an array of entries.
    It's using the -like operator, so you can use wildcards.
 
    .PARAMETER DeleteLimit
    Limit the number of computers that will be deleted. 0 = unlimited. Default is 1.
    This is to prevent accidental deletion of all computers that meet the criteria.
    Adjust the limit to your needs.
 
    .PARAMETER MoveIsEnabled
    Move computer only if it's Enabled or only if it's Disabled.
    By default it will try to Move all computers that are either disabled or enabled.
 
    .PARAMETER MoveNoServicePrincipalName
    Move computer only if it has a ServicePrincipalName or only if it doesn't have a ServicePrincipalName.
    By default it doesn't care if it has a ServicePrincipalName or not.
 
    .PARAMETER MoveLastLogonDateMoreThan
    Move computer only if it has a LastLogonDate that is more than the specified number of days.
 
    .PARAMETER MovePasswordLastSetMoreThan
    Move computer only if it has a PasswordLastSet that is more than the specified number of days.
 
    .PARAMETER MoveListProcessedMoreThan
    Move computer only if it has been processed by this script more than the specified number of days ago.
    This is useful if you want to Move computers that have been disabled for a certain amount of time.
    It uses XML file to store the list of processed computers, so please make sure to not remove it or it will start over.
 
    .PARAMETER MovePasswordLastSetOlderThan
    Move computer only if it has a PasswordLastSet that is older than the specified date.
 
    .PARAMETER MoveRequireWhenCreatedMoreThan
    Move computer only if it was created more than the specified number of days ago.
 
    .PARAMETER MoveLastLogonDateOlderThan
    Move computer only if it has a LastLogonDate that is older than the specified date.
 
    .PARAMETER MoveLastSeenAzureMoreThan
    Move computer only if it Last Seen in Azure is more than the specified number of days.
    Please note that you need to make connection to Azure using Connect-MgGraph with proper permissions first.
    Additionally yopu will need GraphEssentials PowerShell Module installed.
 
    .PARAMETER MoveLastSeenIntuneMoreThan
    Move computer only if it Last Seen in Intune is more than the specified number of days.
    Please note that you need to make connection to Intune using Connect-MgGraph with proper permissions first.
    Additionally you will need GraphEssentials PowerShell Module installed.
 
    .PARAMETER MoveLastSyncAzureMoreThan
    Move computer only if it Last Synced in Azure is more than the specified number of days.
    Please note that you need to make connection to Azure AD using Connect-MgGraph with proper permissions first.
    Additionally you will need GraphEssentials PowerShell Module installed.
 
    .PARAMETER MoveLastContactJamfMoreThan
    Move computer only if it Last Contacted in Jamf is more than the specified number of days.
    Please note that you need to make connection to Jamf using PowerJamf PowerShell Module first.
    Additionally you will need PowerJamf PowerShell Module installed.
 
    .PARAMETER MoveExcludeSystems
    Move computer only if it's not on the list of excluded operating systems.
    If you want to exclude Windows 10, you can specify 'Windows 10' or 'Windows 10*'
    or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'.
    You can also specify multiple operating systems by separating them with a comma.
    It's using the -like operator, so you can use wildcards.
    It's using OperatingSystem property of the computer object for comparison.
 
    .PARAMETER MoveIncludeSystems
    Move computer only if it's on the list of included operating systems.
    If you want to include Windows 10, you can specify 'Windows 10' or 'Windows 10*'
    or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'.
    You can also specify multiple operating systems by separating them with a comma.
    It's using the -like operator, so you can use wildcards.
 
    .PARAMETER MoveExcludeServicePrincipalName
    Move computer only if it's not on the list of excluded ServicePrincipalNames.
    You can also specify multiple ServicePrincipalNames by providing an array of entries.
    It's using the -like operator, so you can use wildcards.
 
    .PARAMETER MoveIncludeServicePrincipalName
    Move computer only if it's on the list of included ServicePrincipalNames.
    You can also specify multiple ServicePrincipalNames by providing an array of entries.
    It's using the -like operator, so you can use wildcards.
 
    .PARAMETER MoveTargetOrganizationalUnit
    Target Organizational Unit where the computer will be moved as part of Move action.
    It can take a string with DistinguishedName, or hashtable with key being the domain, and value being the DistinguishedName.
    If you have a forest with multiple domains and want to move computers to different OUs based on their domain, you can use hashtable.
 
    .PARAMETER MoveLimit
    Limit the number of computers that will be moved. 0 = unlimited. Default is 1.
    This is to prevent accidental move of all computers that meet the criteria.
    Adjust the limit to your needs.
 
    .PARAMETER MoveDoNotAddToPendingList
    By default the script will add computers that are moved to a list of computers that will be actioned later (deleted).
    If you want to move computers, but not add them to the list of computers that will be action later (aka pending list), use this switch.
 
    .PARAMETER DeleteLimit
    Limit the number of computers that will be deleted. 0 = unlimited. Default is 1.
    This is to prevent accidental deletion of all computers that meet the criteria.
    Adjust the limit to your needs.
 
    .PARAMETER DisableLimit
    Limit the number of computers that will be disabled. 0 = unlimited. Default is 1.
    This is to prevent accidental disabling of all computers that meet the criteria.
    Adjust the limit to your needs.
 
    .PARAMETER Exclusions
    List of computers to exclude from the process.
    You can specify multiple computers by separating them with a comma.
    It's using the -like operator, so you can use wildcards.
    You can use SamAccoutName (remember about ending $), DistinguishedName,
    or DNSHostName property of the computer object for comparison.
 
    .PARAMETER DisableModifyDescription
    Modify the description of the computer object to include the date and time when it was disabled.
    By default it will not modify the description.
 
    .PARAMETER DisableModifyAdminDescription
    Modify the admin description of the computer object to include the date and time when it was disabled.
    By default it will not modify the admin description.
 
    .PARAMETER Filter
    Filter to use when searching for computers in Get-ADComputer cmdlet.
    Default is '*'
 
    .PARAMETER SearchBase
    SearchBase to use when searching for computers in Get-ADComputer cmdlet.
    Default is not set. It will search the whole domain.
    You can provide a string or hashtable of domains with their SearchBase.
 
    .PARAMETER DataStorePath
    Path to the XML file that will be used to store the list of processed computers, current run, and history data.
    Default is $PSScriptRoot\ProcessedComputers.xml
 
    .PARAMETER ReportOnly
    Only generate the report, don't disable or delete computers.
 
    .PARAMETER ReportMaximum
    Maximum number of reports to keep. Default is Unlimited (0).
 
    .PARAMETER WhatIfDelete
    WhatIf parameter for the Delete process.
    It's not nessessary to specify this parameter if you use WhatIf parameter which applies to all processes.
 
    .PARAMETER WhatIfDisable
    WhatIf parameter for the Disable process.
    It's not nessessary to specify this parameter if you use WhatIf parameter which applies to all processes.
 
    .PARAMETER WhatIfMove
    WhatIf parameter for the Move process.
    It's not nessessary to specify this parameter if you use WhatIf parameter which applies to all processes.
 
    .PARAMETER LogPath
    Path to the log file. Default is no logging to file.
 
    .PARAMETER LogMaximum
    Maximum number of log files to keep. Default is 5.
 
    .PARAMETER LogShowTime
    Show time in the log file. Default is $false
 
    .PARAMETER LogTimeFormat
    Time format to use when logging to file. Default is 'yyyy-MM-dd HH:mm:ss'
 
    .PARAMETER Suppress
    Suppress output of the object and only display to console
 
    .PARAMETER ShowHTML
    Show HTML report in the browser once the function is complete
 
    .PARAMETER Online
    Online parameter causes HTML report to use CDN for CSS and JS files.
    This can be useful to minimize the size of the HTML report.
    Otherwise the report will start with at least 2MB in size.
 
    .PARAMETER ReportPath
    Path to the HTML report file. Default is $PSScriptRoot\ProcessedComputers.html
 
    .PARAMETER SafetyADLimit
    Minimum number of computers that must be returned by AD cmdlets to proceed with the process.
    Default is not to check.
    This is there to prevent accidental deletion of all computers if there is a problem with AD.
 
    .PARAMETER SafetyAzureADLimit
    Minimum number of computers that must be returned by AzureAD cmdlets to proceed with the process.
    Default is not to check.
    This is there to prevent accidental deletion of all computers if there is a problem with AzureAD.
    It only applies if Azure AD parameters are used.
 
    .PARAMETER SafetyIntuneLimit
    Minimum number of computers that must be returned by Intune cmdlets to proceed with the process.
    Default is not to check.
    This is there to prevent accidental deletion of all computers if there is a problem with Intune.
    It only applies if Intune parameters are used.
 
    .PARAMETER SafetyJamfLimit
    Minimum number of computers that must be returned by Jamf cmdlets to proceed with the process.
    Default is not to check.
    This is there to prevent accidental deletion of all computers if there is a problem with Jamf.
    It only applies if Jamf parameters are used.
 
    .PARAMETER DontWriteToEventLog
    By default the function will write to the event log making sure the cleanup process is logged.
    This parameter will prevent the function from writing to the event log.
 
    .PARAMETER TargetServers
    Target servers to use when connecting to Active Directory.
    It can take a string with server name, or hashtable with key being the domain, and value being the server name.
    If you have a forest with multiple domains and want to use different servers for different domains, you can use hashtable.
    It will use the default server if no server is provided for a domain, which is default approach.
    This feature is only nessecary if you have specific requirments per domain/forest rather than using the automatic detection.
 
    .EXAMPLE
    $Output = Invoke-ADComputersCleanup -DeleteIsEnabled $false -Delete -WhatIfDelete -ShowHTML -ReportOnly -LogPath $PSScriptRoot\Logs\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log -ReportPath $PSScriptRoot\Reports\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html
    $Output
 
    .EXAMPLE
    $Output = Invoke-ADComputersCleanup -DeleteListProcessedMoreThan 100 -Disable -DeleteIsEnabled $false -Delete -WhatIfDelete -ShowHTML -ReportOnly -LogPath $PSScriptRoot\Logs\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log -ReportPath $PSScriptRoot\Reports\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html
    $Output
 
    .EXAMPLE
    # this is a fresh run and it will provide report only according to it's defaults
    $Output = Invoke-ADComputersCleanup -WhatIf -ReportOnly -Disable -Delete -ShowHTML
    $Output
 
    .EXAMPLE
    # this is a fresh run and it will try to disable computers according to it's defaults
    # read documentation to understand what it does
    $Output = Invoke-ADComputersCleanup -Disable -ShowHTML -WhatIfDisable -WhatIfDelete -Delete
    $Output
 
    .EXAMPLE
    # this is a fresh run and it will try to delete computers according to it's defaults
    # read documentation to understand what it does
    $Output = Invoke-ADComputersCleanup -Delete -WhatIfDelete -ShowHTML -LogPath $PSScriptRoot\Logs\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log -ReportPath $PSScriptRoot\Reports\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html
    $Output
 
    .EXAMPLE
    # Run the script
    $Configuration = @{
        Disable = $true
        DisableNoServicePrincipalName = $null
        DisableIsEnabled = $true
        DisableLastLogonDateMoreThan = 90
        DisablePasswordLastSetMoreThan = 90
        DisableExcludeSystems = @(
            # 'Windows Server*'
        )
        DisableIncludeSystems = @()
        DisableLimit = 2 # 0 means unlimited, ignored for reports
        DisableModifyDescription = $false
        DisableAdminModifyDescription = $true
 
        Delete = $true
        DeleteIsEnabled = $false
        DeleteNoServicePrincipalName = $null
        DeleteLastLogonDateMoreThan = 180
        DeletePasswordLastSetMoreThan = 180
        DeleteListProcessedMoreThan = 90 # 90 days since computer was added to list
        DeleteExcludeSystems = @(
            # 'Windows Server*'
        )
        DeleteIncludeSystems = @(
 
        )
        DeleteLimit = 2 # 0 means unlimited, ignored for reports
 
        Exclusions = @(
            '*OU=Domain Controllers*'
            '*OU=Servers,OU=Production*'
            'EVOMONSTER$'
            'EVOMONSTER.AD.EVOTEC.XYZ'
        )
 
        Filter = '*'
        WhatIfDisable = $true
        WhatIfDelete = $true
        LogPath = "$PSScriptRoot\Logs\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log"
        DataStorePath = "$PSScriptRoot\DeleteComputers_ListProcessed.xml"
        ReportPath = "$PSScriptRoot\Reports\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html"
        ShowHTML = $true
    }
 
    # Run one time as admin: Write-Event -ID 10 -LogName 'Application' -EntryType Information -Category 0 -Message 'Initialize' -Source 'CleanupComputers'
    $Output = Invoke-ADComputersCleanup @Configuration
    $Output
 
    .NOTES
    General notes
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [string] $Forest,
        [alias('Domain')][string[]] $IncludeDomains,
        [string[]] $ExcludeDomains,
        # Disable options
        [switch] $Disable,
        [switch] $DisableAndMove,
        [ValidateSet(
            'DisableAndMove',
            'MoveAndDisable'
        )][string] $DisableAndMoveOrder = 'DisableAndMove',
        [nullable[bool]] $DisableIsEnabled,
        [nullable[bool]] $DisableNoServicePrincipalName,
        [nullable[int]] $DisableLastLogonDateMoreThan = 180,
        [nullable[int]] $DisablePasswordLastSetMoreThan = 180,
        [nullable[int]] $DisableRequireWhenCreatedMoreThan = 90,
        [nullable[DateTime]] $DisablePasswordLastSetOlderThan,
        [nullable[DateTime]] $DisableLastLogonDateOlderThan,
        [nullable[int]] $DisableLastSeenAzureMoreThan,
        [nullable[int]] $DisableLastSeenIntuneMoreThan,
        [nullable[int]] $DisableLastSyncAzureMoreThan,
        [nullable[int]] $DisableLastContactJamfMoreThan,
        [Array] $DisableExcludeSystems = @(),
        [Array] $DisableIncludeSystems = @(),
        [Array] $DisableExcludeServicePrincipalName = @(),
        [Array] $DisableIncludeServicePrincipalName = @(),
        [int] $DisableLimit = 1, # 0 = unlimited
        [Object] $DisableMoveTargetOrganizationalUnit,
        [switch] $DisableDoNotAddToPendingList,
        # Move options
        [switch] $Move,
        [nullable[bool]] $MoveIsEnabled,
        [nullable[bool]] $MoveNoServicePrincipalName,
        [nullable[int]] $MoveLastLogonDateMoreThan,
        [nullable[int]] $MovePasswordLastSetMoreThan,
        [nullable[int]] $MoveListProcessedMoreThan,
        [nullable[int]] $MoveRequireWhenCreatedMoreThan,
        [nullable[DateTime]] $MovePasswordLastSetOlderThan,
        [nullable[DateTime]] $MoveLastLogonDateOlderThan,
        [nullable[int]] $MoveLastSeenAzureMoreThan,
        [nullable[int]] $MoveLastSeenIntuneMoreThan,
        [nullable[int]] $MoveLastSyncAzureMoreThan,
        [nullable[int]] $MoveLastContactJamfMoreThan,
        [Array] $MoveExcludeSystems = @(),
        [Array] $MoveIncludeSystems = @(),
        [Array] $MoveExcludeServicePrincipalName = @(),
        [Array] $MoveIncludeServicePrincipalName = @(),
        [int] $MoveLimit = 1, # 0 = unlimited
        [Object] $MoveTargetOrganizationalUnit,
        # Delete options
        [switch] $Delete,
        [nullable[bool]] $DeleteIsEnabled,
        [nullable[bool]] $DeleteNoServicePrincipalName,
        [nullable[int]] $DeleteLastLogonDateMoreThan = 180,
        [nullable[int]] $DeletePasswordLastSetMoreThan = 180,
        [nullable[int]] $DeleteRequireWhenCreatedMoreThan = 90,
        [nullable[int]] $DeleteListProcessedMoreThan,
        [nullable[DateTime]] $DeletePasswordLastSetOlderThan,
        [nullable[DateTime]] $DeleteLastLogonDateOlderThan,
        [nullable[int]] $DeleteLastSeenAzureMoreThan,
        [nullable[int]] $DeleteLastSeenIntuneMoreThan,
        [nullable[int]] $DeleteLastSyncAzureMoreThan,
        [nullable[int]] $DeleteLastContactJamfMoreThan,
        [Array] $DeleteExcludeSystems = @(),
        [Array] $DeleteIncludeSystems = @(),
        [Array] $DeleteExcludeServicePrincipalName = @(),
        [Array] $DeleteIncludeServicePrincipalName = @(),
        [int] $DeleteLimit = 1, # 0 = unlimited
        # General options
        [Array] $Exclusions = @(
            # default globalexclusions
            '*OU=Domain Controllers*'
        ),
        [switch] $DisableModifyDescription,
        [alias('DisableAdminModifyDescription')][switch] $DisableModifyAdminDescription,
        [object] $Filter = '*',
        [object] $SearchBase,
        [string] $DataStorePath,
        [switch] $ReportOnly,
        [int] $ReportMaximum,
        [switch] $WhatIfDelete,
        [switch] $WhatIfDisable,
        [switch] $WhatIfMove,
        [string] $LogPath,
        [int] $LogMaximum = 5,
        [switch] $LogShowTime,
        [string] $LogTimeFormat,
        [switch] $Suppress,
        [switch] $ShowHTML,
        [switch] $Online,
        [string] $ReportPath,
        [nullable[int]] $SafetyADLimit,
        [nullable[int]] $SafetyAzureADLimit,
        [nullable[int]] $SafetyIntuneLimit,
        [nullable[int]] $SafetyJamfLimit,
        [switch] $DontWriteToEventLog,
        [Object] $TargetServers
    )
    # we will use it to check for intune/azuread/jamf functionality
    $Script:CleanupOptions = [ordered] @{}

    # just in case user wants to use -WhatIf instead of -WhatIfDelete and -WhatIfDisable
    if (-not $WhatIfDelete -and -not $WhatIfDisable) {
        $WhatIfDelete = $WhatIfDisable = $WhatIfPreference
    }

    # lets enable global logging
    Set-LoggingCapabilities -LogPath $LogPath -LogMaximum $LogMaximum -ShowTime:$LogShowTime -TimeFormat $LogTimeFormat -ScriptPath $MyInvocation.ScriptName

    # prepare configuration
    $DisableOnlyIf = [ordered] @{
        # Active directory
        IsEnabled                    = $DisableIsEnabled
        NoServicePrincipalName       = $DisableNoServicePrincipalName
        LastLogonDateMoreThan        = $DisableLastLogonDateMoreThan
        PasswordLastSetMoreThan      = $DisablePasswordLastSetMoreThan
        ExcludeSystems               = $DisableExcludeSystems
        IncludeSystems               = $DisableIncludeSystems
        ExcludeServicePrincipalName  = $DisableExcludeServicePrincipalName
        IncludeServicePrincipalName  = $DisableIncludeServicePrincipalName
        PasswordLastSetOlderThan     = $DisablePasswordLastSetOlderThan
        LastLogonDateOlderThan       = $DisableLastLogonDateOlderThan
        # Intune
        LastSeenIntuneMoreThan       = $DisableLastSeenIntuneMoreThan
        # Azure
        LastSyncAzureMoreThan        = $DisableLastSyncAzureMoreThan
        LastSeenAzureMoreThan        = $DisableLastSeenAzureMoreThan
        # Jamf
        LastContactJamfMoreThan      = $DisableLastContactJamfMoreThan

        DoNotAddToPendingList        = $DisableDoNotAddToPendingList
        MoveTargetOrganizationalUnit = $DisableMoveTargetOrganizationalUnit
        DisableAndMove               = $DisableAndMove.IsPresent
        RequireWhenCreatedMoreThan   = $DisableRequireWhenCreatedMoreThan
    }

    $MoveOnlyIf = [ordered] @{
        # Active directory
        IsEnabled                   = $MoveIsEnabled
        NoServicePrincipalName      = $MoveNoServicePrincipalName
        LastLogonDateMoreThan       = $MoveLastLogonDateMoreThan
        PasswordLastSetMoreThan     = $MovePasswordLastSetMoreThan
        ListProcessedMoreThan       = $MoveListProcessedMoreThan
        ExcludeSystems              = $MoveExcludeSystems
        IncludeSystems              = $MoveIncludeSystems
        ExcludeServicePrincipalName = $MoveExcludeServicePrincipalName
        IncludeServicePrincipalName = $MoveIncludeServicePrincipalName
        PasswordLastSetOlderThan    = $MovePasswordLastSetOlderThan
        LastLogonDateOlderThan      = $MoveLastLogonDateOlderThan
        # Intune
        LastSeenIntuneMoreThan      = $MoveLastSeenIntuneMoreThan
        # Azure
        LastSeenAzureMoreThan       = $MoveLastSeenAzureMoreThan
        LastSyncAzureMoreThan       = $MoveLastSyncAzureMoreThan
        # Jamf
        LastContactJamfMoreThan     = $MoveLastContactJamfMoreThan
        # special option for move only
        TargetOrganizationalUnit    = $MoveTargetOrganizationalUnit

        DoNotAddToPendingList       = $MoveDoNotAddToPendingList
        #MoveTargetOrganizationalUnit = $MoveTargetOrganizationalUnit
        RequireWhenCreatedMoreThan  = $MoveRequireWhenCreatedMoreThan
    }

    $DeleteOnlyIf = [ordered] @{
        # Active directory
        IsEnabled                   = $DeleteIsEnabled
        NoServicePrincipalName      = $DeleteNoServicePrincipalName
        LastLogonDateMoreThan       = $DeleteLastLogonDateMoreThan
        PasswordLastSetMoreThan     = $DeletePasswordLastSetMoreThan
        ListProcessedMoreThan       = $DeleteListProcessedMoreThan
        ExcludeSystems              = $DeleteExcludeSystems
        IncludeSystems              = $DeleteIncludeSystems
        ExcludeServicePrincipalName = $DeleteExcludeServicePrincipalName
        IncludeServicePrincipalName = $DeleteIncludeServicePrincipalName
        PasswordLastSetOlderThan    = $DeletePasswordLastSetOlderThan
        LastLogonDateOlderThan      = $DeleteLastLogonDateOlderThan
        # Intune
        LastSeenIntuneMoreThan      = $DeleteLastSeenIntuneMoreThan
        # Azure
        LastSeenAzureMoreThan       = $DeleteLastSeenAzureMoreThan
        LastSyncAzureMoreThan       = $DeleteLastSyncAzureMoreThan
        # Jamf
        LastContactJamfMoreThan     = $DeleteLastContactJamfMoreThan
        RequireWhenCreatedMoreThan  = $DeleteRequireWhenCreatedMoreThan
    }

    if (-not $DataStorePath) {
        $DataStorePath = $($MyInvocation.PSScriptRoot) + '\ProcessedComputers.xml'
    }
    if (-not $ReportPath) {
        $ReportPath = $($MyInvocation.PSScriptRoot) + '\ProcessedComputers.html'
    }

    # lets create report path, reporting is enabled by default
    Set-ReportingCapabilities -ReportPath $ReportPath -ReportMaximum $ReportMaximum -ScriptPath $MyInvocation.ScriptName

    $Success = Assert-InitialSettings -DisableOnlyIf $DisableOnlyIf -MoveOnlyIf $MoveOnlyIf -DeleteOnlyIf $DeleteOnlyIf
    if ($Success -contains $false) {
        return
    }

    $Today = Get-Date
    $Properties = 'DistinguishedName', 'DNSHostName', 'SamAccountName', 'Enabled', 'OperatingSystem', 'OperatingSystemVersion', 'LastLogonDate', 'PasswordLastSet', 'PasswordExpired', 'servicePrincipalName', 'logonCount', 'ManagedBy', 'Description', 'WhenCreated', 'WhenChanged', 'ProtectedFromAccidentalDeletion'

    $Export = [ordered] @{
        Version         = Get-GitHubVersion -Cmdlet 'Invoke-ADComputersCleanup' -RepositoryOwner 'evotecit' -RepositoryName 'CleanupMonster'
        CurrentRun      = $null
        History         = $null
        PendingDeletion = $null
    }

    Write-Color '[i] ', "[CleanupMonster] ", 'Version', ' [Informative] ', $Export['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta
    Write-Color -Text "[i] ", "Started process of cleaning up stale computers" -Color Yellow, White
    Write-Color -Text "[i] ", "Executed by: ", $Env:USERNAME, ' from domain ', $Env:USERDNSDOMAIN -Color Yellow, White, Green, White

    try {
        $ForestInformation = Get-WinADForestDetails -PreferWritable -Forest $Forest -Extended -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains
    } catch {
        Write-Color -Text "[i] ", "Couldn't get forest. Terminating. Lack of domain contact? Error: $($_.Exception.Message)." -Color Yellow, Red
        return
    }

    if (-not $ReportOnly) {
        $ProcessedComputers = Import-ComputersData -Export $Export -DataStorePath $DataStorePath
        if ($ProcessedComputers -eq $false) {
            return
        }
        Write-Color -Text "[i] ", "Loaded ", $($ProcessedComputers.Count), " computers from $($DataStorePath) and added to pending list of computers." -Color Yellow, White, Green, White
    }

    if (-not $Disable -and -not $Delete) {
        Write-Color -Text "[i] ", "No action can be taken. You need to enable Disable or/and Delete feature to have any action." -Color Yellow, Red
        return
    }

    $Report = [ordered] @{}

    $getInitialGraphComputersSplat = [ordered] @{
        SafetyAzureADLimit            = $SafetyAzureADLimit
        SafetyIntuneLimit             = $SafetyIntuneLimit
        DeleteLastSeenAzureMoreThan   = $DeleteLastSeenAzureMoreThan
        DeleteLastSeenIntuneMoreThan  = $DeleteLastSeenIntuneMoreThan
        DeleteLastSyncAzureMoreThan   = $DeleteLastSyncAzureMoreThan
        DisableLastSeenAzureMoreThan  = $DisableLastSeenAzureMoreThan
        DisableLastSeenIntuneMoreThan = $DisableLastSeenIntuneMoreThan
        DisableLastSyncAzureMoreThan  = $DisableLastSyncAzureMoreThan
        MoveLastSeenAzureMoreThan     = $MoveLastSeenAzureMoreThan
        MoveLastSeenIntuneMoreThan    = $MoveLastSeenIntuneMoreThan
        MoveLastSyncAzureMoreThan     = $MoveLastSyncAzureMoreThan
    }
    Remove-EmptyValue -Hashtable $getInitialGraphComputersSplat
    $AzureInformationCache = Get-InitialGraphComputers @getInitialGraphComputersSplat
    if ($AzureInformationCache -eq $false) {
        return
    }

    $getInitialJamf = @{
        DisableLastContactJamfMoreThan = $DisableLastContactJamfMoreThan
        DeleteLastContactJamfMoreThan  = $DeleteLastContactJamfMoreThan
        MoveLastContactJamfMoreThan    = $MoveLastContactJamfMoreThan
        SafetyJamfLimit                = $SafetyJamfLimit
    }
    Remove-EmptyValue -Hashtable $getInitialJamf
    $JamfInformationCache = Get-InitialJamfComputers @getInitialJamf
    if ($JamfInformationCache -eq $false) {
        return
    }

    $SplatADComputers = [ordered] @{
        Report                = $Report
        ForestInformation     = $ForestInformation
        Filter                = $Filter
        SearchBase            = $SearchBase
        Properties            = $Properties
        Disable               = $Disable
        Delete                = $Delete
        Move                  = $Move
        DisableOnlyIf         = $DisableOnlyIf
        DeleteOnlyIf          = $DeleteOnlyIf
        MoveOnlyIf            = $MoveOnlyIf
        Exclusions            = $Exclusions
        ProcessedComputers    = $ProcessedComputers
        SafetyADLimit         = $SafetyADLimit
        AzureInformationCache = $AzureInformationCache
        JamfInformationCache  = $JamfInformationCache
        TargetServers         = $TargetServers
    }

    $AllComputers = Get-InitialADComputers @SplatADComputers
    if ($AllComputers -eq $false) {
        return
    }

    foreach ($Domain in $Report.Keys) {
        if ($Disable -or $DisableAndMove) {
            if ($DisableLimit -eq 0) {
                $DisableLimitText = 'Unlimited'
            } else {
                $DisableLimitText = $DisableLimit
            }
            Write-Color "[i] ", "Computers to be disabled for domain $Domain`: ", $Report["$Domain"]['ComputersToBeDisabled'], ". Current disable limit: ", $DisableLimitText -Color Yellow, Cyan, Green, Cyan, Yellow
        }

        if ($Move) {
            if ($MoveLimit -eq 0) {
                $MoveLimitText = 'Unlimited'
            } else {
                $MoveLimitText = $MoveLimit
            }
            Write-Color "[i] ", "Computers to be moved for domain $Domain`: ", $Report["$Domain"]['ComputersToBeMoved'], ". Current move limit: ", $MoveLimitText -Color Yellow, Cyan, Green, Cyan, Yellow
        }

        if ($Delete) {
            if ($DeleteLimit -eq 0) {
                $DeleteLimitText = 'Unlimited'
            } else {
                $DeleteLimitText = $DeleteLimit
            }
            Write-Color "[i] ", "Computers to be deleted for domain $Domain`: ", $Report["$Domain"]['ComputersToBeDeleted'], ". Current delete limit: ", $DeleteLimitText -Color Yellow, Cyan, Green, Cyan, Yellow
        }
    }


    if ($Disable -or $DisableAndMove) {
        $requestADComputersDisableSplat = @{
            # those 2 are added only to make sure we don't add to processing list
            # if there is no process later on
            Delete                              = $Delete
            Move                                = $Move
            # we can disable and move on one go
            DisableAndMove                      = $DisableAndMove
            Report                              = $Report
            WhatIfDisable                       = $WhatIfDisable
            WhatIf                              = $WhatIfPreference
            DisableModifyDescription            = $DisableModifyDescription.IsPresent
            DisableModifyAdminDescription       = $DisableModifyAdminDescription.IsPresent
            DisableLimit                        = $DisableLimit
            ReportOnly                          = $ReportOnly
            Today                               = $Today
            DontWriteToEventLog                 = $DontWriteToEventLog
            DisableMoveTargetOrganizationalUnit = $DisableMoveTargetOrganizationalUnit

            DoNotAddToPendingList               = $DisableDoNotAddToPendingList
            DisableAndMoveOrder                 = $DisableAndMoveOrder
        }
        [Array] $ReportDisabled = Request-ADComputersDisable @requestADComputersDisableSplat
    }

    if ($Move) {
        $requestADComputersMoveSplat = @{
            Report                   = $Report
            WhatIfMove               = $WhatIfMove
            WhatIf                   = $WhatIfPreference
            MoveLimit                = $MoveLimit
            ReportOnly               = $ReportOnly
            Today                    = $Today
            ProcessedComputers       = $ProcessedComputers
            TargetOrganizationalUnit = $MoveTargetOrganizationalUnit
            DontWriteToEventLog      = $DontWriteToEventLog
            Delete                   = $Delete

            DoNotAddToPendingList    = $MoveDoNotAddToPendingList
        }
        [Array] $ReportMoved = Request-ADComputersMove @requestADComputersMoveSplat
    }

    if ($Delete) {
        $requestADComputersDeleteSplat = @{
            Report              = $Report
            WhatIfDelete        = $WhatIfDelete
            WhatIf              = $WhatIfPreference
            DeleteLimit         = $DeleteLimit
            ReportOnly          = $ReportOnly
            Today               = $Today
            ProcessedComputers  = $ProcessedComputers
            DontWriteToEventLog = $DontWriteToEventLog
        }
        [Array] $ReportDeleted = Request-ADComputersDelete @requestADComputersDeleteSplat
    }

    Write-Color "[i] ", "Cleanup process for processed computers that no longer exists in AD" -Color Yellow, Green
    foreach ($FullName in [string[]] $ProcessedComputers.Keys) {
        if (-not $AllComputers["$($FullName)"]) {
            Write-Color -Text "[*] Removing computer from pending list ", $ProcessedComputers[$FullName].SamAccountName, " ($($ProcessedComputers[$FullName].DistinguishedName))" -Color Yellow, Green, Yellow
            $ProcessedComputers.Remove("$($FullName)")
        }
    }

    # Building up summary
    $Export.PendingDeletion = $ProcessedComputers
    $Export.CurrentRun = @(
        if ($ReportDisabled.Count -gt 0) {
            $ReportDisabled
        }
        if ($ReportMoved.Count -gt 0) {
            $ReportMoved
        }
        if ($ReportDeleted.Count -gt 0) {
            $ReportDeleted
        }
    )
    $Export.History = @(
        if ($Export.History) {
            $Export.History
        }
        if ($ReportDisabled.Count -gt 0) {
            $ReportDisabled
        }
        if ($ReportMoved.Count -gt 0) {
            $ReportMoved
        }
        if ($ReportDeleted.Count -gt 0) {
            $ReportDeleted
        }
    )

    Write-Color "[i] ", "Exporting Processed List" -Color Yellow, Magenta
    if (-not $ReportOnly) {
        try {
            $Export | Export-Clixml -LiteralPath $DataStorePath -Encoding Unicode -WhatIf:$false -ErrorAction Stop
        } catch {
            Write-Color -Text "[-] Exporting Processed List failed. Error: $($_.Exception.Message)" -Color Yellow, Red
        }
    }
    Write-Color -Text "[i] ", "Summary of cleaning up stale computers" -Color Yellow, Cyan
    foreach ($Domain in $Report.Keys) {
        if ($Disable -or $DisableAndMove) {
            Write-Color -Text "[i] ", "Computers to be disabled for domain $Domain`: ", $Report["$Domain"]['ComputersToBeDisabled'] -Color Yellow, Cyan, Green
        }
        if ($Move) {
            Write-Color -Text "[i] ", "Computers to be moved for domain $Domain`: ", $Report["$Domain"]['ComputersToBeMoved'] -Color Yellow, Cyan, Green
        }
        if ($Delete) {
            Write-Color -Text "[i] ", "Computers to be deleted for domain $Domain`: ", $Report["$Domain"]['ComputersToBeDeleted'] -Color Yellow, Cyan, Green
        }
    }
    if (-not $ReportOnly) {
        Write-Color -Text "[i] ", "Computers on pending list`: ", $Export['PendingDeletion'].Count -Color Yellow, Cyan, Green
    }
    if (($Disable -or $DisableAndMove) -and -not $ReportOnly) {
        Write-Color -Text "[i] ", "Computers disabled in this run`: ", $ReportDisabled.Count -Color Yellow, Cyan, Green
    }
    if ($Move -and -not $ReportOnly) {
        Write-Color -Text "[i] ", "Computers moved in this run`: ", $ReportMoved.Count -Color Yellow, Cyan, Green
    }
    if ($Delete -and -not $ReportOnly) {
        Write-Color -Text "[i] ", "Computers deleted in this run`: ", $ReportDeleted.Count -Color Yellow, Cyan, Green
    }

    if ($Export -and $ReportPath) {
        [Array] $ComputersToProcess = foreach ($Domain in $Report.Keys) {
            if ($Report["$Domain"]['Computers'].Count -gt 0) {
                $Report["$Domain"]['Computers']
            }
        }
        Write-Color -Text "[i] ", "Computers to be processed for HTML report`: ", $ComputersToProcess.Count -Color Yellow, Cyan, Green
        $Export.Statistics = New-ADComputersStatistics -ComputersToProcess $ComputersToProcess

        $newHTMLProcessedComputersSplat = @{
            Export             = $Export
            FilePath           = $ReportPath
            Online             = $Online.IsPresent
            ShowHTML           = $ShowHTML.IsPresent
            LogFile            = $LogPath
            ComputersToProcess = $ComputersToProcess
            DisableOnlyIf      = $DisableOnlyIf
            DeleteOnlyIf       = $DeleteOnlyIf
            MoveOnlyIf         = $MoveOnlyIf
            Delete             = $Delete
            Disable            = $Disable
            Move               = $Move
            ReportOnly         = $ReportOnly
        }
        Write-Color "[i] ", "Generating HTML report ($ReportPath)" -Color Yellow, Magenta
        New-HTMLProcessedComputers @newHTMLProcessedComputersSplat
    }

    Write-Color -Text "[i] Finished process of cleaning up stale computers" -Color Green

    if (-not $Suppress) {
        $Export.EmailBody = New-EmailBodyComputers -CurrentRun $Export.CurrentRun
        $Export
    }
}
function Invoke-ADSIDHistoryCleanup {
    <#
    .SYNOPSIS
    Cleans up SID history entries in Active Directory based on various filtering criteria.
 
    .DESCRIPTION
    This function identifies and removes SID history entries from AD objects based on specified filters.
    It can target internal domains (same forest), external domains (trusted), or unknown domains.
    The function allows for detailed reporting before making any changes.
 
    .PARAMETER Forest
    The name of the forest to process. If not specified, uses the current forest.
 
    .PARAMETER IncludeDomains
    An array of domain names to include in the cleanup process.
 
    .PARAMETER ExcludeDomains
    An array of domain names to exclude from the cleanup process.
 
    .PARAMETER IncludeOrganizationalUnit
    An array of organizational units to include in the cleanup process.
 
    .PARAMETER ExcludeOrganizationalUnit
    An array of organizational units to exclude from the cleanup process.
 
    .PARAMETER IncludeSIDHistoryDomain
    An array of domain SIDs to include when cleaning up SID history.
 
    .PARAMETER ExcludeSIDHistoryDomain
    An array of domain SIDs to exclude when cleaning up SID history.
 
    .PARAMETER RemoveLimitSID
    Limits the total number of SID history entries to remove.
 
    .PARAMETER RemoveLimitObject
    Limits the total number of objects to process for SID history removal. Defaults to 1 to prevent accidental mass deletions.
 
    .PARAMETER IncludeType
    Specifies which types of SID history to include: 'Internal', 'External', or 'Unknown'.
    Defaults to all three types if not specified.
 
    .PARAMETER ExcludeType
    Specifies which types of SID history to exclude: 'Internal', 'External', or 'Unknown'.
 
    .PARAMETER DisabledOnly
    Only processes objects that are disabled.
 
    .PARAMETER SafetyADLimit
    Stops processing if the number of objects with SID history in AD is less than the specified limit.
 
    .PARAMETER LogPath
    The path to the log file to write.
 
    .PARAMETER LogMaximum
    The maximum number of log files to keep.
 
    .PARAMETER LogShowTime
    If specified, includes the time in the log entries.
 
    .PARAMETER LogTimeFormat
    The format to use for the time in the log entries.
 
    .PARAMETER Suppress
    Suppresses the output of the function and only returns the summary information.
 
    .PARAMETER ShowHTML
    If specified, shows the HTML report in the default browser.
 
    .PARAMETER Online
    If specified, uses online resources in HTML report (CSS/JS is loaded from CDN). Otherwise local resources are used (bigger HTML file).
 
    .PARAMETER DataStorePath
    Path to the XML file used to store processed SID history entries.
 
    .PARAMETER ReportOnly
    If specified, only generates a report without making any changes.
 
    .PARAMETER Report
    Generates a report of affected objects without making any changes.
 
    .PARAMETER ReportPath
    The path where the HTML report should be saved. Used with the -Report parameter.
 
    .PARAMETER WhatIf
    Shows what would happen if the function runs. The SID history entries aren't actually removed.
 
    .EXAMPLE
    Invoke-ADSIDHistoryCleanup -Forest "contoso.com" -IncludeType "External" -ReportOnly -ReportPath "C:\Temp\SIDHistoryReport.html" -WhatIf
 
    Generates a report of external SID history entries in the contoso.com forest without making any changes.
 
    .EXAMPLE
    Invoke-ADSIDHistoryCleanup -IncludeDomains "domain1.local" -IncludeType "Internal" -RemoveLimitSID 2 -WhatIf
 
    Removes up to 2 internal SID history entries from objects in domain1.local.
 
    .EXAMPLE
    Invoke-ADSIDHistoryCleanup -ExcludeSIDHistoryDomain "S-1-5-21-1234567890-1234567890-1234567890" -WhatIf -RemoveLimitObject 2
 
    Shows what SID history entries would be removed while excluding entries from the specified domain SID. Limits the number of objects to process to 2.
 
    .EXAMPLE
    # Prepare splat
    $invokeADSIDHistoryCleanupSplat = @{
        Verbose = $true
        WhatIf = $true
        IncludeSIDHistoryDomain = @(
            'S-1-5-21-3661168273-3802070955-2987026695'
            'S-1-5-21-853615985-2870445339-3163598659'
        )
        IncludeType = 'External'
        RemoveLimitSID = 1
        RemoveLimitObject = 2
 
        SafetyADLimit = 1
        ShowHTML = $true
        Online = $true
        DisabledOnly = $true
        #ReportOnly = $true
        LogPath = "C:\Temp\ProcessedSIDHistory.log"
        ReportPath = "$PSScriptRoot\ProcessedSIDHistory.html"
        DataStorePath = "$PSScriptRoot\ProcessedSIDHistory.xml"
    }
 
    # Run the script
    $Output = Invoke-ADSIDHistoryCleanup @invokeADSIDHistoryCleanupSplat
    $Output | Format-Table -AutoSize
 
    # Lets send an email
    $EmailBody = $Output.EmailBody
 
    Connect-MgGraph -Scopes 'Mail.Send' -NoWelcome
    Send-EmailMessage -To 'przemyslaw.klys@test.pl' -From 'przemyslaw.klys@test.pl' -MgGraphRequest -Subject "Automated SID Cleanup Report" -Body $EmailBody -Priority Low -Verbose
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [string] $Forest,
        [alias('Domain')][string[]] $IncludeDomains,
        [string[]] $ExcludeDomains,
        [string[]] $IncludeOrganizationalUnit,
        [string[]] $ExcludeOrganizationalUnit,
        [string[]] $IncludeSIDHistoryDomain,
        [string[]] $ExcludeSIDHistoryDomain,
        [nullable[int]] $RemoveLimitSID,
        [nullable[int]] $RemoveLimitObject = 1,
        [ValidateSet('Internal', 'External', 'Unknown')][string[]] $IncludeType = @('Internal', 'External', 'Unknown'),
        [ValidateSet('Internal', 'External', 'Unknown')][string[]] $ExcludeType = @(),
        [string] $ReportPath,
        [string] $DataStorePath,
        [switch] $ReportOnly,
        [string] $LogPath,
        [int] $LogMaximum = 5,
        [switch] $LogShowTime,
        [string] $LogTimeFormat,
        [switch] $Suppress,
        [switch] $ShowHTML,
        [switch] $Online,
        [switch] $DisabledOnly,
        [nullable[int]] $SafetyADLimit,
        [switch] $DontWriteToEventLog
    )

    if (-not $DataStorePath) {
        $DataStorePath = $($MyInvocation.PSScriptRoot) + '\ProcessedSIDHistory.xml'
    }
    if (-not $ReportPath) {
        $ReportPath = $($MyInvocation.PSScriptRoot) + '\ProcessedSIDHistory.html'
    }

    # lets enable global logging
    Set-LoggingCapabilities -LogPath $LogPath -LogMaximum $LogMaximum -ShowTime:$LogShowTime -TimeFormat $LogTimeFormat -ScriptPath $MyInvocation.ScriptName

    $Export = [ordered] @{
        Date             = Get-Date
        Version          = Get-GitHubVersion -Cmdlet 'Invoke-ADComputersCleanup' -RepositoryOwner 'evotecit' -RepositoryName 'CleanupMonster'
        ObjectsToProcess = $null
        CurrentRun       = $null
        History          = [System.Collections.Generic.List[PSCustomObject]]::new()
    }

    Write-Color '[i] ', "[CleanupMonster] ", 'Version', ' [Informative] ', $Export['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta
    Write-Color -Text "[i] ", "Started process of cleaning up SID History for AD Objects" -Color Yellow, White
    Write-Color -Text "[i] ", "Executed by: ", $Env:USERNAME, ' from domain ', $Env:USERDNSDOMAIN -Color Yellow, White, Green, White

    $Export = Import-SIDHistory -DataStorePath $DataStorePath -Export $Export

    # Determine if we're using limits
    if ($Null -eq $RemoveLimitSID -and $null -eq $RemoveLimitObject) {
        $LimitPerObject = $false
        $LimitPerSID = $false
    } elseif ($Null -eq $RemoveLimitSID) {
        $LimitPerObject = $true
        $LimitPerSID = $false
    } elseif ($Null -eq $RemoveLimitObject) {
        $LimitPerObject = $false
        $LimitPerSID = $true
    } else {
        $LimitPerObject = $true
        $LimitPerSID = $true
    }

    $Configuration = [ordered] @{
        Forest                    = $Forest
        IncludeDomains            = $IncludeDomains
        ExcludeDomains            = $ExcludeDomains
        IncludeOrganizationalUnit = $IncludeOrganizationalUnit
        ExcludeOrganizationalUnit = $ExcludeOrganizationalUnit
        IncludeSIDHistoryDomain   = $IncludeSIDHistoryDomain
        ExcludeSIDHistoryDomain   = $ExcludeSIDHistoryDomain
        RemoveLimitSID            = $RemoveLimitSID
        RemoveLimitObject         = $RemoveLimitObject
        IncludeType               = $IncludeType
        ExcludeType               = $ExcludeType
        LimitPerObject            = $LimitPerObject
        LimitPerSID               = $LimitPerSID
        SafetyADLimit             = $SafetyADLimit
        DisabledOnly              = $DisabledOnly
        LogPath                   = $LogPath
        LogMaximum                = $LogMaximum
        LogShowTime               = $LogShowTime
        LogTimeFormat             = $LogTimeFormat
        DontWriteToEventLog       = $DontWriteToEventLog
    }


    # Initialize collections to store objects for processing or reporting
    $ObjectsToProcess = [System.Collections.Generic.List[PSCustomObject]]::new()

    # Get forest details using existing function
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -PreferWritable

    # Get SID history information
    $Output = Get-WinADSIDHistory -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -All

    if ($SafetyADLimit -and $Output.All.Count -le $SafetyADLimit) {
        Write-Color -Text "[i] ", "Number of objects returned with SIDHistory in AD is less than SafetyADLimit ", $SafetyADLimit, ". Stopping processing." -Color Yellow, White, Green, White
        return
    }

    # Extract domain names from the output
    [Array] $DomainNames = foreach ($Key in $Output.Keys) {
        if ($Key -in @('Statistics', 'Trusts', 'DomainSIDs', 'DuplicateSIDs', 'All')) {
            continue
        }
        $Key
    }

    $requestADSIDHistorySplat = @{
        DomainNames             = $DomainNames
        Output                  = $Output
        Export                  = $Export
        IncludeSIDHistoryDomain = $IncludeSIDHistoryDomain
        ExcludeSIDHistoryDomain = $ExcludeSIDHistoryDomain
        IncludeType             = $IncludeType
        ExcludeType             = $ExcludeType
        RemoveLimitObject       = $RemoveLimitObject
        LimitPerObject          = $LimitPerObject
        LimitPerSID             = $LimitPerSID
        ObjectsToProcess        = $ObjectsToProcess
        DisabledOnly            = $DisabledOnly
        ForestInformation       = $ForestInformation
    }

    Request-ADSIDHistory @requestADSIDHistorySplat

    if (-not $ReportOnly) {
        # Process the collected objects for SID removal
        Remove-ADSIDHistory -ObjectsToProcess $ObjectsToProcess -Export $Export -RemoveLimitSID $RemoveLimitSID
    }

    $Export['TotalObjectsFound'] = $ObjectsToProcess.Count
    $Export['TotalSIDsFound'] = ($ObjectsToProcess | ForEach-Object { $_.Object.SIDHistory.Count } | Measure-Object -Sum).Sum

    if (-not $ReportOnly) {
        try {
            $Export | Export-Clixml -LiteralPath $DataStorePath -Encoding Unicode -WhatIf:$false -ErrorAction Stop
        } catch {
            Write-Color -Text "[-] Exporting Processed List failed. Error: $($_.Exception.Message)" -Color Yellow, Red
        }
    }
    $Export['ObjectsToProcess'] = $ObjectsToProcess

    New-HTMLProcessedSIDHistory -Export $Export -FilePath $ReportPath -Output $Output -ForestInformation $ForestInformation -Online:$Online.IsPresent -HideHTML:(-not $ShowHTML.IsPresent) -LogPath $LogPath -Configuration $Configuration

    # Return summary information
    if (-not $Suppress) {
        $Export.EmailBody = New-EmailBodySIDHistory -Export $Export
        $Export
    }
}


# Export functions and aliases as required
Export-ModuleMember -Function @('Invoke-ADComputersCleanup', 'Invoke-ADSIDHistoryCleanup') -Alias @()
# SIG # Begin signature block
# MIItqwYJKoZIhvcNAQcCoIItnDCCLZgCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDySANehsGpfSgj
# +45Kki5w3Z+NdDaDcqDvgr5vtyVeRqCCJq4wggWNMIIEdaADAgECAhAOmxiO+dAt
# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa
# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E
# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy
# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF
# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1
# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB
# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR
# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6
# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB
# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S
# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x
# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB
# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP
# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC
# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc
# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov
# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy
# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW
# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF
# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z
# twGpn1eqXijiuZQwggWQMIIDeKADAgECAhAFmxtXno4hMuI5B72nd3VcMA0GCSqG
# SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvkXUo8MCIw
# aTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdtHauyefLK
# EdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu34LzB4Tm
# dDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0QF+xembu
# d8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2kZhAkHnD
# eMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM1LyuGwN1
# XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmIdph2PVld
# QnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZK37AlLTS
# YW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72gLD76GSm
# M9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqsX40/ybzT
# QRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyhHsXAj6Kx
# fgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD
# VR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzANBgkq
# hkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNkaA9Wz3eucPn9mkqZucl4
# XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjSPMFDQK4dUPVS/JA7u5iZ
# aWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK7VB6fWIhCoDIc2bRoAVg
# X+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eBcg3AFDLvMFkuruBx8lbk
# apdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp5aPNoiBB19GcZNnqJqGL
# FNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msgdDDS4Dk0EIUhFQEI6FUy
# 3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vriRbgjU2wGb2dVf0a1TD9u
# KFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ79ARj6e/CVABRoIoqyc54
# zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5nLGbsQAe79APT0JsyQq8
# 7kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3i0objwG2J5VT6LaJbVu8
# aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0HEEcRrYc9B9F1vM/zZn4w
# ggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqG
# SIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbS
# g9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9
# /UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXn
# HwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0
# VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4f
# sbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40Nj
# gHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0
# QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvv
# mz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T
# /jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk
# 42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5r
# mQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E
# FgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5n
# P+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcG
# CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu
# Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln
# aUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8v
# Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNV
# HSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIB
# AH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxp
# wc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIl
# zpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQ
# cAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfe
# Kuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+j
# Sbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJsh
# IUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6
# OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDw
# N7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR
# 81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2
# VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIGsDCCBJigAwIBAgIQ
# CK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQGEwJVUzEV
# MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t
# MSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjEwNDI5MDAw
# MDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT
# aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zrPYGXcMW7xIUmMJ+k
# jmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHMgQM+TXAkZLON4gh9
# NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8IrgnQnAZaf6mIBJNYc9
# URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyCEUhSaN4QvRRXXegY
# E2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0p6MDDnSlrzm2q2AS
# 4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQakhCBj7A7CdfHmzJa
# wv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0XLyTRSiDNipmKF+w
# c86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960IHnWmZcy740hQ83eR
# Gv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2FKZbS110YU0/EpF2
# 3r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBHX8mBUHOFECMhWWCK
# ZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q27IwyCQLMbDwMVhEC
# AwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFGg34Ou2
# O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9P
# MA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzB3BggrBgEFBQcB
# AQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggr
# BgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1
# c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwHAYDVR0gBBUwEzAH
# BgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIBADojRD2NCHbuj7w6
# mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6jfCbVN7w6XUhtldU/
# SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmImoqKwba9oUgYftzY
# gBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtfJqGVWEjVGv7XJz/9
# kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrxoj7bQ7gzyE84FJKZ
# 9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3LIU/Gs4m6Ri+kAew
# Q3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx4b6cpwoG1iZnt5Lm
# Tl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9Oj9FpsToFpFSi0HA
# SIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+ICw2/O/TOHnuO77Xr
# y7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug0wcCampAMEhLNKhR
# ILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5Vzu0nAPthkX0tGFu
# v2jiJmCG6sivqf6UHedjGzqGVnhOMIIGvDCCBKSgAwIBAgIQC65mvFq6f5WHxvnp
# BOMzBDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5
# NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTI0MDkyNjAwMDAwMFoXDTM1MTEy
# NTIzNTk1OVowQjELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERpZ2lDZXJ0MSAwHgYD
# VQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyNDCCAiIwDQYJKoZIhvcNAQEBBQAD
# ggIPADCCAgoCggIBAL5qc5/2lSGrljC6W23mWaO16P2RHxjEiDtqmeOlwf0KMCBD
# Er4IxHRGd7+L660x5XltSVhhK64zi9CeC9B6lUdXM0s71EOcRe8+CEJp+3R2O8oo
# 76EO7o5tLuslxdr9Qq82aKcpA9O//X6QE+AcaU/byaCagLD/GLoUb35SfWHh43rO
# H3bpLEx7pZ7avVnpUVmPvkxT8c2a2yC0WMp8hMu60tZR0ChaV76Nhnj37DEYTX9R
# eNZ8hIOYe4jl7/r419CvEYVIrH6sN00yx49boUuumF9i2T8UuKGn9966fR5X6kgX
# j3o5WHhHVO+NBikDO0mlUh902wS/Eeh8F/UFaRp1z5SnROHwSJ+QQRZ1fisD8UTV
# DSupWJNstVkiqLq+ISTdEjJKGjVfIcsgA4l9cbk8Smlzddh4EfvFrpVNnes4c16J
# idj5XiPVdsn5n10jxmGpxoMc6iPkoaDhi6JjHd5ibfdp5uzIXp4P0wXkgNs+CO/C
# acBqU0R4k+8h6gYldp4FCMgrXdKWfM4N0u25OEAuEa3JyidxW48jwBqIJqImd93N
# Rxvd1aepSeNeREXAu2xUDEW8aqzFQDYmr9ZONuc2MhTMizchNULpUEoA6Vva7b1X
# CB+1rxvbKmLqfY/M/SdV6mwWTyeVy5Z/JkvMFpnQy5wR14GJcv6dQ4aEKOX5AgMB
# AAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUB
# Af8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1s
# BwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0OBBYEFJ9X
# LAN3DigVkGalY17uT5IfdqBbMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwz
# LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZUaW1l
# U3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUFBzABhhho
# dHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6Ly9jYWNl
# cnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZU
# aW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAD2tHh92mVvjOIQS
# R9lDkfYR25tOCB3RKE/P09x7gUsmXqt40ouRl3lj+8QioVYq3igpwrPvBmZdrlWB
# b0HvqT00nFSXgmUrDKNSQqGTdpjHsPy+LaalTW0qVjvUBhcHzBMutB6HzeledbDC
# zFzUy34VarPnvIWrqVogK0qM8gJhh/+qDEAIdO/KkYesLyTVOoJ4eTq7gj9UFAL1
# UruJKlTnCVaM2UeUUW/8z3fvjxhN6hdT98Vr2FYlCS7Mbb4Hv5swO+aAXxWUm3Wp
# ByXtgVQxiBlTVYzqfLDbe9PpBKDBfk+rabTFDZXoUke7zPgtd7/fvWTlCs30VAGE
# sshJmLbJ6ZbQ/xll/HjO9JbNVekBv2Tgem+mLptR7yIrpaidRJXrI+UzB6vAlk/8
# a1u7cIqV0yef4uaZFORNekUgQHTqddmsPCEIYQP7xGxZBIhdmm4bhYsVA6G2WgNF
# YagLDBzpmk9104WQzYuVNsxyoVLObhx3RugaEGru+SojW4dHPoWrUhftNpFC5H7Q
# EY7MhKRyrBe7ucykW7eaCuWBsBb4HOKRFVDcrZgdwaSIqMDiCLg4D+TPVgKx2EgE
# deoHNHT9l3ZDBD+XgbF+23/zBjeCtxz+dL/9NWR6P2eZRi7zcEO1xwcdcqJsyz/J
# ceENc2Sg8h3KeFUCS7tpFk7CrDqkMIIHXzCCBUegAwIBAgIQB8JSdCgUotar/iTq
# F+XdLjANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT
# aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMB4XDTIzMDQxNjAwMDAwMFoX
# DTI2MDcwNjIzNTk1OVowZzELMAkGA1UEBhMCUEwxEjAQBgNVBAcMCU1pa2/FgsOz
# dzEhMB8GA1UECgwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMSEwHwYDVQQDDBhQ
# cnplbXlzxYJhdyBLxYJ5cyBFVk9URUMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
# ggIKAoICAQCUmgeXMQtIaKaSkKvbAt8GFZJ1ywOH8SwxlTus4McyrWmVOrRBVRQA
# 8ApF9FaeobwmkZxvkxQTFLHKm+8knwomEUslca8CqSOI0YwELv5EwTVEh0C/Daeh
# vxo6tkmNPF9/SP1KC3c0l1vO+M7vdNVGKQIQrhxq7EG0iezBZOAiukNdGVXRYOLn
# 47V3qL5PwG/ou2alJ/vifIDad81qFb+QkUh02Jo24SMjWdKDytdrMXi0235CN4Rr
# W+8gjfRJ+fKKjgMImbuceCsi9Iv1a66bUc9anAemObT4mF5U/yQBgAuAo3+jVB8w
# iUd87kUQO0zJCF8vq2YrVOz8OJmMX8ggIsEEUZ3CZKD0hVc3dm7cWSAw8/FNzGNP
# lAaIxzXX9qeD0EgaCLRkItA3t3eQW+IAXyS/9ZnnpFUoDvQGbK+Q4/bP0ib98XLf
# QpxVGRu0cCV0Ng77DIkRF+IyR1PcwVAq+OzVU3vKeo25v/rntiXCmCxiW4oHYO28
# eSQ/eIAcnii+3uKDNZrI15P7VxDrkUIc6FtiSvOhwc3AzY+vEfivUkFKRqwvSSr4
# fCrrkk7z2Qe72Zwlw2EDRVHyy0fUVGO9QMuh6E3RwnJL96ip0alcmhKABGoIqSW0
# 5nXdCUbkXmhPCTT5naQDuZ1UkAXbZPShKjbPwzdXP2b8I9nQ89VSgQIDAQABo4IC
# AzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYDVR0OBBYE
# FHrxaiVZuDJxxEk15bLoMuFI5233MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK
# BggrBgEFBQcDAzCBtQYDVR0fBIGtMIGqMFOgUaBPhk1odHRwOi8vY3JsMy5kaWdp
# Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEz
# ODQyMDIxQ0ExLmNybDBToFGgT4ZNaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0Rp
# Z2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5j
# cmwwPgYDVR0gBDcwNTAzBgZngQwBBAEwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3
# dy5kaWdpY2VydC5jb20vQ1BTMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcw
# AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8v
# Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmlu
# Z1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEB
# CwUAA4ICAQC3EeHXUPhpe31K2DL43Hfh6qkvBHyR1RlD9lVIklcRCR50ZHzoWs6E
# BlTFyohvkpclVCuRdQW33tS6vtKPOucpDDv4wsA+6zkJYI8fHouW6Tqa1W47YSrc
# 5AOShIcJ9+NpNbKNGih3doSlcio2mUKCX5I/ZrzJBkQpJ0kYha/pUST2CbE3JroJ
# f2vQWGUiI+J3LdiPNHmhO1l+zaQkSxv0cVDETMfQGZKKRVESZ6Fg61b0djvQSx51
# 0MdbxtKMjvS3ZtAytqnQHk1ipP+Rg+M5lFHrSkUlnpGa+f3nuQhxDb7N9E8hUVev
# xALTrFifg8zhslVRH5/Df/CxlMKXC7op30/AyQsOQxHW1uNx3tG1DMgizpwBasrx
# h6wa7iaA+Lp07q1I92eLhrYbtw3xC2vNIGdMdN7nd76yMIjdYnAn7r38wwtaJ3KY
# D0QTl77EB8u/5cCs3ShZdDdyg4K7NoJl8iEHrbqtooAHOMLiJpiL2i9Yn8kQMB6/
# Q6RMO3IUPLuycB9o6DNiwQHf6Jt5oW7P09k5NxxBEmksxwNbmZvNQ65Zn3exUAKq
# G+x31Egz5IZ4U/jPzRalElEIpS0rgrVg8R8pEOhd95mEzp5WERKFyXhe6nB6bSYH
# v8clLAV0iMku308rpfjMiQkqS3LLzfUJ5OHqtKKQNMLxz9z185UCszGCBlMwggZP
# AgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEw
# PwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2
# IFNIQTM4NCAyMDIxIENBMQIQB8JSdCgUotar/iTqF+XdLjANBglghkgBZQMEAgEF
# AKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgor
# BgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3
# DQEJBDEiBCBQB5Gvf5fbYuIo9ofej4y+HU9Y1Va6UyU6wWjl6SOaTTANBgkqhkiG
# 9w0BAQEFAASCAgAcHY48IqWADY0JjhBghFRDUdtNSMT0UZA8pBJ4Ol3/RKBTVdWj
# 5H0Zfjd7WvuuUijMzJSAt5LlbIEW1B2SIbn7OXyKlC+ev9Y860pzZGbbFsHiBZSm
# tJttvXQ/1WmazjFujPxsfW9bcmysbSC+rPsmx+DSSGxNn9vLMuUra4wKzdKct+OP
# o2mMpti9L/r+i7BWxdJVxbCi17NL/NaU5tBFUCY9qXzVdJDiUr02EcysHj0nLokB
# PZHnXC3+03mugQLsNoUUDcTHbArG4p27QqrKjMhi4H2n+If9Q1s952RUj3UQDWbH
# NbZE5f94c8gr39z6r8SBqWTdlzBPfz/9AStPrLfg1QouYl/d7uUHnL9hqlkTFLXx
# 95rnOowK+Xmm7ZBkC49W3m3HxlP0ABDfrrk8NvIoxcyXkwAdNWGfYtvcKNLoyiCA
# 0isYAtXcfkCtOyIw2Ae+TbdzqSV5wdQFnGQ0We1WMt+ao0lCN1SW4ZofLVuBLujD
# dVaXkaCVyShTZ0pjsZajtBdA5gVUjQ5r2764MDh39ip4TNw8ywdBfG3WwhntJgrV
# rrp48+zeNlM71HvlnQW3esH6/2oLsgtca1hMCziztDQENGQncfHXNXgISO7Icbac
# WW6znqbLqecO0ZZFdmexKNuTm3C1LBIXlS4QaBIA+qQq7XUfeQS1H/6TkaGCAyAw
# ggMcBgkqhkiG9w0BCQYxggMNMIIDCQIBATB3MGMxCzAJBgNVBAYTAlVTMRcwFQYD
# VQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBH
# NCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0ECEAuuZrxaun+Vh8b56QTj
# MwQwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG
# CSqGSIb3DQEJBTEPFw0yNTAzMjgxNTA2MzFaMC8GCSqGSIb3DQEJBDEiBCAZjtHj
# xs6q7aGQG2OFW+f+wsWeWvjgMVsYOjcFbASqujANBgkqhkiG9w0BAQEFAASCAgBD
# Pycthdp5fwvPWNPy5j7VtzIa/KkIgGP5xyk9/IG5rEPWjn3XnddrxZiVn5VGMnRf
# CTMM967pAoCx/q8nlJXWOcp+eLkvu/bIwGbZDNhbgoyw+93K+mI3kxbA/AdyAkdB
# QBfCzTyU2+HbyzD2nl+LH4GYSI/TInHNW7pHe5l0eBS0chy5uY/fRUKHkawOWCwB
# KXXZa+HtSkAegCl3vO9jornCtN/PMYXvqgDPRMxCgRvRLTfvJ6Ex9VNGWb5cCAzp
# jboq4vfuwNOx69IPDLKyCHDjRmYfjI7k+r5lBXC//m9LQtHYyFpwP7P8QLy8s0Ua
# nTOdMGL0LkvikXxOvJTUQU2VazN2qNuUzEiSF+zMkml7mVYtHsf8pDg+p4/46lVU
# 6/mR11nIDhhbztb+r6NG1a/6gi1J9m9D3nhZtOX8o/W75ZzXts9bTQMKXZAmNUdO
# XMVscbDeiusJmNDBU++EuX/sfhvAjHUBzecL+tvEkq0DlZplFuIC95O4MZtX2dz6
# jgtIyOcbDxyLwWY8LiL7YFgjAtE2IgFC921VfTbkcDynhqq9FQ7bKrvVMcOcfsIZ
# GiP0mtIeVFgIJT81GYVHofMOX5IEBr72usDwHyFVrf2Ro6fexbr3dW9iOIU1GLpx
# IUfztU4bEZZYJ0S80u6qJXyHcdYt9T9RkOOP8EetxA==
# SIG # End signature block