PasswordSolution.psm1

function ConvertFrom-DistinguishedName { 
    <#
    .SYNOPSIS
    Short description
 
    .DESCRIPTION
    Long description
 
    .PARAMETER DistinguishedName
    Parameter description
 
    .PARAMETER ToOrganizationalUnit
    Parameter description
 
    .PARAMETER ToDC
    Parameter description
 
    .PARAMETER ToDomainCN
    Parameter description
 
    .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
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param([alias('Identity', 'DN')][Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)][string[]] $DistinguishedName,
        [switch] $ToOrganizationalUnit,
        [switch] $ToDC,
        [switch] $ToDomainCN)
    process {
        foreach ($Distinguished in $DistinguishedName) {
            if ($ToDomainCN) {
                $DN = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1'
                $CN = $DN -replace ',DC=', '.' -replace "DC="
                $CN
            } elseif ($ToOrganizationalUnit) { [Regex]::Match($Distinguished, '(?=OU=)(.*\n?)(?<=.)').Value } elseif ($ToDC) { $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1' } else {
                $Regex = '^CN=(?<cn>.+?)(?<!\\),(?<ou>(?:(?:OU|CN).+?(?<!\\),)+(?<dc>DC.+?))$'
                $Output = foreach ($_ in $Distinguished) {
                    $_ -match $Regex
                    $Matches
                }
                $Output.cn
            }
        }
    }
}
function Convert-UserAccountControl { 
    [cmdletBinding()]
    param([alias('UAC')][int] $UserAccountControl,
        [string] $Separator)
    $UserAccount = [ordered] @{"SCRIPT"  = 1
        "ACCOUNTDISABLE"                 = 2
        "HOMEDIR_REQUIRED"               = 8
        "LOCKOUT"                        = 16
        "PASSWD_NOTREQD"                 = 32
        "ENCRYPTED_TEXT_PWD_ALLOWED"     = 128
        "TEMP_DUPLICATE_ACCOUNT"         = 256
        "NORMAL_ACCOUNT"                 = 512
        "INTERDOMAIN_TRUST_ACCOUNT"      = 2048
        "WORKSTATION_TRUST_ACCOUNT"      = 4096
        "SERVER_TRUST_ACCOUNT"           = 8192
        "DONT_EXPIRE_PASSWORD"           = 65536
        "MNS_LOGON_ACCOUNT"              = 131072
        "SMARTCARD_REQUIRED"             = 262144
        "TRUSTED_FOR_DELEGATION"         = 524288
        "NOT_DELEGATED"                  = 1048576
        "USE_DES_KEY_ONLY"               = 2097152
        "DONT_REQ_PREAUTH"               = 4194304
        "PASSWORD_EXPIRED"               = 8388608
        "TRUSTED_TO_AUTH_FOR_DELEGATION" = 16777216
        "PARTIAL_SECRETS_ACCOUNT"        = 67108864
    }
    $Output = foreach ($_ in $UserAccount.Keys) {
        $binaryAnd = $UserAccount[$_] -band $UserAccountControl
        if ($binaryAnd -ne "0") { $_ }
    }
    if ($Separator) { $Output -join $Separator } else { $Output }
}
function 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 ($_ in $ForestInformation.Domains) {
            if ($IncludeDomains) {
                if ($_ -in $IncludeDomains) { $_.ToLower() }
                continue
            }
            if ($_ -notin $ExcludeDomains) { $_.ToLower() }
        }
        foreach ($Domain in $ForestInformation.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
        }
        [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 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
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param([string] $OperatingSystem,
        [string] $OperatingSystemVersion)
    if ($OperatingSystem -like '*Windows 10*') {
        $Systems = @{'10.0 (19043)' = 'Windows 10 21H1'
            '10.0 (19042)'          = 'Windows 10 20H2'
            '10.0 (19041)'          = 'Windows 10 2004'
            '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 (18898)'          = 'Windows 10 Insider Preview'
            '10.0.19043'            = 'Windows 10 21H1'
            '10.0.19042'            = 'Windows 10 20H2'
            '10.0.19041'            = 'Windows 10 2004'
            '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.18898'            = 'Windows 10 Insider Preview'
        }
        $System = $Systems[$OperatingSystemVersion]
        if (-not $System) { $System = $OperatingSystem }
    } elseif ($OperatingSystem -like '*Windows Server*') {
        $Systems = @{'5.2 (3790)' = 'Windows Server 2003'
            '6.1 (7601)'          = 'Windows Server 2008 R2'
            '10.0 (18362)'        = "Windows Server, version 1903 (Semi-Annual Channel) 1903"
            '10.0 (17763)'        = "Windows Server 2019 (Long-Term Servicing Channel) 1809"
            '10.0 (17134)'        = "Windows Server, version 1803 (Semi-Annual Channel) 1803"
            '10.0 (14393)'        = "Windows Server 2016 (Long-Term Servicing Channel) 1607"
            '10.0.18362'          = "Windows Server, version 1903 (Semi-Annual Channel) 1903"
            '10.0.17763'          = "Windows Server 2019 (Long-Term Servicing Channel) 1809"
            '10.0.17134'          = "Windows Server, version 1803 (Semi-Annual Channel) 1803"
            '10.0.14393'          = "Windows Server 2016 (Long-Term Servicing Channel) 1607"
        }
        $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 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-ManagerInformation {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $SummaryDictionary,
        [string] $Type,
        [string] $ManagerType,
        [Object] $Key,
        [PSCustomObject] $User,
        [PSCustomObject] $Rule
        # [bool] $Enabled
    )
    #if ($Enabled) {
    if ($Key) {
        if ($Key -is [string]) {
            $KeyDN = $Key
        } else {
            $KeyDN = $Key.EmailAddress
        }

        if (-not $SummaryDictionary[$KeyDN]) {
            $SummaryDictionary[$KeyDN] = [ordered] @{
                Manager             = $Key
                ManagerDefault      = [ordered] @{}
                ManagerNotCompliant = [ordered] @{}
            }
        }
        $SummaryDictionary[$KeyDN][$Type][$User.DistinguishedName] = [ordered] @{
            Manager       = $User.ManagerDN
            User          = $User
            Rule          = $Rule
            ManagerOption = $Type
            Output        = [ordered] @{}
        }
        $Default = [ordered] @{
            DisplayName     = $User.DisplayName
            Enabled         = $User.Enabled
            SamAccountName  = $User.SamAccountName
            Domain          = $User.Domain
            DateExpiry      = $User.DateExpiry
            DaysToExpire    = $User.DaysToExpire
            PasswordLastSet = $User.PasswordLastSet
            PasswordExpired = $User.PasswordExpired
        }
        if ($Type -ne 'ManagerDefault') {
            $Extended = [ordered] @{
                'Manager Status' = $ManagerType
                #ManagerStatus = $User.ManagerStatus
                Manager          = $User.Manager
                'Manager Email'  = $User.ManagerEmail
                #RuleName = $Rule.Name
                #Type = $Type
            }
            $SummaryDictionary[$KeyDN][$Type][$User.DistinguishedName]['Output'] = [PSCustomObject] ( $Extended + $Default)
        } else {
            $SummaryDictionary[$KeyDN][$Type][$User.DistinguishedName]['Output'] = [PSCustomObject] $Default
        }
    }
    # }
}
function Add-ParametersToString {
    <#
    .SYNOPSIS
    Short description
 
    .DESCRIPTION
    Long description
 
    .PARAMETER String
    Parameter description
 
    .PARAMETER Parameter
    Parameter description
 
    .EXAMPLE
    $Test = 'this is a string $Test - and $Test2 AND $tEST3'
 
    Add-ParametersToString -String $Test -Parameter @{
        Testooo = 'sdsds'
        Test = 'oh my god'
        Test2 = 'ole ole'
        TEST3 = '56555'
    }
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [string] $String,
        [System.Collections.IDictionary] $Parameter
    )
    $Sorted = $Parameter.Keys | Sort-Object { $_.length } -Descending


    foreach ($Key in $Sorted) {
        $String = $String -ireplace [Regex]::Escape("`$$Key"), $Parameter[$Key]
    }
    $String
}
function Find-Password {
    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [System.Collections.IDictionary] $ExtendedForestInformation,
        [string] $OverwriteEmailProperty,
        #[Array] $ConditionProperties,
        [System.Collections.IDictionary] $WriteParameters,
        #[System.Collections.IDictionary] $CachedUsers,
        #[System.Collections.IDictionary] $CachedUsersPrepared,
        #[System.Collections.IDictionary] $CachedManagers
        [switch] $AsHashTable
    )
    $Today = Get-Date
    if ($null -eq $WriteParameters) {
        $WriteParameters = @{
            ShowTime   = $true
            LogFile    = ""
            TimeFormat = "yyyy-MM-dd HH:mm:ss"
        }
    }


    $Properties = @(
        'Manager', 'DisplayName', 'GivenName', 'Surname', 'SamAccountName', 'EmailAddress', 'msDS-UserPasswordExpiryTimeComputed', 'PasswordExpired', 'PasswordLastSet', 'PasswordNotRequired', 'Enabled', 'PasswordNeverExpires', 'Mail', 'MemberOf', 'LastLogonDate'
        'userAccountControl'
        if ($OverwriteEmailProperty) {
            $OverwriteEmailProperty
        }
        if ($ConditionProperties) {
            $ConditionProperties
        }
    )
    # We're caching all users to make sure it's speedy gonzales when querying for Managers
    if (-not $CachedUsers) {
        $CachedUsers = [ordered] @{ }
    }
    if (-not $Cache) {
        $Cache = [ordered] @{ }
    }
    Write-Color @WriteParameters -Text "[i] Discovering forest information" -Color White, Yellow, White, Yellow, White, Yellow, White
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -ExcludeDomains $ExcludeDomains -IncludeDomains $IncludeDomains -ExtendedForestInformation $ExtendedForestInformation

    [Array] $Users = foreach ($Domain in $ForestInformation.Domains) {
        Write-Color @WriteParameters -Text "[i] Discovering DC for domain ", "$($Domain)", " in forest ", $ForestInformation.Name -Color White, Yellow, White, Yellow, White, Yellow, White
        $Server = $ForestInformation['QueryServers'][$Domain]['HostName'][0]

        Write-Color @WriteParameters -Text "[i] Getting users from ", "$($Domain)", " using ", $Server -Color White, Yellow, White, Yellow, White, Yellow, White
        # We query all users instead of using filter. Since we need manager field and manager data this way it should be faster (query once - get it all)
        #$DomainUsers = Get-ADUser -Server $Server -Filter '*' -Properties $Properties -ErrorAction Stop
        #foreach ($_ in $DomainUsers) {
        #$CachedUsers["$($_.DistinguishedName)"] = $_
        # We reuse filtering, account is enabled, password is required and password is not set to change on next logon
        #if ($_.Enabled -eq $true -and $_.PasswordNotRequired -ne $true -and $null -ne $_.PasswordLastSet) {
        # $_
        #}
        #}
        try {
            Get-ADUser -Server $Server -Filter '*' -Properties $Properties -ErrorAction Stop
        } catch {
            $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " "
            Write-Color @WriteParameters '[e] Error: ', $ErrorMessage -Color White, Red
        }
    }
    foreach ($User in $Users) {
        $Cache[$User.DistinguishedName] = $User
    }
    Write-Color @WriteParameters -Text "[i] Preparing all users for password expirations in forest ", $Forest.Name -Color White, Yellow, White, Yellow, White, Yellow, White
    $ProcessedUsers = foreach ($User in $Users) {
        #$UserManager = $Cache["$($User.Manager)"]
        if ($User.Manager) {
            $Manager = $Cache[$User.Manager].DisplayName
            $ManagerSamAccountName = $Cache[$User.Manager].SamAccountName
            $ManagerEmail = $Cache[$User.Manager].Mail
            $ManagerEnabled = $Cache[$User.Manager].Enabled
            $ManagerLastLogon = $Cache[$User.Manager].LastLogonDate
            if ($ManagerLastLogon) {
                $ManagerLastLogonDays = $( - $($ManagerLastLogon - $Today).Days)
            } else {
                $ManagerLastLogonDays = $null
            }
            $ManagerStatus = if ($ManagerEnabled) { 'Enabled' } else { 'Disabled' }
        } else {
            if ($User.ObjectClass -eq 'user') {
                $ManagerStatus = 'Missing'
            } else {
                $ManagerStatus = 'Not available'
            }
            $Manager = $null
            $ManagerSamAccountName = $null
            $ManagerEmail = $null
            $ManagerEnabled = $null
            $ManagerLastLogon = $null
            $ManagerLastLogonDays = $null
        }


        if ($OverwriteEmailProperty) {
            # fix this for a user
            $EmailTemp = $User.$OverwriteEmailProperty
            if ($EmailTemp -like '*@*') {
                $EmailAddress = $EmailTemp
            } else {
                $EmailAddress = $User.EmailAddress
            }
            # Fix this for manager as well
            if ($Cache["$($User.Manager)"]) {
                if ($Cache["$($User.Manager)"].$OverwriteEmailProperty -like '*@*') {
                    # $UserManager.Mail = $UserManager.$OverwriteEmailProperty
                    $ManagerEmail = $Cache["$($User.Manager)"].$OverwriteEmailProperty
                }
            }
        } else {
            $EmailAddress = $User.EmailAddress
        }

        if ($User.PasswordLastSet) {
            $PasswordDays = (New-TimeSpan -Start ($User.PasswordLastSet) -End ($Today)).Days
        }

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

        $UserAccountControl = Convert-UserAccountControl -UserAccountControl $User.UserAccountControl
        if ($UserAccountControl -contains 'INTERDOMAIN_TRUST_ACCOUNT') {
            continue
        }

        $MyUser = [ordered] @{
            UserPrincipalName     = $User.UserPrincipalName
            SamAccountName        = $User.SamAccountName
            Domain                = ConvertFrom-DistinguishedName -DistinguishedName $User.DistinguishedName -ToDomainCN
            Enabled               = $User.Enabled
            EmailAddress          = $EmailAddress
            DateExpiry            = $DateExpiry
            DaysToExpire          = $DaysToExpire
            PasswordExpired       = $User.PasswordExpired
            PasswordDays          = $PasswordDays
            PasswordLastSet       = $User.PasswordLastSet
            PasswordNotRequired   = $User.PasswordNotRequired
            PasswordNeverExpires  = $PasswordNeverExpires
            #PasswordAtNextLogon = $null -eq $User.PasswordLastSet
            Manager               = $Manager
            ManagerSamAccountName = $ManagerSamAccountName
            ManagerEmail          = $ManagerEmail
            ManagerStatus         = $ManagerStatus
            ManagerLastLogonDays  = $ManagerLastLogonDays
            DisplayName           = $User.DisplayName
            GivenName             = $User.GivenName
            Surname               = $User.Surname
            OrganizationalUnit    = ConvertFrom-DistinguishedName -DistinguishedName $User.DistinguishedName -ToOrganizationalUnit
            MemberOf              = $User.MemberOf
            DistinguishedName     = $User.DistinguishedName
            ManagerDN             = $User.Manager
        }
        foreach ($Property in $ConditionProperties) {
            $MyUser["$Property"] = $User.$Property
        }
        #[PSCustomObject] $MyUser

        $CachedUsers["$($User.DistinguishedName)"] = [PSCustomObject] $MyUser
    }
    if ($AsHashTable) {
        $CachedUsers
    } else {
        $CachedUsers.Values
    }
}
function Find-PasswordNotification {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $SearchPath,
        [System.Collections.IDictionary] $DisplayConsole,
        [switch] $Manager
    )
    #$Today = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    # Lets define Write-Color rules
    if ($null -eq $DisplayConsole) {
        $WriteParameters = @{
            ShowTime   = $true
            LogFile    = ""
            TimeFormat = "yyyy-MM-dd HH:mm:ss"
        }
    } else {
        $WriteParameters = $DisplayConsole
    }

    if ($SearchPath) {
        if (Test-Path -LiteralPath $SearchPath) {
            try {
                $SummarySearch = Import-Clixml -LiteralPath $SearchPath -ErrorAction Stop
                #$SummarySearch = Get-Content -LiteralPath $SearchPath -Raw | ConvertFrom-Json
            } catch {
                Write-Color @WriteParameters -Text "[e]", " Couldn't load the file $SearchPath", ". Skipping...", $_.Exception.Message -Color White, Yellow, White, Yellow, White, Yellow, White
            }
            if ($SummarySearch -and $Manager) {
                $SummarySearch.EmailEscalations.Values
            } elseif ($SummarySearch -and $Manager -eq $false) {
                $SummarySearch.EmailSent.Values
            }
        }
    }
}
function Send-PasswordEmail {
    [CmdletBinding()]
    param(
        [scriptblock] $Template,
        [PSCustomObject] $User,
        [Array] $ManagedUsers,
        [Array] $ManagedUsersManagerNotCompliant,
        #[Array] $ManagedUsersManagerDisabled,
        #[Array] $ManagedUsersManagerMissing,
        #[Array] $ManagedUsersManagerMissingEmail,
        [System.Collections.IDictionary] $EmailParameters,
        [string] $Subject
    )

    if ($Template) {
        $SourceParameters = [ordered] @{
            ManagerDisplayName                   = $User.DisplayName
            ManagerUsersTable                    = $ManagedUsers
            ManagerUsersTableManagerNotCompliant = $ManagedUsersManagerNotCompliant
            #ManagerUsersTableManagerDisabled = $ManagedUsersManagerDisabled
            #ManagerUsersTableManagerMissing = $ManagedUsersManagerMissing
            #ManagerUsersTableManagerNoEmail = $ManagedUsersManagerMissingEmail
            UserPrincipalName                    = $User.UserPrincipalName     # : adm.pklys@ad.evotec.xyz
            SamAccountName                       = $User.SamAccountName        # : adm.pklys
            Domain                               = $User.Domain                # : ad.evotec.xyz
            Enabled                              = $User.Enabled
            EmailAddress                         = $User.EmailAddress          # :
            DateExpiry                           = $User.DateExpiry            # :
            DaysToExpire                         = $User.DaysToExpire          # :
            PasswordExpired                      = $User.PasswordExpired       # : False
            PasswordLastSet                      = $User.PasswordLastSet       # : 05.09.2020 11:07:29
            PasswordNotRequired                  = $User.PasswordNotRequired   # : False
            PasswordNeverExpires                 = $User.PasswordNeverExpires  # : True
            ManagerSamAccountName                = $User.ManagerSamAccountName # : przemyslaw.klys
            ManagerEmail                         = $User.ManagerEmail          # : przemyslaw.klys@evotec.pl
            ManagerStatus                        = $User.ManagerStatus         # : Enabled
            ManagerLastLogonDays                 = $User.ManagerLastLogonDays  # : 0
            Manager                              = $User.Manager               # : PrzemysÅ‚aw KÅ‚ys
            DisplayName                          = $User.DisplayName           # : Administrator PrzemysÅ‚aw KÅ‚ys
            GivenName                            = $User.GivenName             # : Administrator PrzemysÅ‚aw
            Surname                              = $User.Surname               # : KÅ‚ys
            OrganizationalUnit                   = $User.OrganizationalUnit    # : OU=Special,OU=Accounts,OU=Production,DC=ad,DC=evotec,DC=xyz
            MemberOf                             = $User.MemberOf              # : {CN=GDS-TestGroup4,OU=Security,OU=Groups,OU=Production,DC=ad,DC=evotec,DC=xyz, CN=GDS-TestGroup2,OU=Security,OU=Groups,OU=Production,DC=ad,DC=evotec,DC=xyz, CN=Domain Admins,CN=Users,DC=ad,DC=evotec,DC=xyz}
            DistinguishedName                    = $User.DistinguishedName     # : CN=Administrator PrzemysÅ‚aw KÅ‚ys,OU=Special,OU=Accounts,OU=Production,DC=ad,DC=evotec,DC=xyz
            ManagerDN                            = $User.ManagerDN             # : CN=PrzemysÅ‚aw KÅ‚ys,OU=Users,OU=Accounts,OU=Production,DC=ad,DC=evotec,DC=xyz
        }
        $Body = EmailBody -EmailBody $Template -Parameter $SourceParameters

        # Below command would require to define variables as they are used in scriptblock
        #$EmailParameters.Subject = $ExecutionContext.InvokeCommand.ExpandString($Subject)
        # following replacement is a bit more cumbersome the the one above but a bit more secure and doesn't require creating 20+ unused variables
        $EmailParameters.Subject = Add-ParametersToString -String $Subject -Parameter $SourceParameters
        $EmailParameters.Body = $Body

        Send-EmailMessage @EmailParameters
    }
}
function Start-PasswordSolution {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $EmailParameters,
        [System.Collections.IDictionary] $ConfigurationParameters,
        [string] $OverwriteEmailProperty,
        [System.Collections.IDictionary] $UserSection,
        [System.Collections.IDictionary] $ManagerSection,
        [System.Collections.IDictionary] $AdminSection,
        [Array] $Rules,
        [scriptblock] $TemplatePreExpiry,
        [string] $TemplatePreExpirySubject,
        [scriptblock] $TemplatePostExpiry,
        [string] $TemplatePostExpirySubject,
        [scriptblock] $TemplateManager,
        [string] $TemplateManagerSubject,
        [scriptblock] $TemplateManagerNotCompliant,
        [string] $TemplateManagerNotCompliantSubject,
        [System.Collections.IDictionary] $DisplayConsole,
        [System.Collections.IDictionary] $HTMLOptions,
        [string] $FilePath,
        [string] $SearchPath
    )
    $Today = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    # Lets define Write-Color rules
    if ($null -eq $DisplayConsole) {
        $WriteParameters = @{
            ShowTime   = $true
            LogFile    = ""
            TimeFormat = "yyyy-MM-dd HH:mm:ss"
        }
    } else {
        $WriteParameters = $DisplayConsole
    }

    if ($SearchPath) {
        if (Test-Path -LiteralPath $SearchPath) {
            try {
                $SummarySearch = Import-Clixml -LiteralPath $SearchPath -ErrorAction Stop
                #$SummarySearch = Get-Content -LiteralPath $SearchPath -Raw | ConvertFrom-Json
            } catch {
                Write-Color @WriteParameters -Text "[e]", " Couldn't load the file $SearchPath", ". Skipping...", $_.Exception.Message -Color White, Yellow, White, Yellow, White, Yellow, White
            }
        }
    }
    if (-not $SummarySearch) {
        $SummarySearch = [ordered] @{
            EmailSent        = [ordered] @{

            }
            EmailEscalations = [ordered] @{

            }
        }
    }

    $Summary = [ordered] @{}
    $Summary['Notify'] = [ordered] @{}
    $Summary['NotifyManager'] = [ordered] @{}
    $Summary['Rules'] = [ordered] @{}

    $CachedUsers = Find-Password -AsHashTable -OverwriteEmailProperty $OverwriteEmailProperty
    foreach ($Rule in $Rules) {
        # Go for each rule and check if the user is in any of those rules
        if ($Rule.Enable -eq $true) {
            Write-Color @WriteParameters -Text "[i]", " Processing rule ", $Rule.Name, ' status: ', $Rule.Enable -Color Yellow, White, Green, White, Green, White, Green, White
            # Lets create summary for the rule
            if (-not $Summary['Rules'][$Rule.Name] ) {
                $Summary['Rules'][$Rule.Name] = [ordered] @{}
            }
            # this will make sure to expand array of multiple arrays of ints if provided
            # for example: (-150..-100),(-60..0), 1, 2, 3
            $Rule.Reminders = $Rule.Reminders | ForEach-Object { $_ }
            foreach ($User in $CachedUsers.Values) {
                if ($User.Enabled -eq $false) {
                    # We don't want to have disabled users
                    continue
                }
                if ($Rule.LimitOU.Count -gt 0) {
                    # Rule defined that only user withi specific OU has to be found
                    $FoundOU = $false
                    foreach ($OU in $Rule.LimitOU) {
                        if ($User.OrganizationalUnit -like $OU) {
                            $FoundOU = $true
                            break
                        }
                    }
                    if (-not $FoundOU) {
                        continue
                    }
                }
                if ($Rule.LimitGroup.Count -gt 0) {
                    # Rule defined that only user withi specific group has to be found
                    $FoundGroup = $false
                    foreach ($Group in $Rule.LimitGroup) {
                        if ($User.MemberOf -contains $Group) {
                            $FoundGroup = $true
                            break
                        }
                    }
                    if (-not $FoundGroup) {
                        continue
                    }
                }

                if ($Summary['Notify'][$User.DistinguishedName]) {
                    # User already exists in the notifications - rules are overlapping, we only take the first one
                    continue
                }


                if ($Rule.PasswordNeverExpires -eq $true) {
                    $DaysToPasswordExpiry = $Rule.PasswordNeverExpiresDays - $User.PasswordDays
                    $User.DaysToExpire = $DaysToPasswordExpiry
                }

                # Lets find users that expire
                if ($User.DaysToExpire -in $Rule.Reminders) {
                    Write-Color @WriteParameters -Text "[i]", " User ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire -Color Yellow, White, Yellow, White, Yellow, White, White, Blue
                    $Summary['Notify'][$User.DistinguishedName] = [ordered] @{
                        User = $User
                        Rule = $Rule
                    }
                    $Summary['Rules'][$Rule.Name][$User.DistinguishedName] = [ordered] @{
                        User = $User
                        Rule = $Rule
                    }

                    if ($Rule.SendToManager) {
                        if ($Rule.SendToManager.Manager -and $Rule.SendToManager.Manager.Enable -eq $true -and $User.ManagerStatus -eq 'Enabled' -and $User.ManagerEmail -like "*@*") {
                            # Manager is enabled and has an email, this is standard situation for manager in AD
                            # But before we go and do that, maybe user wants to send emails to managers if those users are in specific group or OU
                            if ($Rule.SendToManager.Manager.LimitOU.Count -gt 0) {
                                # Rule defined that only user withi specific OU has to be found
                                $FoundOU = $false
                                foreach ($OU in $Rule.SendToManager.Manager.LimitOU) {
                                    if ($User.OrganizationalUnit -like $OU) {
                                        $FoundOU = $true
                                        break
                                    }
                                }
                                if (-not $FoundOU) {
                                    continue
                                }
                            }
                            if ($Rule.SendToManager.Manager.LimitGroup.Count -gt 0) {
                                # Rule defined that only user withi specific group has to be found
                                $FoundGroup = $false
                                foreach ($Group in $Rule.SendToManager.Manager.LimitGroup) {
                                    if ($User.MemberOf -contains $Group) {
                                        $FoundGroup = $true
                                        break
                                    }
                                }
                                if (-not $FoundGroup) {
                                    continue
                                }
                            }
                            $Splat = [ordered] @{
                                SummaryDictionary = $Summary['NotifyManager']
                                Type              = 'ManagerDefault'
                                ManagerType       = 'Ok'
                                Key               = $User.ManagerDN
                                User              = $User
                                Rule              = $Rule
                                #Enabled = $true
                            }
                            Add-ManagerInformation @Splat
                        } else {
                            # Not compliant (missing, disabled, no email), covers all the below options
                            if ($Rule.SendToManager.ManagerNotCompliant -and $Rule.SendToManager.ManagerNotCompliant.Enable -and $Rule.SendToManager.ManagerNotCompliant.Manager) {

                                # But before we go and do that, maybe user wants to send emails to managers if those users are in specific group or OU
                                if ($Rule.SendToManager.ManagerNotCompliant.LimitOU.Count -gt 0) {
                                    # Rule defined that only user withi specific OU has to be found
                                    $FoundOU = $false
                                    foreach ($OU in $Rule.SendToManager.Manager.LimitOU) {
                                        if ($User.OrganizationalUnit -like $OU) {
                                            $FoundOU = $true
                                            break
                                        }
                                    }
                                    if (-not $FoundOU) {
                                        continue
                                    }
                                }
                                if ($Rule.SendToManager.ManagerNotCompliant.LimitGroup.Count -gt 0) {
                                    # Rule defined that only user withi specific group has to be found
                                    $FoundGroup = $false
                                    foreach ($Group in $Rule.SendToManager.Manager.LimitGroup) {
                                        if ($User.MemberOf -contains $Group) {
                                            $FoundGroup = $true
                                            break
                                        }
                                    }
                                    if (-not $FoundGroup) {
                                        continue
                                    }
                                }


                                if ($Rule.SendToManager.ManagerNotCompliant.MissingEmail -and $User.ManagerStatus -eq 'Enabled') {
                                    # Manager is enabled but missing email
                                    $Splat = [ordered] @{
                                        SummaryDictionary = $Summary['NotifyManager']
                                        Type              = 'ManagerNotCompliant'
                                        ManagerType       = 'No email'

                                        Key               = $Rule.SendToManager.ManagerNotCompliant.Manager
                                        User              = $User
                                        Rule              = $Rule

                                    }
                                    Add-ManagerInformation @Splat
                                } elseif ($Rule.SendToManager.ManagerNotCompliant.Disabled -and $User.ManagerStatus -eq 'Disabled') {
                                    # Manager is disabled, regardless if he/she has email
                                    $Splat = [ordered] @{
                                        SummaryDictionary = $Summary['NotifyManager']
                                        Type              = 'ManagerNotCompliant'
                                        ManagerType       = 'Manager disabled'
                                        Key               = $Rule.SendToManager.ManagerNotCompliant.Manager
                                        User              = $User
                                        Rule              = $Rule

                                    }
                                    Add-ManagerInformation @Splat
                                } elseif ($Rule.SendToManager.ManagerNotCompliant.LastLogon -and $User.ManagerLastLogonDays -ge $Rule.SendToManager.ManagerNotCompliant.LastLogonDays) {
                                    # Manager Last Logon over X days
                                    $Splat = [ordered] @{
                                        SummaryDictionary = $Summary['NotifyManager']
                                        Type              = 'ManagerNotCompliant'
                                        ManagerType       = 'Manager not logging in'
                                        Key               = $Rule.SendToManager.ManagerNotCompliant.Manager
                                        User              = $User
                                        Rule              = $Rule

                                    }
                                    Add-ManagerInformation @Splat
                                } elseif ($Rule.SendToManager.ManagerNotCompliant.Missing -and $User.ManagerStatus -eq 'Missing') {
                                    # Manager is missing
                                    $Splat = [ordered] @{
                                        SummaryDictionary = $Summary['NotifyManager']
                                        Type              = 'ManagerNotCompliant'
                                        ManagerType       = 'Manager not set'
                                        Key               = $Rule.SendToManager.ManagerNotCompliant.Manager
                                        User              = $User
                                        Rule              = $Rule

                                    }
                                    Add-ManagerInformation @Splat
                                }
                            }
                        }
                    }
                }
            }
        } else {
            Write-Color @WriteParameters -Text "[i]", " Processing rule ", $Rule.Name, ' status: ', $Rule.Enable -Color Red, White, Red, White, Red, White, Red, White
        }
    }

    if ($UserSection.Enable) {
        Write-Color @WriteParameters -Text "[i] Sending notifications to users " -Color White, Yellow, White, Yellow, White, Yellow, White
        $CountUsers = 0
        [Array] $SummaryUsersEmails = foreach ($Notify in $Summary['Notify'].Values) {
            $CountUsers++
            $EmailSplat = [ordered] @{}

            if ($Notify.User.DaysToExpire -ge 0) {
                if ($Notify.Rule.TemplatePreExpiry) {
                    # User uses template per rule
                    $EmailSplat.Template = $Notify.Rule.TemplatePreExpiry
                } elseif ($TemplatePreExpiry) {
                    # User uses global template
                    $EmailSplat.Template = $TemplatePreExpiry
                } else {
                    # User uses built-in template
                    $EmailSplat.Template = {

                    }
                }
                if ($Notify.Rule.TemplatePreExpirySubject) {
                    $EmailSplat.Subject = $Notify.Rule.TemplatePreExpirySubject
                } elseif ($TemplatePreExpirySubject) {
                    $EmailSplat.Subject = $TemplatePreExpirySubject
                } else {
                    $EmailSplat.Subject = '[Password] Your password will expire on $DateExpiry ($TimeToExpire days)'
                }
            } else {
                if ($Notify.Rule.TemplatePostExpiry) {
                    $EmailSplat.Template = $Notify.Rule.TemplatePostExpiry
                } elseif ($TemplatePostExpiry) {
                    $EmailSplat.Template = $TemplatePostExpiry
                } else {
                    $EmailSplat.Template = {

                    }
                }
                if ($Notify.Rule.TemplatePostExpirySubject) {
                    $EmailSplat.Subject = $Notify.Rule.TemplatePostExpirySubject
                } elseif ($TemplatePostExpirySubject) {
                    $EmailSplat.Subject = $TemplatePostExpirySubject
                } else {
                    $EmailSplat.Subject = '[Password] Your password expired on $DateExpiry ($TimeToExpire days ago)'
                }
            }
            $EmailSplat.User = $Notify.User
            $EmailSplat.EmailParameters = $EmailParameters

            if ($UserSection.SendToDefaultEmail -ne $true) {
                $EmailSplat.EmailParameters.To = $Notify.User.EmailAddress
            }
            if ($Notify.User.EmailAddress -like "*@*") {
                # Regardless if we send email to default email or to user, if user doesn't have email address we shouldn't send an email
                $EmailResult = Send-PasswordEmail @EmailSplat
                [PSCustomObject] @{
                    UserPrincipalName    = $EmailSplat.User.UserPrincipalName
                    SamAccountName       = $EmailSplat.User.SamAccountName
                    Domain               = $EmailSplat.User.Domain
                    Rule                 = $Notify.Rule.Name
                    Status               = $EmailResult.Status
                    StatusWhen           = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
                    StatusError          = $EmailResult.Error
                    SentTo               = $EmailResult.SentTo
                    DateExpiry           = $EmailSplat.User.DateExpiry
                    DaysToExpire         = $EmailSplat.User.DaysToExpire
                    PasswordExpired      = $EmailSplat.User.PasswordExpired
                    PasswordNeverExpires = $EmailSplat.User.PasswordNeverExpires
                    PasswordLastSet      = $EmailSplat.User.PasswordLastSet
                }
                if ($UserSection.SendCountMaximum -gt 0) {
                    if ($UserSection.SendCountMaximum -le $CountUsers) {
                        Write-Color @WriteParameters -Text "[i]", " Send count maximum reached. There may be more accounts that match the rule." -Color Red, DarkMagenta
                        break
                    }
                }
            } else {
                # Email not sent
                $EmailResult = @{
                    Status = $false
                    Error  = 'No email address for user'
                    SentTo = ''
                }
                [PSCustomObject] @{
                    UserPrincipalName    = $EmailSplat.User.UserPrincipalName
                    SamAccountName       = $EmailSplat.User.SamAccountName
                    Domain               = $EmailSplat.User.Domain
                    Rule                 = $Notify.Rule.Name
                    Status               = $EmailResult.Status
                    StatusWhen           = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
                    StatusError          = $EmailResult.Error
                    SentTo               = $EmailResult.SentTo
                    DateExpiry           = $EmailSplat.User.DateExpiry
                    DaysToExpire         = $EmailSplat.User.DaysToExpire
                    PasswordExpired      = $EmailSplat.User.PasswordExpired
                    PasswordNeverExpires = $EmailSplat.User.PasswordNeverExpires
                    PasswordLastSet      = $EmailSplat.User.PasswordLastSet
                }
            }
            if ($EmailResult.Status -eq $true) {
                Write-Color @WriteParameters -Text "[i]", " Sending ", $Notify.User.DisplayName, " (", $Notify.User.UserPrincipalName, ")", " status: ", $EmailResult.Status, ", details: ", $EmailResult.Error -Color Yellow, White, Yellow, White, Yellow, White, White, Blue, White, Blue
            } else {
                Write-Color @WriteParameters -Text "[i]", " Sending ", $Notify.User.DisplayName, " (", $Notify.User.UserPrincipalName, ")", " status: ", $EmailResult.Status, ", details: ", $EmailResult.Error -Color Yellow, White, Yellow, White, Yellow, White, White, Red, White, Red
            }
        }
        Write-Color @WriteParameters -Text "[i] Sending notifications to users (sent: ", $SummaryUsersEmails.Count, " out of ", $Summary['Notify'].Values.Count, ")" -Color White, Yellow, White, Yellow, White, Yellow, White
    } else {
        Write-Color @WriteParameters -Text "[i] Sending notifications to users is ", "disabled!" -Color White, Yellow, DarkMagenta
    }
    if ($ManagerSection.Enable) {
        Write-Color @WriteParameters -Text "[i] Sending notifications to managers " -Color White, Yellow, White, Yellow, White, Yellow, White
        $CountManagers = 0
        [Array] $SummaryManagersEmails = foreach ($Manager in $Summary['NotifyManager'].Keys) {
            $CountManagers++
            if ($CachedUsers[$Manager]) {
                # This user is "findable" in AD
                $ManagerUser = $CachedUsers[$Manager]
            } else {
                # This user is provided by user in config file
                $ManagerUser = $Summary['NotifyManager'][$Manager]['Manager']
            }
            [Array] $ManagedUsers = $Summary['NotifyManager'][$Manager]['ManagerDefault'].Values.Output
            [Array] $ManagedUsersManagerNotCompliant = $Summary['NotifyManager'][$Manager]['ManagerNotCompliant'].Values.Output

            $EmailSplat = [ordered] @{}

            if ($Summary['NotifyManager'][$Manager].ManagerDefault.Count -gt 0) {
                if ($TemplateManager) {
                    # User uses global template
                    $EmailSplat.Template = $TemplateManager
                } else {
                    # User uses built-in template
                    $EmailSplat.Template = {

                    }
                }
                if ($TemplateManagerSubject) {
                    $EmailSplat.Subject = $TemplateManagerSubject
                } else {
                    $EmailSplat.Subject = "[Password Expiring] Dear Manager - Your accounts are expiring!"
                }
            } elseif ($Summary['NotifyManager'][$Manager].ManagerNotCompliant.Count -gt 0) {
                if ($TemplateManagerNotCompliant) {
                    # User uses global template
                    $EmailSplat.Template = $TemplateManagerNotCompliant
                } else {
                    # User uses built-in template
                    $EmailSplat.Template = {

                    }
                }
                if ($TemplateManagerNotCompliantSubject) {
                    $EmailSplat.Subject = $TemplateManagerNotCompliantSubject
                } else {
                    $EmailSplat.Subject = "[Password Escalation] Accounts are expiring with non-compliant manager"
                }
            }

            $EmailSplat.User = $ManagerUser
            $EmailSplat.ManagedUsers = $ManagedUsers
            $EmailSplat.ManagedUsersManagerNotCompliant = $ManagedUsersManagerNotCompliant
            #$EmailSplat.ManagedUsersManagerDisabled = $ManagedUsersManagerDisabled
            #$EmailSplat.ManagedUsersManagerMissing = $ManagedUsersManagerMissing
            #$EmailSplat.ManagedUsersManagerMissingEmail = $ManagedUsersManagerMissingEmail
            $EmailSplat.EmailParameters = $EmailParameters

            if ($ManagerSection.SendToDefaultEmail -ne $true) {
                $EmailSplat.EmailParameters.To = $ManagerUser.EmailAddress
            }

            $EmailResult = Send-PasswordEmail @EmailSplat
            [PSCustomObject] @{
                DisplayName              = $ManagerUser.DisplayName
                SamAccountName           = $ManagerUser.SamAccountName
                Domain                   = $ManagerUser.Domain
                Status                   = $EmailResult.Status
                StatusWhen               = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
                SentTo                   = $EmailResult.SentTo
                StatusError              = $EmailResult.Error
                Accounts                 = $ManagedUsers.SamAccountName
                AccountsCount            = $ManagedUsers.Count
                Template                 = 'Unknown'
                ManagerNotCompliant      = $ManagedUsersManagerNotCompliant.SamAccountName
                ManagerNotCompliantCount = $ManagedUsersManagerNotCompliant.Count
                #ManagerDisabled = $ManagedUsersManagerDisabled.SamAccountName
                #ManagerDisabledCount = $ManagedUsersManagerDisabled.Count
                #ManagerMissing = $ManagedUsersManagerMissing.SamAccountName
                #ManagerMissingCount = $ManagedUsersManagerMissing.Count
                #ManagerMissingEmail = $ManagedUsersManagerMissingEmail.SamAccountName
                #ManagerMissingEmailCount = $ManagedUsersManagerMissingEmail.Count
            }
            if ($ManagerSection.SendCountMaximum -gt 0) {
                if ($ManagerSection.SendCountMaximum -le $CountManagers) {
                    Write-Color @WriteParameters -Text "[i]", " Send count maximum reached. There may be more managers that match the rule." -Color Red, DarkMagenta
                    break
                }
            }
        }
        Write-Color @WriteParameters -Text "[i] Sending notifications to managers (sent: ", $SummaryManagersEmails.Count, " out of ", $Summary['NotifyManager'].Values.Count, ")" -Color White, Yellow, White, Yellow, White, Yellow, White
        #Write-Color @WriteParameters -Text "[i] Sending notifications to managers (sent: ", $SummaryManagersEmails.Count, ")" -Color White, Yellow, White, Yellow, White, Yellow, White
    } else {
        Write-Color @WriteParameters -Text "[i] Sending notifications to managers is ", "disabled!" -Color White, Yellow, DarkMagenta
    }

    # Create report
    New-HTML {
        New-TableOption -DataStore JavaScript -ArrayJoin -BoolAsString
        New-HTMLTab -Name 'All Users' {
            New-HTMLTable -DataTable $CachedUsers.Values -Filtering {
                New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string
                New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string
            }
        }
        foreach ($Rule in  $Summary['Rules'].Keys) {
            New-HTMLTab -Name $Rule {
                New-HTMLTable -DataTable $Summary['Rules'][$Rule].Values.User -Filtering {
                    New-TableCondition -Name 'Enabled' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string
                    New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string
                    New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string
                }
            }
        }
        New-HTMLTab -Name 'Email sent to users' {
            New-HTMLTable -DataTable $SummaryUsersEmails {
                New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary'
                New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo'
                New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string
                New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string
            }
        }
        New-HTMLTab -Name 'Email sent to manager' {
            New-HTMLTable -DataTable $SummaryManagersEmails {
                New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary'
                New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo'
            }
        }
    } -ShowHTML:$HTMLOptions.ShowHTML -FilePath $FilePath -Online:$HTMLOptions.Online

    if ($SearchPath) {

        $SummarySearch['EmailSent'][$Today] = $SummaryUsersEmails
        $SummarySearch['EmailEscalations'][$Today] = $SummaryManagersEmails

        try {
            $SummarySearch | Export-Clixml -LiteralPath $SearchPath
            #$SummarySearch | ConvertTo-Json | Out-File -LiteralPath $SearchPath
        } catch {
            Write-Color @WriteParameters -Text "[e]", " Couldn't save to file $SearchPath", ". Error: ", $_.Exception.Message -Color White, Yellow, White, Yellow, White, Yellow, White
        }
    }
}



