DelegationModel.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
 
    .EXAMPLE
    $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz'
    ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName -ToOrganizationalUnit
 
    Output:
    OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz
 
    .EXAMPLE
    $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz'
    ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName
 
    Output:
    Przemyslaw Klys
 
    .EXAMPLE
    ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToMultipleOrganizationalUnit -IncludeParent
 
    Output:
    OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz
    OU=Production,DC=ad,DC=evotec,DC=xyz
 
    .EXAMPLE
    ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToMultipleOrganizationalUnit
 
    Output:
    OU=Production,DC=ad,DC=evotec,DC=xyz
 
    .EXAMPLE
    $Con = @(
        'CN=Windows Authorization Access Group,CN=Builtin,DC=ad,DC=evotec,DC=xyz'
        'CN=Mmm,DC=elo,CN=nee,DC=RootDNSServers,CN=MicrosoftDNS,CN=System,DC=ad,DC=evotec,DC=xyz'
        'CN=e6d5fd00-385d-4e65-b02d-9da3493ed850,CN=Operations,CN=DomainUpdates,CN=System,DC=ad,DC=evotec,DC=xyz'
        'OU=Domain Controllers,DC=ad,DC=evotec,DC=pl'
        'OU=Microsoft Exchange Security Groups,DC=ad,DC=evotec,DC=xyz'
    )
 
    ConvertFrom-DistinguishedName -DistinguishedName $Con -ToLastName
 
    Output:
    Windows Authorization Access Group
    Mmm
    e6d5fd00-385d-4e65-b02d-9da3493ed850
    Domain Controllers
    Microsoft Exchange Security Groups
 
    .EXAMPLEE
    ConvertFrom-DistinguishedName -DistinguishedName 'DC=ad,DC=evotec,DC=xyz' -ToCanonicalName
    ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToCanonicalName
    ConvertFrom-DistinguishedName -DistinguishedName 'CN=test,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToCanonicalName
 
    Output:
    ad.evotec.xyz
    ad.evotec.xyz\Production\Users
    ad.evotec.xyz\Production\Users\test
 
    .NOTES
    General notes
    #>

    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param([Parameter(ParameterSetName = 'ToOrganizationalUnit')]
        [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')]
        [Parameter(ParameterSetName = 'ToDC')]
        [Parameter(ParameterSetName = 'ToDomainCN')]
        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'ToLastName')]
        [Parameter(ParameterSetName = 'ToCanonicalName')]
        [alias('Identity', 'DN')][Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)][string[]] $DistinguishedName,
        [Parameter(ParameterSetName = 'ToOrganizationalUnit')][switch] $ToOrganizationalUnit,
        [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')][alias('ToMultipleOU')][switch] $ToMultipleOrganizationalUnit,
        [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')][switch] $IncludeParent,
        [Parameter(ParameterSetName = 'ToDC')][switch] $ToDC,
        [Parameter(ParameterSetName = 'ToDomainCN')][switch] $ToDomainCN,
        [Parameter(ParameterSetName = 'ToLastName')][switch] $ToLastName,
        [Parameter(ParameterSetName = 'ToCanonicalName')][switch] $ToCanonicalName)
    Process {
        foreach ($Distinguished in $DistinguishedName) {
            if ($ToDomainCN) {
                $DN = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1'
                $CN = $DN -replace ',DC=', '.' -replace "DC="
                if ($CN) { $CN }
            } elseif ($ToOrganizationalUnit) {
                $Value = [Regex]::Match($Distinguished, '(?=OU=)(.*\n?)(?<=.)').Value
                if ($Value) { $Value }
            } elseif ($ToMultipleOrganizationalUnit) {
                if ($IncludeParent) { $Distinguished }
                while ($true) {
                    $Distinguished = $Distinguished -replace '^.+?,(?=..=)'
                    if ($Distinguished -match '^DC=') { break }
                    $Distinguished
                }
            } elseif ($ToDC) {
                $Value = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1'
                if ($Value) { $Value }
            } elseif ($ToLastName) {
                $NewDN = $Distinguished -split ",DC="
                if ($NewDN[0].Contains(",OU=")) { [Array] $ChangedDN = $NewDN[0] -split ",OU=" } elseif ($NewDN[0].Contains(",CN=")) { [Array] $ChangedDN = $NewDN[0] -split ",CN=" } else { [Array] $ChangedDN = $NewDN[0] }
                if ($ChangedDN[0].StartsWith('CN=')) { $ChangedDN[0] -replace 'CN=', '' } else { $ChangedDN[0] -replace 'OU=', '' }
            } elseif ($ToCanonicalName) {
                $Domain = $null
                $Rest = $null
                foreach ($O in $Distinguished -split '(?<!\\),') { if ($O -match '^DC=') { $Domain += $O.Substring(3) + '.' } else { $Rest = $O.Substring(3) + '\' + $Rest } }
                if ($Domain -and $Rest) { $Domain.Trim('.') + '\' + ($Rest.TrimEnd('\') -replace '\\,', ',') } elseif ($Domain) { $Domain.Trim('.') } elseif ($Rest) { $Rest.TrimEnd('\') -replace '\\,', ',' }
            } else {
                $Regex = '^CN=(?<cn>.+?)(?<!\\),(?<ou>(?:(?:OU|CN).+?(?<!\\),)+(?<dc>DC.+?))$'
                $Found = $Distinguished -match $Regex
                if ($Found) { $Matches.cn }
            }
        }
    }
}
function 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-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)" }
    }
}
function Get-WinADForestDetails { 
    [CmdletBinding()]
    param([alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers', 'ComputerName')][string[]] $IncludeDomainControllers,
        [switch] $SkipRODC,
        [string] $Filter = '*',
        [switch] $TestAvailability,
        [ValidateSet('All', 'Ping', 'WinRM', 'PortOpen', 'Ping+WinRM', 'Ping+PortOpen', 'WinRM+PortOpen')] $Test = 'All',
        [int[]] $Ports = 135,
        [int] $PortsTimeout = 100,
        [int] $PingCount = 1,
        [switch] $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
                $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 } } }
                    $Server = [ordered] @{Domain = $Domain
                        HostName                 = $S.HostName
                        Name                     = $S.Name
                        Forest                   = $ForestInformation.RootDomain
                        Site                     = $S.Site
                        IPV4Address              = $S.IPV4Address
                        IPV6Address              = $S.IPV6Address
                        IsGlobalCatalog          = $S.IsGlobalCatalog
                        IsReadOnly               = $S.IsReadOnly
                        IsSchemaMaster           = ($S.OperationMasterRoles -contains 'SchemaMaster')
                        IsDomainNamingMaster     = ($S.OperationMasterRoles -contains 'DomainNamingMaster')
                        IsPDC                    = ($S.OperationMasterRoles -contains 'PDCEmulator')
                        IsRIDMaster              = ($S.OperationMasterRoles -contains 'RIDMaster')
                        IsInfrastructureMaster   = ($S.OperationMasterRoles -contains 'InfrastructureMaster')
                        OperatingSystem          = $S.OperatingSystem
                        OperatingSystemVersion   = $S.OperatingSystemVersion
                        OperatingSystemLong      = ConvertTo-OperatingSystem -OperatingSystem $S.OperatingSystem -OperatingSystemVersion $S.OperatingSystemVersion
                        LdapPort                 = $S.LdapPort
                        SslPort                  = $S.SslPort
                        DistinguishedName        = $S.ComputerObjectDN
                        Pingable                 = $null
                        WinRM                    = $null
                        PortOpen                 = $null
                        Comment                  = ''
                    }
                    if ($TestAvailability) {
                        if ($Test -eq 'All' -or $Test -like 'Ping*') { $Server.Pingable = Test-Connection -ComputerName $Server.IPV4Address -Quiet -Count $PingCount }
                        if ($Test -eq 'All' -or $Test -like '*WinRM*') { $Server.WinRM = (Test-WinRM -ComputerName $Server.HostName).Status }
                        if ($Test -eq 'All' -or '*PortOpen*') { $Server.PortOpen = (Test-ComputerPort -Server $Server.HostName -PortTCP $Ports -Timeout $PortsTimeout).Status }
                    }
                    [PSCustomObject] $Server
                }
            } catch {
                [PSCustomObject]@{Domain     = $Domain
                    HostName                 = ''
                    Name                     = ''
                    Forest                   = $ForestInformation.RootDomain
                    IPV4Address              = ''
                    IPV6Address              = ''
                    IsGlobalCatalog          = ''
                    IsReadOnly               = ''
                    Site                     = ''
                    SchemaMaster             = $false
                    DomainNamingMasterMaster = $false
                    PDCEmulator              = $false
                    RIDMaster                = $false
                    InfrastructureMaster     = $false
                    LdapPort                 = ''
                    SslPort                  = ''
                    DistinguishedName        = ''
                    Pingable                 = $null
                    WinRM                    = $null
                    PortOpen                 = $null
                    Comment                  = $_.Exception.Message -replace "`n", " " -replace "`r", " "
                }
            }
            if ($SkipRODC) { [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC | Where-Object { $_.IsReadOnly -eq $false } } else { [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC }
            [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 Remove-EmptyValue {  
    [alias('Remove-EmptyValues')]
    [CmdletBinding()]
    param([alias('Splat', 'IDictionary')][Parameter(Mandatory)][System.Collections.IDictionary] $Hashtable,
        [string[]] $ExcludeParameter,
        [switch] $Recursive,
        [int] $Rerun,
        [switch] $DoNotRemoveNull,
        [switch] $DoNotRemoveEmpty,
        [switch] $DoNotRemoveEmptyArray,
        [switch] $DoNotRemoveEmptyDictionary)
    foreach ($Key in [string[]] $Hashtable.Keys) { if ($Key -notin $ExcludeParameter) { if ($Recursive) { if ($Hashtable[$Key] -is [System.Collections.IDictionary]) { if ($Hashtable[$Key].Count -eq 0) { if (-not $DoNotRemoveEmptyDictionary) { $Hashtable.Remove($Key) } } else { Remove-EmptyValue -Hashtable $Hashtable[$Key] -Recursive:$Recursive } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } }
    if ($Rerun) { for ($i = 0; $i -lt $Rerun; $i++) { Remove-EmptyValue -Hashtable $Hashtable -Recursive:$Recursive } }
}
function Write-Color { 
    <#
    .SYNOPSIS
        Write-Color is a wrapper around Write-Host.
 
        It provides:
        - Easy manipulation of colors,
        - Logging output to file (log)
        - Nice formatting options out of the box.
 
    .DESCRIPTION
        Author: przemyslaw.klys at evotec.pl
        Project website: https://evotec.xyz/hub/scripts/write-color-ps1/
        Project support: https://github.com/EvotecIT/PSWriteColor
 
        Original idea: Josh (https://stackoverflow.com/users/81769/josh)
 
    .EXAMPLE
    Write-Color -Text "Red ", "Green ", "Yellow " -Color Red,Green,Yellow
 
    .EXAMPLE
    Write-Color -Text "This is text in Green ",
                    "followed by red ",
                    "and then we have Magenta... ",
                    "isn't it fun? ",
                    "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan
 
    .EXAMPLE
    Write-Color -Text "This is text in Green ",
                    "followed by red ",
                    "and then we have Magenta... ",
                    "isn't it fun? ",
                    "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan -StartTab 3 -LinesBefore 1 -LinesAfter 1
 
    .EXAMPLE
    Write-Color "1. ", "Option 1" -Color Yellow, Green
    Write-Color "2. ", "Option 2" -Color Yellow, Green
    Write-Color "3. ", "Option 3" -Color Yellow, Green
    Write-Color "4. ", "Option 4" -Color Yellow, Green
    Write-Color "9. ", "Press 9 to exit" -Color Yellow, Gray -LinesBefore 1
 
    .EXAMPLE
    Write-Color -LinesBefore 2 -Text "This little ","message is ", "written to log ", "file as well." `
                -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" -TimeFormat "yyyy-MM-dd HH:mm:ss"
    Write-Color -Text "This can get ","handy if ", "want to display things, and log actions to file ", "at the same time." `
                -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt"
 
    .EXAMPLE
    # Added in 0.5
    Write-Color -T "My text", " is ", "all colorful" -C Yellow, Red, Green -B Green, Green, Yellow
    wc -t "my text" -c yellow -b green
    wc -text "my text" -c red
 
    .NOTES
        Additional Notes:
        - TimeFormat https://msdn.microsoft.com/en-us/library/8kb3ddd4.aspx
    #>

    [alias('Write-Colour')]
    [CmdletBinding()]
    param ([alias ('T')] [String[]]$Text,
        [alias ('C', 'ForegroundColor', 'FGC')] [ConsoleColor[]]$Color = [ConsoleColor]::White,
        [alias ('B', 'BGC')] [ConsoleColor[]]$BackGroundColor = $null,
        [alias ('Indent')][int] $StartTab = 0,
        [int] $LinesBefore = 0,
        [int] $LinesAfter = 0,
        [int] $StartSpaces = 0,
        [alias ('L')] [string] $LogFile = '',
        [Alias('DateFormat', 'TimeFormat')][string] $DateTimeFormat = 'yyyy-MM-dd HH:mm:ss',
        [alias ('LogTimeStamp')][bool] $LogTime = $true,
        [int] $LogRetry = 2,
        [ValidateSet('unknown', 'string', 'unicode', 'bigendianunicode', 'utf8', 'utf7', 'utf32', 'ascii', 'default', 'oem')][string]$Encoding = 'Unicode',
        [switch] $ShowTime,
        [switch] $NoNewLine)
    $DefaultColor = $Color[0]
    if ($null -ne $BackGroundColor -and $BackGroundColor.Count -ne $Color.Count) {
        Write-Error "Colors, BackGroundColors parameters count doesn't match. Terminated."
        return
    }
    if ($LinesBefore -ne 0) { for ($i = 0; $i -lt $LinesBefore; $i++) { Write-Host -Object "`n" -NoNewline } }
    if ($StartTab -ne 0) { for ($i = 0; $i -lt $StartTab; $i++) { Write-Host -Object "`t" -NoNewline } }
    if ($StartSpaces -ne 0) { for ($i = 0; $i -lt $StartSpaces; $i++) { Write-Host -Object ' ' -NoNewline } }
    if ($ShowTime) { Write-Host -Object "[$([datetime]::Now.ToString($DateTimeFormat))] " -NoNewline }
    if ($Text.Count -ne 0) {
        if ($Color.Count -ge $Text.Count) { if ($null -eq $BackGroundColor) { for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline } } else { for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline } } } else {
            if ($null -eq $BackGroundColor) {
                for ($i = 0; $i -lt $Color.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline }
                for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -NoNewline }
            } else {
                for ($i = 0; $i -lt $Color.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline }
                for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -BackgroundColor $BackGroundColor[0] -NoNewline }
            }
        }
    }
    if ($NoNewLine -eq $true) { Write-Host -NoNewline } else { Write-Host }
    if ($LinesAfter -ne 0) { for ($i = 0; $i -lt $LinesAfter; $i++) { Write-Host -Object "`n" -NoNewline } }
    if ($Text.Count -and $LogFile) {
        $TextToFile = ""
        for ($i = 0; $i -lt $Text.Length; $i++) { $TextToFile += $Text[$i] }
        $Saved = $false
        $Retry = 0
        Do {
            $Retry++
            try {
                if ($LogTime) { "[$([datetime]::Now.ToString($DateTimeFormat))] $TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false } else { "$TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false }
                $Saved = $true
            } catch { if ($Saved -eq $false -and $Retry -eq $LogRetry) { $PSCmdlet.WriteError($_) } else { Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Retrying... ($Retry/$LogRetry)" } }
        } Until ($Saved -eq $true -or $Retry -ge $LogRetry)
    }
}
function 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 (22000)' = 'Windows 11 21H2'
            '10.0 (19043)'          = 'Windows 10 21H1'
            '10.0 (19042)'          = 'Windows 10 20H2'
            '10.0 (19041)'          = 'Windows 10 2004'
            '10.0 (18898)'          = 'Windows 10 Insider Preview'
            '10.0 (18363)'          = "Windows 10 1909"
            '10.0 (18362)'          = "Windows 10 1903"
            '10.0 (17763)'          = "Windows 10 1809"
            '10.0 (17134)'          = "Windows 10 1803"
            '10.0 (16299)'          = "Windows 10 1709"
            '10.0 (15063)'          = "Windows 10 1703"
            '10.0 (14393)'          = "Windows 10 1607"
            '10.0 (10586)'          = "Windows 10 1511"
            '10.0 (10240)'          = "Windows 10 1507"
            '10.0.22000'            = 'Windows 11 21H2'
            '10.0.19043'            = 'Windows 10 21H1'
            '10.0.19042'            = 'Windows 10 20H2'
            '10.0.19041'            = 'Windows 10 2004'
            '10.0.18898'            = 'Windows 10 Insider Preview'
            '10.0.18363'            = "Windows 10 1909"
            '10.0.18362'            = "Windows 10 1903"
            '10.0.17763'            = "Windows 10 1809"
            '10.0.17134'            = "Windows 10 1803"
            '10.0.16299'            = "Windows 10 1709"
            '10.0.15063'            = "Windows 10 1703"
            '10.0.14393'            = "Windows 10 1607"
            '10.0.10586'            = "Windows 10 1511"
            '10.0.10240'            = "Windows 10 1507"
            '22000'                 = 'Windows 11 21H2'
            '19043'                 = 'Windows 10 21H1'
            '19042'                 = 'Windows 10 20H2'
            '19041'                 = 'Windows 10 2004'
            '18898'                 = 'Windows 10 Insider Preview'
            '18363'                 = "Windows 10 1909"
            '18362'                 = "Windows 10 1903"
            '17763'                 = "Windows 10 1809"
            '17134'                 = "Windows 10 1803"
            '16299'                 = "Windows 10 1709"
            '15063'                 = "Windows 10 1703"
            '14393'                 = "Windows 10 1607"
            '10586'                 = "Windows 10 1511"
            '10240'                 = "Windows 10 1507"
        }
        $System = $Systems[$OperatingSystemVersion]
        if (-not $System) { $System = $OperatingSystem }
    } elseif ($OperatingSystem -like 'Windows Server*') {
        $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 = $OperatingSystem }
    } else { $System = $OperatingSystem }
    if ($System) { $System } else { 'Unknown' }
}
function Copy-DictionaryManual { 
    [CmdletBinding()]
    param([System.Collections.IDictionary] $Dictionary)
    $clone = @{}
    foreach ($Key in $Dictionary.Keys) {
        $value = $Dictionary.$Key
        $clonedValue = switch ($Dictionary.$Key) {
            { $null -eq $_ } {
                $null
                continue
            }
            { $_ -is [System.Collections.IDictionary] } {
                Copy-DictionaryManual -Dictionary $_
                continue
            }
            { $type = $_.GetType()
                $type.IsPrimitive -or $type.IsValueType -or $_ -is [string] } {
                $_
                continue
            }
            default { $_ | Select-Object -Property * }
        }
        if ($value -is [System.Collections.IList]) { $clone[$Key] = @($clonedValue) } else { $clone[$Key] = $clonedValue }
    }
    $clone
}
function Get-GitHubLatestRelease { 
    <#
    .SYNOPSIS
    Gets one or more releases from GitHub repository
 
    .DESCRIPTION
    Gets one or more releases from GitHub repository
 
    .PARAMETER Url
    Url to github repository
 
    .EXAMPLE
    Get-GitHubLatestRelease -Url "https://api.github.com1/repos/evotecit/Testimo/releases" | Format-Table
 
    .NOTES
    General notes
    #>

    [CmdLetBinding()]
    param([parameter(Mandatory)][alias('ReleasesUrl')][uri] $Url)
    $ProgressPreference = 'SilentlyContinue'
    $Responds = Test-Connection -ComputerName $URl.Host -Quiet -Count 1
    if ($Responds) {
        Try {
            [Array] $JsonOutput = (Invoke-WebRequest -Uri $Url -ErrorAction Stop | ConvertFrom-Json)
            foreach ($JsonContent in $JsonOutput) {
                [PSCustomObject] @{PublishDate = [DateTime] $JsonContent.published_at
                    CreatedDate                = [DateTime] $JsonContent.created_at
                    PreRelease                 = [bool] $JsonContent.prerelease
                    Version                    = [version] ($JsonContent.name -replace 'v', '')
                    Tag                        = $JsonContent.tag_name
                    Branch                     = $JsonContent.target_commitish
                    Errors                     = ''
                }
            }
        } catch {
            [PSCustomObject] @{PublishDate = $null
                CreatedDate                = $null
                PreRelease                 = $null
                Version                    = $null
                Tag                        = $null
                Branch                     = $null
                Errors                     = $_.Exception.Message
            }
        }
    } else {
        [PSCustomObject] @{PublishDate = $null
            CreatedDate                = $null
            PreRelease                 = $null
            Version                    = $null
            Tag                        = $null
            Branch                     = $null
            Errors                     = "No connection (ping) to $($Url.Host)"
        }
    }
    $ProgressPreference = 'Continue'
}
function Test-ComputerPort { 
    [CmdletBinding()]
    param ([alias('Server')][string[]] $ComputerName,
        [int[]] $PortTCP,
        [int[]] $PortUDP,
        [int]$Timeout = 5000)
    begin {
        if ($Global:ProgressPreference -ne 'SilentlyContinue') {
            $TemporaryProgress = $Global:ProgressPreference
            $Global:ProgressPreference = 'SilentlyContinue'
        }
    }
    process {
        foreach ($Computer in $ComputerName) {
            foreach ($P in $PortTCP) {
                $Output = [ordered] @{'ComputerName' = $Computer
                    'Port'                           = $P
                    'Protocol'                       = 'TCP'
                    'Status'                         = $null
                    'Summary'                        = $null
                    'Response'                       = $null
                }
                $TcpClient = 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 { 
    [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 Add-GroupMembersOf {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [string] $Identity,
        [string] $Group,
        [string] $DC,
        [ValidateSet('Add', 'Remove', 'Skip')][string[]] $LogOption
    )
    $CacheMembers = [ordered] @{}
    try {
        $MemberExists = Get-ADGroupMember -Identity $Identity -Server $DC -ErrorAction Stop
    } catch {
        Write-Color -Text '[!] ', "Member ", $Group, " addition to $Identity failed. Error: ", $_.Exception.Message -Color Red, Yellow, Red, Yellow
        return
    }
    foreach ($Member in $MemberExists) {
        $CacheMembers[$Member.SamAccountName] = $Member
        $CacheMembers[$Member.DistinguishedName] = $Member
        $CacheMembers[$Member.SID.Value] = $Member
    }
    try {
        if ($CacheMembers[$Group]) {
            if ($LogOption -contains 'Skip') {
                Write-Color -Text '[s] ', "Member ", $Group, " already exists in ", $Identity -Color Magenta, Yellow, Magenta, Yellow
            }
            continue
        }
        Add-ADGroupMember -Identity $Identity -Members $Group -ErrorAction Stop -Server $DC
        if ($LogOption -contains 'Add') {
            Write-Color -Text '[+] ', "Member ", $Group, " added to $Identity" -Color Green, White, Green, White
        }
    } catch {
        Write-Color -Text '[!] ', "Member ", $Group, " addition to $Identity failed. Error: ", $_.Exception.Message -Color Red, Yellow, Red, Yellow
    }
}
function Convert-DelegationGroups {
    <#
    .SYNOPSIS
    Internal function that converts the groups to a hashtable and makes sure all values are set as expected
 
    .DESCRIPTION
    Internal function that converts the groups to a hashtable and makes sure all values are set as expected
 
    .PARAMETER GroupInformation
    Converts the groups to a hashtable that are created by New-DelegationGroup
 
    .PARAMETER Groups
    Fixes the groups that are created by hashtable approach
 
    .PARAMETER Destination
    The destination OU where the groups will be created. This is used when user doesn't provide Path for given group
 
    .PARAMETER MembersBehaviour
    The behaviour for members. This is used when user doesn't provide MembersBehaviour for given group
 
    .EXAMPLE
    $Groups = Convert-DelegationGroups -GroupInformation $DelegationOutput -Destination $Destination -MembersBehaviour $MembersBehaviour
 
    .EXAMPLE
    $Groups = Convert-DelegationGroups -Groups $Groups -Destination $Destination -MembersBehaviour $MembersBehaviour
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [System.Collections.IDictionary[]] $GroupInformation,
        [System.Collections.IDictionary] $Groups,
        [string] $Destination,
        [string[]][ValidateSet('Add', 'Remove')] $MembersBehaviour,
        [bool] $ProtectedFromAccidentalDeletion

    )
    $Count = 0
    if ($GroupInformation) {
        $GroupsInfo = [ordered] @{}
        foreach ($Group in $GroupInformation) {
            $Count++
            $GroupNameToUse = $Group.Name + $Count

            if (-not $Group.Name) {
                $DefaultGroupName = $GroupName
            } else {
                $DefaultGroupName = $Group.Name
            }
            if (-not $Group.DisplayName) {
                $DefaultGroupDisplayName = $DefaultGroupName
            } else {
                $DefaultGroupDisplayName = $Group.DisplayName
            }

            $GroupsInfo[$GroupNameToUse] = [ordered] @{
                Name                            = $DefaultGroupName
                DisplayName                     = $DefaultGroupDisplayName
                Path                            = if ($Group.Path) { $Group.Path } else { $Destination }
                Description                     = $Group.Description
                GroupScope                      = $Group.GroupScope
                GroupCategory                   = $Group.GroupCategory
                ProtectedFromAccidentalDeletion = if ($null -eq $Group.ProtectedFromAccidentalDeletion) { $ProtectedFromAccidentalDeletion } else { $Group.ProtectedFromAccidentalDeletion }
                MembersBehaviour                = if ($Group.MembersBehaviour) { $Group.MembersBehaviour } else { $MembersBehaviour }
                Members                         = if ($Group.Members) { $Group.Members } else { $null }
                MemberOf                        = if ($Group.MemberOf) { $Group.MemberOf } else { $null }
            }
            Remove-EmptyValue -Hashtable $GroupsInfo[$GroupNameToUse]
        }
        $GroupsInfo
    } else {
        $GroupsInfo = [ordered] @{}
        foreach ($GroupName in [string[]] $Groups.Keys) {
            $Count++
            $Group = $Groups[$GroupName]
            $GroupNameToUse = $GroupName + $Count

            if (-not $Group.Name) {
                $DefaultGroupName = $GroupName
            } else {
                $DefaultGroupName = $Group.Name
            }
            if (-not $Group.DisplayName) {
                $DefaultGroupDisplayName = $DefaultGroupName
            } else {
                $DefaultGroupDisplayName = $Group.DisplayName
            }
            $GroupsInfo[$GroupNameTouse] = [ordered] @{
                Name                            = $DefaultGroupName
                DisplayName                     = $DefaultGroupDisplayName
                Path                            = if ($Group.Path) { $Group.Path } else { $Destination }
                Description                     = $Group.Description
                GroupScope                      = $Group.GroupScope
                GroupCategory                   = $Group.GroupCategory
                ProtectedFromAccidentalDeletion = if ($null -eq $Group.ProtectedFromAccidentalDeletion) { $ProtectedFromAccidentalDeletion } else { $Group.ProtectedFromAccidentalDeletion }
                MembersBehaviour                = if ($Group.MembersBehaviour) { $Group.MembersBehaviour } else { $MembersBehaviour }
                Members                         = if ($Group.Members) { $Group.Members } else { $null }
                MemberOf                        = if ($Group.MemberOf) { $Group.MemberOf } else { $null }
            }
            Remove-EmptyValue -Hashtable $GroupsInfo[$GroupNameToUse]
        }
        $GroupsInfo
    }
}
function Convert-DelegationModel {
    [cmdletBinding()]
    param(
        [System.Collections.IDictionary[]] $DelegationInput,
        [System.Collections.IDictionary] $Definition,
        [string] $Destination,
        [bool] $ProtectedFromAccidentalDeletion
    )
    if ($DelegationInput) {
        $Output = [ordered] @{}
        foreach ($Delegation in $DelegationInput) {
            # $CanonicalNameOU = $Delegation.CanonicalNameOU
            # $ConfigurationOU = $Delegation.ConfigurationOU
            # $CanonicalNameOU = "$Destination\$CanonicalNameOU"
            $Output[$Delegation.CanonicalNameOU] = [ordered] @{}

            $Output[$Delegation.CanonicalNameOU].CanonicalNameOU = $Delegation.CanonicalNameOU
            $Output[$Delegation.CanonicalNameOU].Delegation = $Delegation.Delegation
            $Output[$Delegation.CanonicalNameOU].Description = $Delegation.Description
            $Output[$Delegation.CanonicalNameOU].DelegationInheritance = $Delegation.DelegationInheritance
            $Output[$Delegation.CanonicalNameOU].ProtectedFromAccidentalDeletion = if ($null -eq $Delegation.ProtectedFromAccidentalDeletion) {
                $ProtectedFromAccidentalDeletion
            } else {
                $Delegation.ProtectedFromAccidentalDeletion
            }
        }
        $Output
    } else {
        foreach ($CanonicalNameOU in $Definition.Keys) {
            $ConfigurationOU = $Definition[$CanonicalNameOU]
            # $CanonicalNameOU = "$Destination\$CanonicalNameOU"
            # $Output[$CanonicalNameOU] = $ConfigurationOU
            # $Output[$CanonicalNameOU].Delegation = $true
            $Definition[$CanonicalNameOU].ProtectedFromAccidentalDeletion = if ($null -eq $ConfigurationOU.ProtectedFromAccidentalDeletion) {
                $ProtectedFromAccidentalDeletion
            } else {
                $ConfigurationOU.ProtectedFromAccidentalDeletion
            }
        }
        $Definition
    }
}
function Export-DelegationLogs {
    [CmdletBinding()]
    param(
        $CanonicalNameOU,
        $OutputFromDelegation,
        [ValidateSet('Add', 'Remove', 'Skip')][string[]] $LogOption
    )
    foreach ($Type in @('Skip', 'Add', 'Remove', 'Warnings', 'Errors')) {
        foreach ($D in $OutputFromDelegation.$Type) {
            if ($Type -eq 'Skip') {
                if ($LogOption -notcontains 'Skip') {
                    continue
                }
                $Action = 'Skipping'
                $ActionSign = '[s]'
                $ActionColor = [System.ConsoleColor]::Magenta
            } elseif ($Type -eq 'Add') {
                if ($LogOption -notcontains 'Add') {
                    continue
                }
                $Action = 'Adding'
                $ActionSign = '[+]'
                $ActionColor = [System.ConsoleColor]::Green
            } elseif ($Type -eq 'Remove') {
                if ($LogOption -notcontains 'Remove') {
                    continue
                }
                $Action = 'Removing'
                $ActionSign = '[-]'
                $ActionColor = [System.ConsoleColor]::DarkRed
            } elseif ($Type -eq 'Warnings') {
                $Action = 'Warning'
                $ActionSign = '[!]'
                $ActionColor = [System.ConsoleColor]::Magenta
                Write-Color -Text $ActionSign, "[$($CanonicalNameOU)]", "[$Action] ", $D -Color $ActionColor, DarkGray, $ActionColor, White
                continue
            } elseif ($Type -eq 'Errors') {
                $Action = 'Error'
                $ActionSign = '[!]'
                $ActionColor = [System.ConsoleColor]::Red
                Write-Color -Text $ActionSign, "[$($CanonicalNameOU)]", "[$Action] ", $D -Color $ActionColor, DarkGray, $ActionColor, White
                continue
            }
            $OptionColor = [System.ConsoleColor]::DarkGray
            $ValueColor = [System.ConsoleColor]::Magenta
            $BracketColor = [System.ConsoleColor]::DarkGray
            if ($D.Permissions.AccessControlType -eq 'Allow') {
                $ColorAccessControlType = [System.ConsoleColor]::Green
            } else {
                $ColorAccessControlType = [System.ConsoleColor]::Red
            }
            $PrincipalColor = [System.ConsoleColor]::Magenta
            Write-Color -Text @(
                $ActionSign,
                "[$($CanonicalNameOU)]",
                "[$Action]",
                "[Principal: ", $($D.Principal), "]",
                "[AccessControlType: ", $($D.Permissions.AccessControlType), "]",
                "[ActiveDirectoryRights: ", $($D.Permissions.ActiveDirectoryRights), "]",
                "[ObjectTypeName: ", $($D.Permissions.ObjectTypeName), "]",
                "[InheritedObjectTypeName: ", $($D.Permissions.InheritedObjectTypeName), "]",
                "[InheritanceType: ", $($D.Permissions.InheritanceType), "]"
            ) -Color $ActionColor, DarkGray, $ActionColor, $OptionColor, $PrincipalColor, $BracketColor, $OptionColor, $ColorAccessControlType, $OptionColor, $BracketColor, $ValueColor, $BracketColor, $OptionColor, $ValueColor, $BracketColor, $OptionColor, $ValueColor, $BracketColor, $OptionColor, $ValueColor, $BracketColor, $OptionColor, $ValueColor, $BracketColor
        }
    }
}
function Find-GroupMembersActions {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(Mandatory)][string] $Identity,
        [parameter()][Array] $ExpectedMembers,
        [parameter(Mandatory)][string] $DC,
        [parameter(Mandatory)][string[]] $MembersBehaviour,
        [ValidateSet('Add', 'Remove', 'Skip')][string[]] $LogOption
    )
    $CacheMembers = [ordered] @{}
    $MemberExists = Get-ADGroupMember -Identity $Identity -Server $DC
    foreach ($Member in $MemberExists) {
        $CacheMembers[$Member.SamAccountName] = $Member
        $CacheMembers[$Member.DistinguishedName] = $Member
        $CacheMembers[$Member.SID.Value] = $Member
    }

    $MemberToAdd = foreach ($Member in $ExpectedMembers) {
        if ($CacheMembers[$Member]) {
            #Write-Color -Text '[-] ', "Member ", $Member, " already exists in ", $Identity -Color Red, Yellow, Red, Yellow
            continue
        } else {
            #Write-Color -Text '[+] ', "Member ", $Member, " will be added to ", $Identity -Color Green, Yellow, Green, Yellow
            $Member
        }
    }
    $MemberToRemove = foreach ($Member in $MemberExists) {
        if ($Member.SamAccountName -notin $ExpectedMembers -and $Member.distinguishedName -notin $ExpectedMembers -and $Member.SID.Value -notin $ExpectedMembers) {
            #Write-Color -Text '[-] ', "Member ", $Member, " will be removed from ", $Identity -Color Red, Yellow, Red, Yellow
            $Member
        } else {
            #Write-Color -Text '[+] ', "Member ", $Member, " already exists in ", $Identity -Color Green, Yellow, Green, Yellow
            continue
        }
    }

    if ($MembersBehaviour -contains 'Remove') {
        foreach ($Member in $MemberToRemove) {
            try {
                Remove-ADGroupMember -Identity $Identity -Members $Member -ErrorAction Stop -Confirm:$false -Server $DC
                if ($LogOption -contains 'Remove') {
                    Write-Color -Text '[+] ', "Member ", $Member, " removed from $Identity" -Color Green, White, Green, White
                }
            } catch {
                Write-Color -Text '[!] ', "Member ", $Member, " removal from $Identity failed. Error: ", $_.Exception.Message -Color Red, Yellow, Red, Yellow
            }
        }
    }
    if ($MembersBehaviour -contains 'Add') {
        foreach ($Member in $MemberToAdd) {
            try {
                Add-ADGroupMember -Identity $Identity -Members $Member -ErrorAction Stop -Server $DC
                if ($LogOption -contains 'Add') {
                    Write-Color -Text '[+] ', "Member ", $Member, " added to $Identity" -Color Green, White, Green, White
                }
            } catch {
                Write-Color -Text '[!] ', "Member ", $Member, " addition to $Identity failed. Error: ", $_.Exception.Message -Color Red, Yellow, Red, Yellow
            }
        }
    }
}
function Initialize-DelegationModel {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $Domain
    )
    $Script:Reporting = [ordered] @{}
    $Script:Reporting['Version'] = Get-GitHubVersion -Cmdlet 'Start-DelegationModel' -RepositoryOwner 'evotecit' -RepositoryName 'DelegationModel'
    $Script:Reporting['Settings'] = @{
        ShowError   = $ShowError.IsPresent
        ShowWarning = $ShowWarning.IsPresent
        HideSteps   = $HideSteps.IsPresent
    }

    if ($LogFile) {
        $FolderPath = [io.path]::GetDirectoryName($LogFile)
        if (-not (Test-Path -LiteralPath $FolderPath)) {
            $null = New-Item -Path $FolderPath -ItemType Directory -Force -WhatIf:$false
        }
        $PSDefaultParameterValues = @{
            "Write-Color:LogFile" = $LogFile
        }
        Write-Color '[i]', "[DelegationModel] ", 'Version ', $Script:Reporting['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta
        $CurrentLogs = Get-ChildItem -LiteralPath $FolderPath | Sort-Object -Property CreationTime -Descending | Select-Object -Skip $LogMaximum
        if ($CurrentLogs) {
            Write-Color -Text '[i]', "[DelegationModel] ", "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 '[i]', "[DelegationModel] ", '[log deleted] ', "Deleted ", "$($Log.FullName)" -Color Yellow, White, Green
                } catch {
                    Write-Color -Text '[i]', "[DelegationModel] ", '[log error] ', "Couldn't delete log file $($Log.FullName). Error: ', "$($_.Exception.Message) -Color Yellow, White, Red
                }
            }
        }
    } else {
        Write-Color '[i]', "[DelegationModel] ", 'Version ', $Script:Reporting['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta
    }

    Write-Color '[i]', "[DelegationModel] ", 'Getting forest information' -Color Yellow, DarkGray, Yellow
    $ForestInformation = Get-WinADForestDetails
    if (-not $ForestInformation) {
        Write-Color -Text '[-] ', "Forest information could not be retrieved. Please check your connection to the domain controller." -Color Red, White
        return
    }

    $DC = $ForestInformation['QueryServers'][$Domain]
    $DC = $DC.HostName[0]
    if (-not $DC) {
        Write-Color -Text '[!] ', "Given domain $Domain can't be found in the forest. Please make sure to provide proper value." -Color Red, White
        return
    }
    $DC
}
function New-DelegationModel {
    [cmdletBinding()]
    param(
        [string] $CanonicalNameOU,
        [System.Collections.IDictionary] $ConfigurationOU,
        [string] $BasePath,
        [string] $Domain
    )
    $CanonicalNameOU = $CanonicalNameOU.Replace("\", "/")
    $DNOU = ConvertTo-DistinguishedName -CanonicalName "$Domain/$($CanonicalNameOU)" -ToOU
    if ($null -ne $ConfigurationOU.DelegationInheritance) {
        Set-ADACLInheritance -ADObject $DNOU -Inheritance $ConfigurationOU.DelegationInheritance -WarningAction SilentlyContinue -WarningVariable warnings #-ErrorVariable errors -ErrorAction SilentlyContinue
        foreach ($W in $Warnings) {
            Write-Color -Text "[!]", "[$CanonicalNameOU]", "[Warning]", " ACL Inheritance: $($W)" -Color Magenta, DarkGray, Magenta, White
        }
        # foreach ($E in $Errors) {
        # Write-Color -Text "[!]", "[$CanonicalNameOU]", "[Error]", " ACL Inheritance: $($E.Exception.Message)" -Color Red, DarkGray, Red, White
        # }
    }
    Set-ADACL -ADObject $DNOU -ACLSettings $ConfigurationOU.Delegation -Inheritance $ConfigurationOU.DelegationInheritance -WarningAction SilentlyContinue
}
function New-OUStructure {
    [cmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $CanonicalNameOU,
        [Parameter(Mandatory)][System.Collections.IDictionary] $ConfigurationOU,
        [Parameter(Mandatory)][string] $BasePath,
        [Parameter(Mandatory)][string] $DC #,
        #[bool] $ProtectedFromAccidentalDeletion
    )
    $IgnoredProperties = @('Delegation', 'DelegationInheritance', 'CanonicalNameOU')
    $OUProperties = @('Description', 'ProtectedFromAccidentalDeletion')
    $PartsOU = $CanonicalNameOU.Split("\")

    $LevelPath = $BasePath
    for ($i = 0; $i -lt $PartsOU.Length; $i++) {
        $O = $PartsOU[$i]
        $CurrentPath = 'OU=' + $O + ',' + $LevelPath
        $CanonicalCurrentPath = ConvertFrom-DistinguishedName -DistinguishedName ($CurrentPath.Replace(",$BasePath", "")) -ToCanonicalName
        $DirectRequest = $false
        if ($i -eq ($PartsOU.Count - 1)) {
            $DirectRequest = $true
        }

        # lets create new OU, but if it exists we will just inform user
        $OrganizationalUnitExists = Get-ADOrganizationalUnit -Filter "DistinguishedName -eq '$CurrentPath'" -ErrorAction SilentlyContinue -Properties $OUProperties -Server $DC
        if (-not $OrganizationalUnitExists) {
            try {
                $newADOrganizationalUnitSplat = @{
                    Name        = $O
                    Path        = $LevelPath
                    #ProtectedFromAccidentalDeletion = $False
                    Server      = $DC
                    ErrorAction = 'Stop'
                }
                if ($DirectRequest) {
                    foreach ($V in $ConfigurationOU.Keys) {
                        if ($V -notin $IgnoredProperties) {
                            $newADOrganizationalUnitSplat[$V] = $ConfigurationOU[$V]
                        }
                    }
                }
                Remove-EmptyValue -Hashtable $newADOrganizationalUnitSplat
                if ($newADOrganizationalUnitSplat.Count -eq 1) {
                    #Write-Color -Text '[!] ', "No OU will be created ", $CurrentPath -Color Red, White
                } else {
                    New-ADOrganizationalUnit @newADOrganizationalUnitSplat
                    Write-Color -Text '[+]', "[$CanonicalCurrentPath]", "[Adding]", " Added new organizational unit" -Color Green, DarkGray, Green, White
                }
            } catch {
                if ($_.Exception.Message -notlike '*with a name that is already in use*') {
                    Write-Color -Text '[!]', "[$CanonicalCurrentPath]", "[Error]", " Error $($_.Exception.message)" -Color Red, DarkGray, Yellow, Magenta, White
                } else {
                    if ($DirectRequest) {
                        Write-Color -Text '[*]', "[$CanonicalCurrentPath]", "[Skipping]", " Skipped new organizational unit, already exists!" -Color Magenta, DarkGray, Yellow, Magenta, White
                    }
                }
            }
        } else {
            if ($DirectRequest) {
                Write-Color -Text '[*]', "[$CanonicalCurrentPath]", "[Skipping]", " Skipped new organizational unit, already exists!" -Color Magenta, DarkGray, Yellow, Magenta, White
            }
        }
        if ($DirectRequest) {
            # lets update only if it's direct request, and not just a parent OU that was created before
            # once the OU is created we should update it with additional fields
            $setADOrganizationalUnitSplat = @{
                Identity = $CurrentPath
                Server   = $DC
            }
            $PropertiesToUpdate = foreach ($V in $ConfigurationOU.Keys) {
                if ($V -notin $IgnoredProperties) {
                    if ($OrganizationalUnitExists.$V -ne $ConfigurationOU[$V]) {
                        $V
                        $setADOrganizationalUnitSplat[$V] = $ConfigurationOU[$V]
                    }
                }
            }
            if ($setADOrganizationalUnitSplat.Count -eq 2) {
                #Write-Color -Text '[!] ', "Nothing to update for OU ", $CurrentPath -Color Red, White
            } else {
                Write-Color -Text '[+]', "[$CanonicalCurrentPath]", "[Updating]", " Updating organizational unit with fields: ", ($PropertiesToUpdate -join ", ") -Color Green, DarkGray, Green, White
                Set-ADOrganizationalUnit @setADOrganizationalUnitSplat
            }
        }
        $LevelPath = 'OU=' + $O + ',' + $LevelPath
    }
}
function Repair-GroupData {
    [CmdletBinding()]
    param(
        [string] $Group,
        [Array]  $PropertiesChangable,
        [Array] $StandardChangable,
        [Microsoft.ActiveDirectory.Management.ADGroup] $GroupExists,
        [System.Collections.IDictionary]$GroupObject,
        [string] $DC,
        [ValidateSet('Add', 'Remove', 'Skip')][string[]] $LogOption
    )
    if ($LogOption -contains 'Skip') {
        Write-Color -Text '[s] ', "Group ", $Group, " already exists" -Color Magenta, White, Magenta, White
    }
    foreach ($Key in $PropertiesChangable) {
        # we need to check whether key is defined at all and user wants to update it
        if ($null -ne $GroupObject.$Key -and $GroupExists.$Key -ne $GroupObject.$Key) {
            try {
                # we need to make sure DisplayName, Name are not empty, rest can be empty
                if ($Key -in @('DisplayName', 'Name') -and -not $Key) {
                    continue
                }
                if ($Key -in $StandardChangable) {
                    $setADObjectSplat = @{
                        Identity    = $GroupExists.DistinguishedName
                        ErrorAction = 'Stop'
                        $Key        = $GroupObject.$Key
                        Server      = $DC
                    }
                    Set-ADObject @setADObjectSplat
                } else {
                    Set-ADGroup -Identity $Group -Replace @{ $Key = $GroupObject.$Key } -ErrorAction Stop -Server $DC
                }
                if ($LogOption -contains 'Add') {
                    Write-Color -Text '[+] ', "Group ", $Group, " ", $Key, " updated" -Color Green, White, Green, White, Green, White
                }
            } catch {
                Write-Color -Text '[!] ', "Group ", $Group, " ", $Key, " update failed. Error: ", $_.Exception.Message -Color Red, White, Red, White, Red
            }
        }
    }
    $Location = ConvertFrom-DistinguishedName -DistinguishedName $GroupExists.DistinguishedName -ToOrganizationalUnit
    if ($Location -ne $GroupObject.Path) {
        if ($GroupExists.ProtectedFromAccidentalDeletion) {
            $ProtectedFromAccidentalDeletionFailed = $false
            try {
                Set-ADObject -ProtectedFromAccidentalDeletion $false -Identity $GroupExists.DistinguishedName -ErrorAction Stop -Server $DC
            } catch {
                Write-Color -Text '[!] ', "Group ", $Group, " move to ", $GroupObject.Path, " failed. Couldn't disable ProtectedFromAccidentalDeletion. Error: ", $_.Exception.Message -Color Red, White, Red, White, Red
                $ProtectedFromAccidentalDeletionFailed = true
            }
        }
        if (-not $ProtectedFromAccidentalDeletionFailed) {
            $MoveFailed = $false
            try {
                $null = Move-ADObject -Identity $GroupExists.DistinguishedName -TargetPath $GroupObject.Path -ErrorAction Stop -Server $DC
                if ($LogOption -contains 'Add') {
                    Write-Color -Text '[+] ', "Group ", $Group, " moved to ", $GroupObject.Path -Color Green, White, Green, White
                }
            } catch {
                $MoveFailed = $true
                Write-Color -Text '[!] ', "Group ", $Group, " move to ", $GroupObject.Path, " failed. Error: ", $_.Exception.Message -Color Red, White, Red, White, Red
            }
            if (-not $MoveFailed) {
                $PathToGroup = $GroupObject.Path
            } else {
                $PathToGroup = $GroupExists.DistinguishedName
            }
            if ($GroupExists.ProtectedFromAccidentalDeletion) {
                try {
                    Set-ADObject -ProtectedFromAccidentalDeletion $true -Identity $PathToGroup -ErrorAction Stop -Server $DC
                } catch {
                    Write-Color -Text '[!] ', "Group ", $Group, " move to ", $GroupObject.Path, " failed (maybe?). Couldn't enable ProtectedFromAccidentalDeletion. Error: ", $_.Exception.Message -Color Red, White, Red, White, Red
                }
            }

        }
    }
}
function New-DelegationGroup {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $Name,
        [string] $DisplayName,
        [Parameter()][string] $Path,
        [string] $Description,
        [Parameter(Mandatory)]
        [Microsoft.ActiveDirectory.Management.ADGroupScope] $GroupScope = [Microsoft.ActiveDirectory.Management.ADGroupScope]::DomainLocal,
        [Microsoft.ActiveDirectory.Management.ADGroupCategory] $GroupCategory = [Microsoft.ActiveDirectory.Management.ADGroupCategory]::Security,
        [string[]][ValidateSet('Add', 'Remove')] $MembersBehaviour,
        [string[]] $Members,
        [string[]] $MemberOf,
        [bool] $ProtectedFromAccidentalDeletion
    )
    [ordered] @{
        Name                            = $Name
        DisplayName                     = if (-not $DisplayName) { $Name } else { $DisplayName }
        Path                            = $Path
        Description                     = $Description
        GroupScope                      = $GroupScope
        GroupCategory                   = $GroupCategory
        ProtectedFromAccidentalDeletion = if ($PSBoundParameters.ContainsKey('ProtectedFromAccidentalDeletion')) { $ProtectedFromAccidentalDeletion } else { $null }
        MembersBehaviour                = $MembersBehaviour
        Members                         = if ($PSBoundParameters.ContainsKey('Members')) { $Members } else { $null }
        MemberOf                        = if ($PSBoundParameters.ContainsKey('MemberOf')) { $MemberOf } else { $null }
    }
}
function New-DelegationOrganizationalUnit {
    [alias('New-DelegationOU')]
    [CmdletBinding()]
    param(
        [parameter(Mandatory)][string] $CanonicalNameOU,
        [string] $Description,
        [ValidateSet('Enabled', 'Disabled')][string] $DelegationInheritance,
        [alias('DelegationRights')][Array] $Delegation,
        [bool] $ProtectedFromAccidentalDeletion
    )

    $InputData = [ordered] @{
        CanonicalNameOU                 = $CanonicalNameOU
        Description                     = $Description
        DelegationInheritance           = $DelegationInheritance
        Delegation                      = $Delegation
        ProtectedFromAccidentalDeletion = If ($PSBoundParameters.ContainsKey('ProtectedFromAccidentalDeletion')) { $ProtectedFromAccidentalDeletion } else { $null }
    }
    Remove-EmptyValue -Hashtable $InputData
    $InputData
}
function Start-DelegationGroups {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [scriptblock] $DelegationGroupsDefinition,
        [Parameter()][string] $Destination,
        [Parameter(Mandatory)][string] $Domain,
        [System.Collections.IDictionary] $Groups,
        [string[]][ValidateSet('Add', 'Remove')] $MembersBehaviour = @('Add', 'Remove'),
        [bool] $ProtectedFromAccidentalDeletion,
        [ValidateSet('Add', 'Remove', 'Skip')][string[]] $LogOption = @('Add', 'Skip', 'Remove')
    )

    $Properties = @('Name', 'Description', 'DisplayName', 'GroupScope', 'GroupCategory', 'ProtectedFromAccidentalDeletion')
    $PropertiesChangable = @('Description', 'DisplayName', 'ProtectedFromAccidentalDeletion', 'Path')
    $StandardChangable = @("GroupCategory", "GroupScope", "Name", "Path", "ProtectedFromAccidentalDeletion")

    $BasePath = ConvertTo-DistinguishedName -CanonicalName $Domain
    if (-not $BasePath) {
        return
    }

    # Initialize Delegation Model
    $DC = Initialize-DelegationModel -Domain $Domain
    if (-not $DC) {
        return
    }

    if ($PSBoundParameters.ContainsKey('DelegationGroupsDefinition')) {
        $DelegationOutput = & $DelegationGroupsDefinition
        $Groups = Convert-DelegationGroups -GroupInformation $DelegationOutput -Destination $Destination -MembersBehaviour $MembersBehaviour -ProtectedFromAccidentalDeletion $ProtectedFromAccidentalDeletion
    } else {
        $Groups = Convert-DelegationGroups -Groups $Groups -Destination $Destination -MembersBehaviour $MembersBehaviour -ProtectedFromAccidentalDeletion $ProtectedFromAccidentalDeletion
    }

    $OUCheck = $true
    foreach ($Group in $Groups.Keys) {
        # we need to check whether OU exists first, if not terminate and ask user to create it
        $GroupObject = $Groups[$Group]
        if ($GroupObject.Path) {
            try {
                $null = Get-ADOrganizationalUnit -Identity $GroupObject.Path -ErrorAction Stop -Server $DC
            } catch {
                $OUCheck = $false
                Write-Color -Text '[!] ', "Path OU $($GroupObject.Path)", " verification failed. Please create all organizational Units before continuing. Error: ", $_.Exception.Message -Color Red, Yellow, White
            }
        }
    }
    if ($OUCheck) {
        # lets fix the groups
        Write-Color -Text "[i] ", "Processing creation of groups" -Color Cyan, White
        foreach ($Group in $Groups.Keys) {
            $GroupObject = $Groups[$Group]
            $newADGroupSplat = @{
                WhatIf        = $false
                Path          = $GroupObject.Path
                Name          = if ($GroupObject.Name) { $GroupObject.Name } else { $Group }
                GroupScope    = $GroupObject.GroupScope
                GroupCategory = $GroupObject.GroupCategory
                Description   = $GroupObject.Description
                DisplayName   = if ($GroupObject.DisplayName) { $GroupObject.DisplayName } else { $Group }
            }
            Remove-EmptyValue -Hashtable $newADGroupSplat
            $GroupExists = Get-ADGroup -Filter "Name -eq '$($GroupObject.Name)'" -Properties $Properties -Server $DC
            if (-not $GroupExists) {
                # if the group does not exist, we will create it
                try {
                    $null = New-ADGroup @newADGroupSplat -ErrorAction Stop
                    if ($LogOption -contains 'Add') {
                        Write-Color -Text '[+] ', "Group ", $GroupObject.Name, " created" -Color Green, White, Green, White
                    }
                } catch {
                    Write-Color -Text '[!] ', "Group ", $GroupObject.Name, " creation failed. Error: ", $_.Exception.Message -Color Red, White, Red, White
                }
            } else {
                # if the group exists, we will check whether it needs to be updated
                Repair-GroupData -Group $GroupObject.Name -PropertiesChangable $PropertiesChangable -StandardChangable $StandardChangable -GroupObject $GroupObject -GroupExists $GroupExists -DC $DC -LogOption $LogOption
            }
        }
        Write-Color -Text "[i] ", "Processing Members for groups" -Color Cyan, White
        foreach ($Group in $Groups.Keys) {
            $GroupObject = $Groups[$Group]
            if ($null -ne $Groups[$Group].Members) {
                Find-GroupMembersActions -Identity $GroupObject.Name -ExpectedMembers $Groups[$Group].Members -DC $DC -MembersBehaviour $Groups[$Group].MembersBehaviour -LogOption $LogOption
            }
        }
        # MemberOf we will not attempt to cleanup as this may be some special group
        Write-Color -Text "[i] ", "Processing MemberOf for groups" -Color Cyan, White
        foreach ($Group in $Groups.Keys) {
            $GroupObject = $Groups[$Group]
            if ($null -ne $Groups[$Group].MemberOf) {
                foreach ($MemberOf in $Groups[$Group].MemberOf) {
                    Add-GroupMembersOf -Identity $MemberOf -Group $GroupObject.Name -DC $DC -LogOption $LogOption
                }
            }
        }
    }
}
function Start-DelegationModel {
    [cmdletBinding()]
    param(
        [scriptblock] $DelegationModelDefinition,
        [Parameter(Mandatory)][string] $Domain,
        [System.Collections.IDictionary] $Definition,
        [bool] $ProtectedFromAccidentalDeletion,
        [switch] $DontSuppress,
        [string] $LogFile,
        [int] $LogMaximum = 60,
        [ValidateSet('Add', 'Remove', 'Skip')][string[]] $LogOption = @('Add', 'Skip', 'Remove')
    )
    $Script:Cache = [ordered] @{}

    $BasePath = ConvertTo-DistinguishedName -CanonicalName $Domain
    if (-not $BasePath) {
        return
    }

    # Initialize Delegation Model
    $DC = Initialize-DelegationModel -Domain $Domain
    if (-not $DC) {
        return
    }

    Write-Color -Text '[i]', "[DelegationModel] ", 'Domain Controller ', $DC -Color Yellow, DarkGray, Yellow, DarkGray, Magenta
    # lets reset the cache of users and groups
    # this is required since we often just created groups in the same run
    $null = New-ADACLObject -Principal 'S-1-5-11' -AccessControlType Allow -ObjectType All -InheritedObjectTypeName All -AccessRule GenericAll -InheritanceType None -Force

    Write-Color '[i]', "[DelegationModel] ", 'Preparing data to be configured' -Color Yellow, DarkGray, Yellow
    if ($PSBoundParameters.ContainsKey('DelegationModelDefinition')) {
        $DelegationInput = Invoke-Command -ScriptBlock $DelegationModelDefinition -Verbose -WarningAction SilentlyContinue -WarningVariable Warnings
        #$DelegationInput = Invoke-CommandCustom -ScriptBlock $DelegationModelDefinition -ReturnVerbose -ReturnError -ReturnWarning
        # foreach ($W in $DelegationInput.Warning) {
        # Write-Color -Text "[!]", "[Delegationmodel]", "[Warning]", " Preloading rules, $($W)" -Color Magenta, DarkGray, Magenta, White
        # }
        # foreach ($W in $DelegationInput.Error) {
        # Write-Color -Text "[!]", "[Delegationmodel]", "[Error]", " Preloading rules, $($W)" -Color Magenta, DarkGray, Magenta, White
        # }
        #$Definition = Convert-DelegationModel -DelegationInput $DelegationInput.Output -Destination $Destination -ProtectedFromAccidentalDeletion $ProtectedFromAccidentalDeletion
        $Definition = Convert-DelegationModel -DelegationInput $DelegationInput -Destination $Destination -ProtectedFromAccidentalDeletion $ProtectedFromAccidentalDeletion
    } else {
        $Definition = Convert-DelegationModel -Definition $Definition -Destination $Destination -ProtectedFromAccidentalDeletion $ProtectedFromAccidentalDeletion
    }

    Write-Color '[i]', "[DelegationModel] ", 'Managing Organizational Units' -Color Yellow, DarkGray, Yellow
    foreach ($CanonicalNameOU in $Definition.Keys) {
        $ConfigurationOU = $Definition[$CanonicalNameOU]
        New-OUStructure -CanonicalNameOU $CanonicalNameOU -ConfigurationOU $ConfigurationOU -BasePath $BasePath -DC $DC # -ProtectedFromAccidentalDeletion $ProtectedFromAccidentalDeletion
    }
    Write-Color '[i]', "[DelegationModel] ", 'Managing Delegation' -Color Yellow, DarkGray, Yellow
    foreach ($CanonicalNameOU in $Definition.Keys) {
        $ConfigurationOU = $Definition[$CanonicalNameOU]
        if ($ConfigurationOU.Delegation) {
            $OutputFromDelegation = New-DelegationModel -Domain $Domain -CanonicalNameOU $CanonicalNameOU -ConfigurationOU $ConfigurationOU -BasePath $BasePath
            Export-DelegationLogs -LogOption $LogOption -OutputFromDelegation $OutputFromDelegation -CanonicalNameOU $CanonicalNameOU
            if ($DontSuppress) {
                $OutputFromDelegation
            }
        }
    }
}



# Export functions and aliases as required
Export-ModuleMember -Function @('New-DelegationGroup', 'New-DelegationOrganizationalUnit', 'Start-DelegationGroups', 'Start-DelegationModel') -Alias @('New-DelegationOU')
# SIG # Begin signature block
# MIInPgYJKoZIhvcNAQcCoIInLzCCJysCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDtmXX2mPMB9AIr
# Z8JtPeOK34R5xNnnyTDVa2zKGiF3xKCCITcwggO3MIICn6ADAgECAhAM5+DlF9hG
# /o/lYPwb8DA5MA0GCSqGSIb3DQEBBQUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBa
# Fw0zMTExMTAwMDAwMDBaMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lD
# ZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
# AQoCggEBAK0OFc7kQ4BcsYfzt2D5cRKlrtwmlIiq9M71IDkoWGAM+IDaqRWVMmE8
# tbEohIqK3J8KDIMXeo+QrIrneVNcMYQq9g+YMjZ2zN7dPKii72r7IfJSYd+fINcf
# 4rHZ/hhk0hJbX/lYGDW8R82hNvlrf9SwOD7BG8OMM9nYLxj+KA+zp4PWw25EwGE1
# lhb+WZyLdm3X8aJLDSv/C3LanmDQjpA1xnhVhyChz+VtCshJfDGYM2wi6YfQMlqi
# uhOCEe05F52ZOnKh5vqk2dUXMXWuhX0irj8BRob2KHnIsdrkVxfEfhwOsLSSplaz
# vbKX7aqn8LfFqD+VFtD/oZbrCF8Yd08CAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGG
# MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEXroq/0ksuCMS1Ri6enIZ3zbcgP
# MB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBBQUA
# A4IBAQCiDrzf4u3w43JzemSUv/dyZtgy5EJ1Yq6H6/LV2d5Ws5/MzhQouQ2XYFwS
# TFjk0z2DSUVYlzVpGqhH6lbGeasS2GeBhN9/CTyU5rgmLCC9PbMoifdf/yLil4Qf
# 6WXvh+DfwWdJs13rsgkq6ybteL59PyvztyY1bV+JAbZJW58BBZurPSXBzLZ/wvFv
# hsb6ZGjrgS2U60K3+owe3WLxvlBnt2y98/Efaww2BxZ/N3ypW2168RJGYIPXJwS+
# S86XvsNnKmgR34DnDDNmvxMNFG7zfx9jEB76jRslbWyPpbdhAbHSoyahEHGdreLD
# +cOZUbcrBwjOLuZQsqf6CkUvovDyMIIFMDCCBBigAwIBAgIQBAkYG1/Vu2Z1U0O1
# b5VQCDANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGln
# aUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtE
# aWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMTMxMDIyMTIwMDAwWhcNMjgx
# MDIyMTIwMDAwWjByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5j
# MRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBT
# SEEyIEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0BAQEF
# AAOCAQ8AMIIBCgKCAQEA+NOzHH8OEa9ndwfTCzFJGc/Q+0WZsTrbRPV/5aid2zLX
# cep2nQUut4/6kkPApfmJ1DcZ17aq8JyGpdglrA55KDp+6dFn08b7KSfH03sjlOSR
# I5aQd4L5oYQjZhJUM1B0sSgmuyRpwsJS8hRniolF1C2ho+mILCCVrhxKhwjfDPXi
# TWAYvqrEsq5wMWYzcT6scKKrzn/pfMuSoeU7MRzP6vIK5Fe7SrXpdOYr/mzLfnQ5
# Ng2Q7+S1TqSp6moKq4TzrGdOtcT3jNEgJSPrCGQ+UpbB8g8S9MWOD8Gi6CxR93O8
# vYWxYoNzQYIH5DiLanMg0A9kczyen6Yzqf0Z3yWT0QIDAQABo4IBzTCCAckwEgYD
# VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYB
# BQUHAwMweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k
# aWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0
# LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4
# oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJv
# b3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy
# dEFzc3VyZWRJRFJvb3RDQS5jcmwwTwYDVR0gBEgwRjA4BgpghkgBhv1sAAIEMCow
# KAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCgYIYIZI
# AYb9bAMwHQYDVR0OBBYEFFrEuXsqCqOl6nEDwGD5LfZldQ5YMB8GA1UdIwQYMBaA
# FEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBCwUAA4IBAQA+7A1aJLPz
# ItEVyCx8JSl2qB1dHC06GsTvMGHXfgtg/cM9D8Svi/3vKt8gVTew4fbRknUPUbRu
# pY5a4l4kgU4QpO4/cY5jDhNLrddfRHnzNhQGivecRk5c/5CxGwcOkRX7uq+1UcKN
# JK4kxscnKqEpKBo6cSgCPC6Ro8AlEeKcFEehemhor5unXCBc2XGxDI+7qPjFEmif
# z0DLQESlE/DmZAwlCEIysjaKJAL+L3J+HNdJRZboWR3p+nRka7LrZkPas7CM1ekN
# 3fYBIM6ZMWM9CBoYs4GbT8aTEAb8B4H6i9r5gkn3Ym6hU/oSlBiFLpKR6mhsRDKy
# ZqHnGKSaZFHvMIIFPTCCBCWgAwIBAgIQBNXcH0jqydhSALrNmpsqpzANBgkqhkiG
# 9w0BAQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkw
# FwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEy
# IEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTIwMDYyNjAwMDAwMFoXDTIz
# MDcwNzEyMDAwMFowejELMAkGA1UEBhMCUEwxEjAQBgNVBAgMCcWabMSFc2tpZTER
# MA8GA1UEBxMIS2F0b3dpY2UxITAfBgNVBAoMGFByemVteXPFgmF3IEvFgnlzIEVW
# T1RFQzEhMB8GA1UEAwwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMIIBIjANBgkq
# hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7KB3iyBrhkLUbbFe9qxhKKPBYqDBqln
# r3AtpZplkiVjpi9dMZCchSeT5ODsShPuZCIxJp5I86uf8ibo3vi2S9F9AlfFjVye
# 3dTz/9TmCuGH8JQt13ozf9niHecwKrstDVhVprgxi5v0XxY51c7zgMA2g1Ub+3ti
# i0vi/OpmKXdL2keNqJ2neQ5cYly/GsI8CREUEq9SZijbdA8VrRF3SoDdsWGf3tZZ
# zO6nWn3TLYKQ5/bw5U445u/V80QSoykszHRivTj+H4s8ABiforhi0i76beA6Ea41
# zcH4zJuAp48B4UhjgRDNuq8IzLWK4dlvqrqCBHKqsnrF6BmBrv+BXQIDAQABo4IB
# xTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0OBBYE
# FBixNSfoHFAgJk4JkDQLFLRNlJRmMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK
# BggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdpY2Vy
# dC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2NybDQu
# ZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUwQzA3
# BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu
# Y29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNpZ25p
# bmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAmr1sz4ls
# LARi4wG1eg0B8fVJFowtect7SnJUrp6XRnUG0/GI1wXiLIeow1UPiI6uDMsRXPHU
# F/+xjJw8SfIbwava2eXu7UoZKNh6dfgshcJmo0QNAJ5PIyy02/3fXjbUREHINrTC
# vPVbPmV6kx4Kpd7KJrCo7ED18H/XTqWJHXa8va3MYLrbJetXpaEPpb6zk+l8Rj9y
# G4jBVRhenUBUUj3CLaWDSBpOA/+sx8/XB9W9opYfYGb+1TmbCkhUg7TB3gD6o6ES
# Jre+fcnZnPVAPESmstwsT17caZ0bn7zETKlNHbc1q+Em9kyBjaQRcEQoQQNpezQu
# g9ufqExx6lHYDjCCBY0wggR1oAMCAQICEA6bGI750C3n79tQ4ghAGFowDQYJKoZI
# hvcNAQEMBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ
# MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNz
# dXJlZCBJRCBSb290IENBMB4XDTIyMDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1OVow
# YjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ
# d3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290
# IEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAv+aQc2jeu+RdSjww
# IjBpM+zCpyUuySE98orYWcLhKac9WKt2ms2uexuEDcQwH/MbpDgW61bGl20dq7J5
# 8soR0uRf1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNwwrK6dZlqczKU0RBEEC7fgvMH
# hOZ0O21x4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs06wXGXuxbGrzryc/NrDRAX7F6
# Zu53yEioZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e5TXnMcvak17cjo+A2raRmECQ
# ecN4x7axxLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtVgkEy19sEcypukQF8IUzUvK4b
# A3VdeGbZOjFEmjNAvwjXWkmkwuapoGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2mHY9
# WV1CdoeJl2l6SPDgohIbZpp0yt5LHucOY67m1O+SkjqePdwA5EUlibaaRBkrfsCU
# tNJhbesz2cXfSwQAzH0clcOP9yGyshG3u3/y1YxwLEFgqrFjGESVGnZifvaAsPvo
# ZKYz0YkH4b235kOkGLimdwHhD5QMIR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxfjT/J
# vNNBERJb5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFrb7GrhotPwtZFX50g/KEexcCP
# orF+CiaZ9eRpL5gdLfXZqbId5RsCAwEAAaOCATowggE2MA8GA1UdEwEB/wQFMAMB
# Af8wHQYDVR0OBBYEFOzX44LScV1kTN8uZz/nupiuHA9PMB8GA1UdIwQYMBaAFEXr
# oq/0ksuCMS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQEAwIBhjB5BggrBgEFBQcBAQRt
# MGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEF
# BQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJl
# ZElEUm9vdENBLmNydDBFBgNVHR8EPjA8MDqgOKA2hjRodHRwOi8vY3JsMy5kaWdp
# Y2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMBEGA1UdIAQKMAgw
# BgYEVR0gADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3v1cH
# vZqsoYcs7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy3iS8
# UgPITtAq3votVs/59PesMHqai7Je1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cnRNTn
# f+hZqPC/Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4oVaO7KTVPeix3P0c2PR3WlxU
# jG/voVA9/HYJaISfb8rbII01YBwCA8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2zm8j
# LfR+cWojayL/ErhULSd+2DrZ8LaHlv1b0VysGMNNn3O3AamfV6peKOK5lDCCBq4w
# ggSWoAMCAQICEAc2N7ckVHzYR6z9KGYqXlswDQYJKoZIhvcNAQELBQAwYjELMAkG
# A1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRp
# Z2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MB4X
# DTIyMDMyMzAwMDAwMFoXDTM3MDMyMjIzNTk1OVowYzELMAkGA1UEBhMCVVMxFzAV
# BgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVk
# IEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFtcGluZyBDQTCCAiIwDQYJKoZIhvcN
# AQEBBQADggIPADCCAgoCggIBAMaGNQZJs8E9cklRVcclA8TykTepl1Gh1tKD0Z5M
# om2gsMyD+Vr2EaFEFUJfpIjzaPp985yJC3+dH54PMx9QEwsmc5Zt+FeoAn39Q7SE
# 2hHxc7Gz7iuAhIoiGN/r2j3EF3+rGSs+QtxnjupRPfDWVtTnKC3r07G1decfBmWN
# lCnT2exp39mQh0YAe9tEQYncfGpXevA3eZ9drMvohGS0UvJ2R/dhgxndX7RUCyFo
# bjchu0CsX7LeSn3O9TkSZ+8OpWNs5KbFHc02DVzV5huowWR0QKfAcsW6Th+xtVhN
# ef7Xj3OTrCw54qVI1vCwMROpVymWJy71h6aPTnYVVSZwmCZ/oBpHIEPjQ2OAe3Vu
# JyWQmDo4EbP29p7mO1vsgd4iFNmCKseSv6De4z6ic/rnH1pslPJSlRErWHRAKKtz
# Q87fSqEcazjFKfPKqpZzQmiftkaznTqj1QPgv/CiPMpC3BhIfxQ0z9JMq++bPf4O
# uGQq+nUoJEHtQr8FnGZJUlD0UfM2SU2LINIsVzV5K6jzRWC8I41Y99xh3pP+OcD5
# sjClTNfpmEpYPtMDiP6zj9NeS3YSUZPJjAw7W4oiqMEmCPkUEBIDfV8ju2TjY+Cm
# 4T72wnSyPx4JduyrXUZ14mCjWAkBKAAOhFTuzuldyF4wEr1GnrXTdrnSDmuZDNIz
# tM2xAgMBAAGjggFdMIIBWTASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBS6
# FtltTYUvcyl2mi91jGogj57IbzAfBgNVHSMEGDAWgBTs1+OC0nFdZEzfLmc/57qY
# rhwPTzAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgwdwYIKwYB
# BQUHAQEEazBpMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w
# QQYIKwYBBQUHMAKGNWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy
# dFRydXN0ZWRSb290RzQuY3J0MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwz
# LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3JsMCAGA1UdIAQZ
# MBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAgEAfVmO
# wJO2b5ipRCIBfmbW2CFC4bAYLhBNE88wU86/GPvHUF3iSyn7cIoNqilp/GnBzx0H
# 6T5gyNgL5Vxb122H+oQgJTQxZ822EpZvxFBMYh0MCIKoFr2pVs8Vc40BIiXOlWk/
# R3f7cnQU1/+rT4osequFzUNf7WC2qk+RZp4snuCKrOX9jLxkJodskr2dfNBwCnzv
# qLx1T7pa96kQsl3p/yhUifDVinF2ZdrM8HKjI/rAJ4JErpknG6skHibBt94q6/ae
# sXmZgaNWhqsKRcnfxI2g55j7+6adcq/Ex8HBanHZxhOACcS2n82HhyS7T6NJuXdm
# kfFynOlLAlKnN36TU6w7HQhJD5TNOXrd/yVjmScsPT9rp/Fmw0HNT7ZAmyEhQNC3
# EyTN3B14OuSereU0cZLXJmvkOHOrpgFPvT87eK1MrfvElXvtCl8zOYdBeHo46Zzh
# 3SP9HSjTx/no8Zhf+yvYfvJGnXUsHicsJttvFXseGYs2uJPU5vIXmVnKcPA3v5gA
# 3yAWTyf7YGcWoWa63VXAOimGsJigK+2VQbc61RWYMbRiCQ8KvYHZE/6/pNHzV9m8
# BPqC3jLfBInwAM1dwvnQI38AC+R2AibZ8GV2QqYphwlHK+Z/GqSFD/yYlvZVVCsf
# gPrA8g4r5db7qS9EFUrnEw4d2zc4GqEr9u3WfPwwggbAMIIEqKADAgECAhAMTWly
# S5T6PCpKPSkHgD1aMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVTMRcwFQYD
# VQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBH
# NCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwHhcNMjIwOTIxMDAwMDAw
# WhcNMzMxMTIxMjM1OTU5WjBGMQswCQYDVQQGEwJVUzERMA8GA1UEChMIRGlnaUNl
# cnQxJDAiBgNVBAMTG0RpZ2lDZXJ0IFRpbWVzdGFtcCAyMDIyIC0gMjCCAiIwDQYJ
# KoZIhvcNAQEBBQADggIPADCCAgoCggIBAM/spSY6xqnya7uNwQ2a26HoFIV0Mxom
# rNAcVR4eNm28klUMYfSdCXc9FZYIL2tkpP0GgxbXkZI4HDEClvtysZc6Va8z7GGK
# 6aYo25BjXL2JU+A6LYyHQq4mpOS7eHi5ehbhVsbAumRTuyoW51BIu4hpDIjG8b7g
# L307scpTjUCDHufLckkoHkyAHoVW54Xt8mG8qjoHffarbuVm3eJc9S/tjdRNlYRo
# 44DLannR0hCRRinrPibytIzNTLlmyLuqUDgN5YyUXRlav/V7QG5vFqianJVHhoV5
# PgxeZowaCiS+nKrSnLb3T254xCg/oxwPUAY3ugjZNaa1Htp4WB056PhMkRCWfk3h
# 3cKtpX74LRsf7CtGGKMZ9jn39cFPcS6JAxGiS7uYv/pP5Hs27wZE5FX/NurlfDHn
# 88JSxOYWe1p+pSVz28BqmSEtY+VZ9U0vkB8nt9KrFOU4ZodRCGv7U0M50GT6Vs/g
# 9ArmFG1keLuY/ZTDcyHzL8IuINeBrNPxB9ThvdldS24xlCmL5kGkZZTAWOXlLimQ
# prdhZPrZIGwYUWC6poEPCSVT8b876asHDmoHOWIZydaFfxPZjXnPYsXs4Xu5zGcT
# B5rBeO3GiMiwbjJ5xwtZg43G7vUsfHuOy2SJ8bHEuOdTXl9V0n0ZKVkDTvpd6kVz
# HIR+187i1Dp3AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/
# BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEE
# AjALBglghkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8w
# HQYDVR0OBBYEFGKK3tBh/I8xFO2XC809KpQU31KcMFoGA1UdHwRTMFEwT6BNoEuG
# SWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQw
# OTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQG
# CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKG
# TGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJT
# QTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIB
# AFWqKhrzRvN4Vzcw/HXjT9aFI/H8+ZU5myXm93KKmMN31GT8Ffs2wklRLHiIY1UJ
# RjkA/GnUypsp+6M/wMkAmxMdsJiJ3HjyzXyFzVOdr2LiYWajFCpFh0qYQitQ/Bu1
# nggwCfrkLdcJiXn5CeaIzn0buGqim8FTYAnoo7id160fHLjsmEHw9g6A++T/350Q
# p+sAul9Kjxo6UrTqvwlJFTU2WZoPVNKyG39+XgmtdlSKdG3K0gVnK3br/5iyJpU4
# GYhEFOUKWaJr5yI+RCHSPxzAm+18SLLYkgyRTzxmlK9dAlPrnuKe5NMfhgFknADC
# 6Vp0dQ094XmIvxwBl8kZI4DXNlpflhaxYwzGRkA7zl011Fk+Q5oYrsPJy8P7mxNf
# arXH4PMFw1nfJ2Ir3kHJU7n/NBBn9iYymHv+XEKUgZSCnawKi8ZLFUrTmJBFYDOA
# 4CPe+AOk9kVH5c64A0JH6EE2cXet/aLol3ROLtoeHYxayB6a1cLwxiKoT5u92Bya
# UcQvmvZfpyeXupYuhVfAYOd4Vn9q78KVmksRAsiCnMkaBXy6cbVOepls9Oie1FqY
# yJ+/jbsYXEP10Cro4mLueATbvdH7WwqocH7wl4R44wgDXUcsY6glOJcB0j862uXl
# 9uab3H4szP8XTE0AotjWAQ64i+7m4HJViSwnGWH2dwGMMYIFXTCCBVkCAQEwgYYw
# cjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ
# d3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVk
# IElEIENvZGUgU2lnbmluZyBDQQIQBNXcH0jqydhSALrNmpsqpzANBglghkgBZQME
# AgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEM
# BgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqG
# SIb3DQEJBDEiBCDRkLm3F+MkMJzBiMgZH/eMGr5MgYghmwP0L+3JUz0ncjANBgkq
# hkiG9w0BAQEFAASCAQBTT6/BlbDJjaP8MPCBxEENoImGdw9P4LUWOkeI7tgMhKR5
# Ykvw+NcyyGF2qdwDR1zaysbTctTBDd0PN29XKBG9l4Ci/rHnOAMYuEB3lMxQqbQ3
# uHhMTAYFMNGkv8iIEXdsMw4FJJr3dnLAYP4P9tq0ffZiBxsdh7lBG6CSt3NZ3px8
# e2JmcDknA8Q2vtyDjtCDQIDc8GuNHsjBiAFpSTxclgZG8umTSu+PwDUxL9gWO/X9
# U9aoU2kJN52dM7+1hrjS4iPHokwGcQv9CdfFirTOFbTVRYzYFMHTDR2X1cMBxP/h
# ibArxvCjYnAmtrUCs4YqI1hUaEXbftRuLecYFwg8oYIDIDCCAxwGCSqGSIb3DQEJ
# BjGCAw0wggMJAgEBMHcwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0
# LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hB
# MjU2IFRpbWVTdGFtcGluZyBDQQIQDE1pckuU+jwqSj0pB4A9WjANBglghkgBZQME
# AgEFAKBpMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X
# DTIzMDIyMTE0MjY1NVowLwYJKoZIhvcNAQkEMSIEIE1rHCkNZEWMP8M3ts3pryRF
# G0xqmgzdlquhBMxmJcQnMA0GCSqGSIb3DQEBAQUABIICALRcC/NCcsn19qGpl64D
# uWPOE13Tjg/qPmIoj4eem3w0dBCdUDTSp3gQhy8GExDYlCSE2vopZzIupd6xrAZs
# NxBev0IVF2ya17HM07o14mIV7P4qbpdjkNRMz9rba8knZMw7YQuhhRRbSx855dSn
# YVMNLWOgeobI82DhlUCcLj8k6N8s76+hv0hTWBL/vOHBJQCxpTc8P7flZReFlg7o
# O/+NW9XL1asHUtQ+602Vj4JLFpSPIY4rneHVe/+l7jeL8x8gfjhXx+HNRJdcF3C0
# /VZE5evWjMvFrlrE7Ji8LBFbJVpES8ncYATQMGEUdS/hBugch9vhTGQGra/p7Wja
# 83uB1auMe8+OCXmZHoifhQA/k1ufh/dClK8LCBKPGbyI0B0h1/M3af88KxF2Y3HG
# DS3/a6AiiSuFOAW3qEenn2D87jtjTn4QmhEfXAkJT4Dpy1V3fWEs+912/NNAJ6ze
# dFNzvFSHuXJ0Eqly6CRph1B2LV9txbZPpZZa2pu9kZQpbs96apsy4FqcDEvAVDfg
# e2rgQigKUyRIv89k2Yi0MTDMdE5EzrDRbiXF4flRlYGnssF7Qc+SnUc45WOzlxSl
# vCV7VeVu7u8SD5GP1UuQJLuJ1MW0y6Bu8O7+BQNYLaF+DSKJ9/iDFBe6J2wSr2BQ
# Yk2jXpyEDbMKywy/ddZXPPeT
# SIG # End signature block