Export-ModuleMember -Function @('Find-Password', 'Find-PasswordNotification', 'Send-PasswordEmail', 'Start-PasswordSolution') -Alias @()
# SIG # Begin signature block
# MIIdWQYJKoZIhvcNAQcCoIIdSjCCHUYCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQU9zyTJDj+XAnb+3dRRbQ9F6xS
# 1eKgghhnMIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0B
# AQUFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD
# VQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVk
# IElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQsw
# CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
# ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg
# Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg
# +XESpa7cJpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lT
# XDGEKvYPmDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5
# a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g
# 0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1
# roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf
# GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0G
# A1UdDgQWBBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLL
# gjEtUYunpyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3
# cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmr
# EthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+
# fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5Q
# Z7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu
# 838fYxAe+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw
# 8jCCBP4wggPmoAMCAQICEA1CSuC+Ooj/YEAhzhQA8N0wDQYJKoZIhvcNAQELBQAw
# cjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ
# d3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVk
# IElEIFRpbWVzdGFtcGluZyBDQTAeFw0yMTAxMDEwMDAwMDBaFw0zMTAxMDYwMDAw
# MDBaMEgxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjEgMB4G
# A1UEAxMXRGlnaUNlcnQgVGltZXN0YW1wIDIwMjEwggEiMA0GCSqGSIb3DQEBAQUA
# A4IBDwAwggEKAoIBAQDC5mGEZ8WK9Q0IpEXKY2tR1zoRQr0KdXVNlLQMULUmEP4d
# yG+RawyW5xpcSO9E5b+bYc0VkWJauP9nC5xj/TZqgfop+N0rcIXeAhjzeG28ffnH
# bQk9vmp2h+mKvfiEXR52yeTGdnY6U9HR01o2j8aj4S8bOrdh1nPsTm0zinxdRS1L
# sVDmQTo3VobckyON91Al6GTm3dOPL1e1hyDrDo4s1SPa9E14RuMDgzEpSlwMMYpK
# jIjF9zBa+RSvFV9sQ0kJ/SYjU/aNY+gaq1uxHTDCm2mCtNv8VlS8H6GHq756Wwog
# L0sJyZWnjbL61mOLTqVyHO6fegFz+BnW/g1JhL0BAgMBAAGjggG4MIIBtDAOBgNV
# HQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcD
# CDBBBgNVHSAEOjA4MDYGCWCGSAGG/WwHATApMCcGCCsGAQUFBwIBFhtodHRwOi8v
# d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwHwYDVR0jBBgwFoAU9LbhIB3+Ka7S5GGlsqIl
# ssgXNW4wHQYDVR0OBBYEFDZEho6kurBmvrwoLR1ENt3janq8MHEGA1UdHwRqMGgw
# MqAwoC6GLGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtdHMu
# Y3JsMDKgMKAuhixodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc2hhMi1hc3N1cmVk
# LXRzLmNybDCBhQYIKwYBBQUHAQEEeTB3MCQGCCsGAQUFBzABhhhodHRwOi8vb2Nz
# cC5kaWdpY2VydC5jb20wTwYIKwYBBQUHMAKGQ2h0dHA6Ly9jYWNlcnRzLmRpZ2lj
# ZXJ0LmNvbS9EaWdpQ2VydFNIQTJBc3N1cmVkSURUaW1lc3RhbXBpbmdDQS5jcnQw
# DQYJKoZIhvcNAQELBQADggEBAEgc3LXpmiO85xrnIA6OZ0b9QnJRdAojR6OrktIl
# xHBZvhSg5SeBpU0UFRkHefDRBMOG2Tu9/kQCZk3taaQP9rhwz2Lo9VFKeHk2eie3
# 8+dSn5On7UOee+e03UEiifuHokYDTvz0/rdkd2NfI1Jpg4L6GlPtkMyNoRdzDfTz
# ZTlwS/Oc1np72gy8PTLQG8v1Yfx1CAB2vIEO+MDhXM/EEXLnG2RJ2CKadRVC9S0y
# OIHa9GCiurRS+1zgYSQlT7LfySmoc0NR2r1j1h9bm/cuG08THfdKDXF+l7f0P4Tr
# weOjSaH6zqe/Vs+6WXZhiV9+p7SOZ3j5NpjhyyjaW4emii8wggUwMIIEGKADAgEC
# AhAECRgbX9W7ZnVTQ7VvlVAIMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVT
# MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
# b20xJDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xMzEw
# MjIxMjAwMDBaFw0yODEwMjIxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNV
# BAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwggEi
# MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD407Mcfw4Rr2d3B9MLMUkZz9D7
# RZmxOttE9X/lqJ3bMtdx6nadBS63j/qSQ8Cl+YnUNxnXtqrwnIal2CWsDnkoOn7p
# 0WfTxvspJ8fTeyOU5JEjlpB3gvmhhCNmElQzUHSxKCa7JGnCwlLyFGeKiUXULaGj
# 6YgsIJWuHEqHCN8M9eJNYBi+qsSyrnAxZjNxPqxwoqvOf+l8y5Kh5TsxHM/q8grk
# V7tKtel05iv+bMt+dDk2DZDv5LVOpKnqagqrhPOsZ061xPeM0SAlI+sIZD5SlsHy
# DxL0xY4PwaLoLFH3c7y9hbFig3NBggfkOItqcyDQD2RzPJ6fpjOp/RnfJZPRAgMB
# AAGjggHNMIIByTASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjAT
# BgNVHSUEDDAKBggrBgEFBQcDAzB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGG
# GGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2Nh
# Y2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCB
# gQYDVR0fBHoweDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lD
# ZXJ0QXNzdXJlZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNl
# cnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDBPBgNVHSAESDBGMDgG
# CmCGSAGG/WwAAgQwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu
# Y29tL0NQUzAKBghghkgBhv1sAzAdBgNVHQ4EFgQUWsS5eyoKo6XqcQPAYPkt9mV1
# DlgwHwYDVR0jBBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wDQYJKoZIhvcNAQEL
# BQADggEBAD7sDVoks/Mi0RXILHwlKXaoHV0cLToaxO8wYdd+C2D9wz0PxK+L/e8q
# 3yBVN7Dh9tGSdQ9RtG6ljlriXiSBThCk7j9xjmMOE0ut119EefM2FAaK95xGTlz/
# kLEbBw6RFfu6r7VRwo0kriTGxycqoSkoGjpxKAI8LpGjwCUR4pwUR6F6aGivm6dc
# IFzZcbEMj7uo+MUSaJ/PQMtARKUT8OZkDCUIQjKyNookAv4vcn4c10lFluhZHen6
# dGRrsutmQ9qzsIzV6Q3d9gEgzpkxYz0IGhizgZtPxpMQBvwHgfqL2vmCSfdibqFT
# +hKUGIUukpHqaGxEMrJmoecYpJpkUe8wggUxMIIEGaADAgECAhAKoSXW1jIbfkHk
# Bdo2l8IVMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxE
# aWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMT
# G0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xNjAxMDcxMjAwMDBaFw0z
# MTAxMDcxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0
# IFNIQTIgQXNzdXJlZCBJRCBUaW1lc3RhbXBpbmcgQ0EwggEiMA0GCSqGSIb3DQEB
# AQUAA4IBDwAwggEKAoIBAQC90DLuS82Pf92puoKZxTlUKFe2I0rEDgdFM1EQfdD5
# fU1ofue2oPSNs4jkl79jIZCYvxO8V9PD4X4I1moUADj3Lh477sym9jJZ/l9lP+Cb
# 6+NGRwYaVX4LJ37AovWg4N4iPw7/fpX786O6Ij4YrBHk8JkDbTuFfAnT7l3ImgtU
# 46gJcWvgzyIQD3XPcXJOCq3fQDpct1HhoXkUxk0kIzBdvOw8YGqsLwfM/fDqR9mI
# UF79Zm5WYScpiYRR5oLnRlD9lCosp+R1PrqYD4R/nzEU1q3V8mTLex4F0IQZchfx
# FwbvPc3WTe8GQv2iUypPhR3EHTyvz9qsEPXdrKzpVv+TAgMBAAGjggHOMIIByjAd
# BgNVHQ4EFgQU9LbhIB3+Ka7S5GGlsqIlssgXNW4wHwYDVR0jBBgwFoAUReuir/SS
# y4IxLVGLp6chnfNtyA8wEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMC
# AYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUF
# BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6
# Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5j
# cnQwgYEGA1UdHwR6MHgwOqA4oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9E
# aWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwUAYDVR0gBEkw
# RzA4BgpghkgBhv1sAAIEMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2lj
# ZXJ0LmNvbS9DUFMwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEBCwUAA4IBAQBxlRLp
# UYdWac3v3dp8qmN6s3jPBjdAhO9LhL/KzwMC/cWnww4gQiyvd/MrHwwhWiq3BTQd
# aq6Z+CeiZr8JqmDfdqQ6kw/4stHYfBli6F6CJR7Euhx7LCHi1lssFDVDBGiy23UC
# 4HLHmNY8ZOUfSBAYX4k4YU1iRiSHY4yRUiyvKYnleB/WCxSlgNcSR3CzddWThZN+
# tpJn+1Nhiaj1a5bA9FhpDXzIAbG5KHW3mWOFIoxhynmUfln8jA/jb7UBJrZspe6H
# USHkWGCbugwtK22ixH67xCUrRwIIfEmuE7bhfEJCKMYYVs9BNLZmXbZ0e/VWMyIv
# IjayS6JKldj1po5SMIIFPTCCBCWgAwIBAgIQBNXcH0jqydhSALrNmpsqpzANBgkq
# hkiG9w0BAQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5j
# MRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBT
# SEEyIEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTIwMDYyNjAwMDAwMFoX
# DTIzMDcwNzEyMDAwMFowejELMAkGA1UEBhMCUEwxEjAQBgNVBAgMCcWabMSFc2tp
# ZTERMA8GA1UEBxMIS2F0b3dpY2UxITAfBgNVBAoMGFByemVteXPFgmF3IEvFgnlz
# IEVWT1RFQzEhMB8GA1UEAwwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMIIBIjAN
# BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7KB3iyBrhkLUbbFe9qxhKKPBYqD
# Bqlnr3AtpZplkiVjpi9dMZCchSeT5ODsShPuZCIxJp5I86uf8ibo3vi2S9F9AlfF
# jVye3dTz/9TmCuGH8JQt13ozf9niHecwKrstDVhVprgxi5v0XxY51c7zgMA2g1Ub
# +3tii0vi/OpmKXdL2keNqJ2neQ5cYly/GsI8CREUEq9SZijbdA8VrRF3SoDdsWGf
# 3tZZzO6nWn3TLYKQ5/bw5U445u/V80QSoykszHRivTj+H4s8ABiforhi0i76beA6
# Ea41zcH4zJuAp48B4UhjgRDNuq8IzLWK4dlvqrqCBHKqsnrF6BmBrv+BXQIDAQAB
# o4IBxTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0O
# BBYEFBixNSfoHFAgJk4JkDQLFLRNlJRmMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUE
# DDAKBggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdp
# Y2VydC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2Ny
# bDQuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUw
# QzA3BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNl
# cnQuY29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcw
# AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8v
# Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNp
# Z25pbmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAmr1s
# z4lsLARi4wG1eg0B8fVJFowtect7SnJUrp6XRnUG0/GI1wXiLIeow1UPiI6uDMsR
# XPHUF/+xjJw8SfIbwava2eXu7UoZKNh6dfgshcJmo0QNAJ5PIyy02/3fXjbUREHI
# NrTCvPVbPmV6kx4Kpd7KJrCo7ED18H/XTqWJHXa8va3MYLrbJetXpaEPpb6zk+l8
# Rj9yG4jBVRhenUBUUj3CLaWDSBpOA/+sx8/XB9W9opYfYGb+1TmbCkhUg7TB3gD6
# o6ESJre+fcnZnPVAPESmstwsT17caZ0bn7zETKlNHbc1q+Em9kyBjaQRcEQoQQNp
# ezQug9ufqExx6lHYDjGCBFwwggRYAgEBMIGGMHIxCzAJBgNVBAYTAlVTMRUwEwYD
# VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAv
# BgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EC
# EATV3B9I6snYUgC6zZqbKqcwCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcCAQwxCjAI
# oAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIB
# CzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFCM1Z5yXjY05NEY9zoF0
# d4axb1nbMA0GCSqGSIb3DQEBAQUABIIBAGbTOrU6F6h3N+Yytj0eBeAKZZTg1id1
# AnEtMqw7VvdPPoS1ZkPX6Ce+CHiVlqj3D17zD7ubftRJLJvrDSiEutE7xE7Wa6bo
# Qg0fFYXK4j2/UHKAfRFz60V/q7cOe3gruxbiHBEfXvlUNWp7SJ2tUuODgDNHXSwW
# Gm1FmA3lGTXqQezNIQnXIjncD5X3Soz0ss7ge/opgUSSuKbID3uQePOc32cib2ok
# QQqq7+sDUJ0QYQ7ZIsgaiovYzRq11BJQyIA23r6AKbAo5AEyteSTjFtV+touy7Vu
# cSkH1Qkj7qzdi0/rrSOtBVJ3L2ZzeUikCxTwavNNlMSbx2WiUN53RuqhggIwMIIC
# LAYJKoZIhvcNAQkGMYICHTCCAhkCAQEwgYYwcjELMAkGA1UEBhMCVVMxFTATBgNV
# BAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8G
# A1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIFRpbWVzdGFtcGluZyBDQQIQ
# DUJK4L46iP9gQCHOFADw3TANBglghkgBZQMEAgEFAKBpMBgGCSqGSIb3DQEJAzEL
# BgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIxMDYyMDE2MjYyN1owLwYJKoZI
# hvcNAQkEMSIEIGX2kLlqK2LdcBAn8CkIWEwESnmem094U2+qlLHMJXI+MA0GCSqG
# SIb3DQEBAQUABIIBAAgsbNpOhTfwXaQ9KesIb7dX/M/wu7Xu7NsGSZZWW7Vw2+Jj
# aHpCOeKmp5mdLojNX/Qn+OfjsXNtj2NFvyFtSQQfh6UI3cN/D6KX7E+nmRfyOPzA
# 1SSIkHaalOI+yC1zzvIx7rTjoAGVchiMq1DtuHDJVJQayLNWWOxw5VSZlCNhcTBY
# L7fzT45MJLvneUJ+M/cmyq9yj2zMjMJBt9AIQ+ji26WV02FCN4BrQGW9ROnmKqvq
# CB2loCgf7n4y84ynl224Np1ufosdMn+EOm5H+iUaHZww6UsRJa6+/nHRdDdP4tmb
# GK3Bdw4XUjU5jUuvLeVgM2o9zR5i/weFljXHmy8=
# SIG # End signature block