PasswordSolution.psm1

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

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

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

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

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

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

            $RegionInformation = [System.Globalization.RegionInfo]::new($Culture)
            $Script:QuickSearch[$RegionInformation.TwoLetterISORegionName] = @{
                'Culture'           = $Culture
                'RegionInformation' = $RegionInformation
            }
            $Script:QuickSearch[$RegionInformation.ThreeLetterISORegionName] = @{
                'Culture'           = $Culture
                'RegionInformation' = $RegionInformation
            }
        }
        if ($PSBoundParameters.ContainsKey('CountryCode')) {
            if ($All) {
                $Script:QuickSearch[$CountryCode]
            } else {
                $Script:QuickSearch[$CountryCode].RegionInformation.EnglishName
            }
        } else {
            $Script:QuickSearch
        }
    }
}
function Convert-CountryToContinent { 
    <#
    .SYNOPSIS
    Convert country to continent
 
    .DESCRIPTION
    Convert country to continent or return a hashtable of countries and their corresponding continent.
    If the country is not found (for example empty), it will return "Unknown"
 
    .PARAMETER Country
    Country to convert. If country is not given it will return a hashtable of countries and their corresponding continent.
 
    .EXAMPLE
    Convert-CountryToContinent -Country "Poland"
 
    .EXAMPLE
    Convert-CountryToContinent
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [string] $Country,
        [switch] $ReturnHashTable
    )
    $CountryToContinent = [ordered] @{
        "Afghanistan"                       = "Asia"
        "Albania"                           = "Europe"
        "Algeria"                           = "Africa"
        "Andorra"                           = "Europe"
        "Angola"                            = "Africa"
        "Antigua and Barbuda"               = "North America"
        "Argentina"                         = "South America"
        "Armenia"                           = "Asia"
        "Australia"                         = "Australia/Oceania"
        "Austria"                           = "Europe"
        "Azerbaijan"                        = "Asia"
        "Bahamas"                           = "North America"
        "Bahrain"                           = "Asia"
        "Bangladesh"                        = "Asia"
        "Barbados"                          = "North America"
        "Belarus"                           = "Europe"
        "Belgium"                           = "Europe"
        "Belize"                            = "North America"
        "Benin"                             = "Africa"
        "Bhutan"                            = "Asia"
        "Bolivia"                           = "South America"
        "Bosnia and Herzegovina"            = "Europe"
        "Botswana"                          = "Africa"
        "Brazil"                            = "South America"
        "Brunei"                            = "Asia"
        "Bulgaria"                          = "Europe"
        "Burkina Faso"                      = "Africa"
        "Burundi"                           = "Africa"
        "Cabo Verde"                        = "Africa"
        "Cambodia"                          = "Asia"
        "Cameroon"                          = "Africa"
        "Canada"                            = "North America"
        "Central African Republic"          = "Africa"
        "Chad"                              = "Africa"
        "Chile"                             = "South America"
        "China"                             = "Asia"
        "Colombia"                          = "South America"
        "Comoros"                           = "Africa"
        "Congo, Democratic Republic of the" = "Africa"
        "Congo, Republic of the"            = "Africa"
        "Costa Rica"                        = "North America"
        "Cote d'Ivoire"                     = "Africa"
        "Croatia"                           = "Europe"
        "Cuba"                              = "North America"
        "Cyprus"                            = "Asia"
        "Czechia"                           = "Europe"
        "Denmark"                           = "Europe"
        "Djibouti"                          = "Africa"
        "Dominica"                          = "North America"
        "Dominican Republic"                = "North America"
        "Ecuador"                           = "South America"
        "Egypt"                             = "Africa"
        "El Salvador"                       = "North America"
        "Equatorial Guinea"                 = "Africa"
        "Eritrea"                           = "Africa"
        "Estonia"                           = "Europe"
        "Eswatini"                          = "Africa"
        "Ethiopia"                          = "Africa"
        "Fiji"                              = "Australia/Oceania"
        "Finland"                           = "Europe"
        "France"                            = "Europe"
        "Gabon"                             = "Africa"
        "Gambia"                            = "Africa"
        "Georgia"                           = "Asia"
        "Germany"                           = "Europe"
        "Ghana"                             = "Africa"
        "Greece"                            = "Europe"
        "Grenada"                           = "North America"
        "Guatemala"                         = "North America"
        "Guinea"                            = "Africa"
        "Guinea-Bissau"                     = "Africa"
        "Guyana"                            = "South America"
        "Haiti"                             = "North America"
        "Honduras"                          = "North America"
        "Hungary"                           = "Europe"
        "Iceland"                           = "Europe"
        "India"                             = "Asia"
        "Indonesia"                         = "Asia"
        "Iran"                              = "Asia"
        "Iraq"                              = "Asia"
        "Ireland"                           = "Europe"
        "Israel"                            = "Asia"
        "Italy"                             = "Europe"
        "Jamaica"                           = "North America"
        "Japan"                             = "Asia"
        "Jordan"                            = "Asia"
        "Kazakhstan"                        = "Asia"
        "Kenya"                             = "Africa"
        "Kiribati"                          = "Australia/Oceania"
        "Kosovo"                            = "Europe"
        "Kuwait"                            = "Asia"
        "Kyrgyzstan"                        = "Asia"
        "Laos"                              = "Asia"
        "Latvia"                            = "Europe"
        "Lebanon"                           = "Asia"
        "Lesotho"                           = "Africa"
        "Liberia"                           = "Africa"
        "Libya"                             = "Africa"
        "Liechtenstein"                     = "Europe"
        "Lithuania"                         = "Europe"
        "Luxembourg"                        = "Europe"
        "Madagascar"                        = "Africa"
        "Malawi"                            = "Africa"
        "Malaysia"                          = "Asia"
        "Maldives"                          = "Asia"
        "Mali"                              = "Africa"
        "Malta"                             = "Europe"
        "Marshall Islands"                  = "Australia/Oceania"
        "Mauritania"                        = "Africa"
        "Mauritius"                         = "Africa"
        "Mexico"                            = "North America"
        "Micronesia"                        = "Australia/Oceania"
        "Moldova"                           = "Europe"
        "Monaco"                            = "Europe"
        "Mongolia"                          = "Asia"
        "Montenegro"                        = "Europe"
        "Morocco"                           = "Africa"
        "Mozambique"                        = "Africa"
        "Myanmar"                           = "Asia"
        "Namibia"                           = "Africa"
        "Nauru"                             = "Australia/Oceania"
        "Nepal"                             = "Asia"
        "Netherlands"                       = "Europe"
        "New Zealand"                       = "Australia/Oceania"
        "Nicaragua"                         = "North America"
        "Niger"                             = "Africa"
        "Nigeria"                           = "Africa"
        "North Korea"                       = "Asia"
        "North Macedonia"                   = "Europe"
        "Norway"                            = "Europe"
        "Oman"                              = "Asia"
        "Pakistan"                          = "Asia"
        "Palau"                             = "Australia/Oceania"
        "Panama"                            = "North America"
        "Papua New Guinea"                  = "Australia/Oceania"
        "Paraguay"                          = "South America"
        "Peru"                              = "South America"
        "Philippines"                       = "Asia"
        "Poland"                            = "Europe"
        "Portugal"                          = "Europe"
        "Qatar"                             = "Asia"
        "Romania"                           = "Europe"
        "Russia"                            = "Asia"
        "Rwanda"                            = "Africa"
        "Saint Kitts and Nevis"             = "North America"
        "Saint Lucia"                       = "North America"
        "Saint Vincent and the Grenadines"  = "North America"
        "Samoa"                             = "Australia/Oceania"
        "San Marino"                        = "Europe"
        "Sao Tome and Principe"             = "Africa"
        "Saudi Arabia"                      = "Asia"
        "Senegal"                           = "Africa"
        "Serbia"                            = "Europe"
        "Seychelles"                        = "Africa"
        "Sierra Leone"                      = "Africa"
        "Singapore"                         = "Asia"
        "Slovakia"                          = "Europe"
        "Slovenia"                          = "Europe"
        "Solomon Islands"                   = "Australia/Oceania"
        "Somalia"                           = "Africa"
        "South Africa"                      = "Africa"
        "South Korea"                       = "Asia"
        "South Sudan"                       = "Africa"
        "Spain"                             = "Europe"
        "Sri Lanka"                         = "Asia"
        "Sudan"                             = "Africa"
        "Suriname"                          = "South America"
        "Sweden"                            = "Europe"
        "Switzerland"                       = "Europe"
        "Syria"                             = "Asia"
        "Taiwan"                            = "Asia"
        "Tajikistan"                        = "Asia"
        "Tanzania"                          = "Africa"
        "Thailand"                          = "Asia"
        "Timor-Leste"                       = "Asia"
        "Togo"                              = "Africa"
        "Tonga"                             = "Australia/Oceania"
        "Trinidad and Tobago"               = "North America"
        "Tunisia"                           = "Africa"
        "Turkey"                            = "Asia"
        "Turkmenistan"                      = "Asia"
        "Tuvalu"                            = "Australia/Oceania"
        "Uganda"                            = "Africa"
        "Ukraine"                           = "Europe"
        "United Arab Emirates"              = "Asia"
        "United Kingdom"                    = "Europe"
        "United States of America"          = "North America"
        "Uruguay"                           = "South America"
        "Uzbekistan"                        = "Asia"
        "Vanuatu"                           = "Australia/Oceania"
        "Vatican City (Holy See)"           = "Europe"
        "Venezuela"                         = "South America"
        "Vietnam"                           = "Asia"
        "Yemen"                             = "Asia"
        "Zambia"                            = "Africa"
        "Zimbabwe"                          = "Africa"
    }
    if ($PSBoundParameters.ContainsKey('Country')) {
        if ($CountryToContinent[$Country]) {
            $CountryToContinent[$Country]
        } else {
            "Unknown"
        }
    } else {
        $CountryToContinent
    }
}
function Convert-UserAccountControl { 
    <#
    .SYNOPSIS
    Converts the UserAccountControl flags to their corresponding names.
 
    .DESCRIPTION
    This function takes a UserAccountControl value and converts it into a human-readable format by matching the flags to their corresponding names.
 
    .PARAMETER UserAccountControl
    Specifies the UserAccountControl value to be converted.
 
    .PARAMETER Separator
    Specifies the separator to use when joining the converted flags. If not provided, the flags will be returned as a list.
 
    .EXAMPLE
    Convert-UserAccountControl -UserAccountControl 66048
    Outputs: "DONT_EXPIRE_PASSWORD, PASSWORD_EXPIRED"
 
    .EXAMPLE
    Convert-UserAccountControl -UserAccountControl 512 -Separator ', '
    Outputs: "NORMAL_ACCOUNT"
 
    #>

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

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

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

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

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

                $Found = $Distinguished -match $Regex
                if ($Found) {
                    $Matches.cn
                }
            }
        }
    }
}
function Get-FileInformation { 
    <#
    .SYNOPSIS
    Get information about file such as Name, FullName and Size
 
    .DESCRIPTION
    Get information about file such as Name, FullName and Size
 
    .PARAMETER File
    File to get information about
 
    .EXAMPLE
    Get-FileInformation -File 'C:\Support\GitHub\PSSharedGoods\Public\FilesFolders\Get-FileInformation.ps1'
 
    #>

    [CmdletBinding()]
    param(
        [alias('LiteralPath', 'Path')][string] $File
    )
    if (Test-Path -LiteralPath $File) {
        $Item = Get-Item -LiteralPath $File
        [PSCustomObject] @{
            Name          = $Item.Name
            FullName      = $Item.FullName
            Size          = Get-FileSize -Bytes $Item.Length
            IsReadOnly    = $Item.IsReadOnly
            LastWriteTime = $Item.LastWriteTime
        }
    }
}
function Get-GitHubVersion { 
    <#
    .SYNOPSIS
    Get the latest version of a GitHub repository and compare with local version
 
    .DESCRIPTION
    Get the latest version of a GitHub repository and compare with local version
 
    .PARAMETER Cmdlet
    Cmdlet to find module for
 
    .PARAMETER RepositoryOwner
    Repository owner
 
    .PARAMETER RepositoryName
    Repository name
 
    .EXAMPLE
    Get-GitHubVersion -Cmdlet 'Start-DelegationModel' -RepositoryOwner 'evotecit' -RepositoryName 'DelegationModel'
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $Cmdlet,
        [Parameter(Mandatory)][string] $RepositoryOwner,
        [Parameter(Mandatory)][string] $RepositoryName
    )
    $App = Get-Command -Name $Cmdlet -ErrorAction SilentlyContinue
    if ($App) {
        [Array] $GitHubReleases = (Get-GitHubLatestRelease -Url "https://api.github.com/repos/$RepositoryOwner/$RepositoryName/releases" -Verbose:$false)
        $LatestVersion = $GitHubReleases[0]
        if (-not $LatestVersion.Errors) {
            if ($App.Version -eq $LatestVersion.Version) {
                "Current/Latest: $($LatestVersion.Version) at $($LatestVersion.PublishDate)"
            } elseif ($App.Version -lt $LatestVersion.Version) {
                "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Update?"
            } elseif ($App.Version -gt $LatestVersion.Version) {
                "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Lucky you!"
            }
        } else {
            "Current: $($App.Version)"
        }
    } else {
        "Current: Unknown"
    }
}
function Get-WinADForestDetails { 
    <#
    .SYNOPSIS
    Get details about Active Directory Forest, Domains and Domain Controllers in a single query
 
    .DESCRIPTION
    Get details about Active Directory Forest, Domains and Domain Controllers in a single query
 
    .PARAMETER Forest
    Target different Forest, by default current forest is used
 
    .PARAMETER ExcludeDomains
    Exclude domain from search, by default whole forest is scanned
 
    .PARAMETER IncludeDomains
    Include only specific domains, by default whole forest is scanned
 
    .PARAMETER ExcludeDomainControllers
    Exclude specific domain controllers, by default there are no exclusions, as long as VerifyDomainControllers switch is enabled. Otherwise this parameter is ignored.
 
    .PARAMETER IncludeDomainControllers
    Include only specific domain controllers, by default all domain controllers are included, as long as VerifyDomainControllers switch is enabled. Otherwise this parameter is ignored.
 
    .PARAMETER SkipRODC
    Skip Read-Only Domain Controllers. By default all domain controllers are included.
 
    .PARAMETER ExtendedForestInformation
    Ability to provide Forest Information from another command to speed up processing
 
    .PARAMETER Filter
    Filter for Get-ADDomainController
 
    .PARAMETER TestAvailability
    Check if Domain Controllers are available
 
    .PARAMETER Test
    Pick what to check for availability. Options are: All, Ping, WinRM, PortOpen, Ping+WinRM, Ping+PortOpen, WinRM+PortOpen. Default is All
 
    .PARAMETER Ports
    Ports to check for availability. Default is 135
 
    .PARAMETER PortsTimeout
    Ports timeout for availability check. Default is 100
 
    .PARAMETER PingCount
    How many pings to send. Default is 1
 
    .PARAMETER PreferWritable
    Prefer writable domain controllers over read-only ones when returning Query Servers
 
    .PARAMETER Extended
    Return extended information about domains with NETBIOS names
 
    .EXAMPLE
    Get-WinADForestDetails | Format-Table
 
    .EXAMPLE
    Get-WinADForestDetails -Forest 'ad.evotec.xyz' | Format-Table
 
    .NOTES
    General notes
    #>

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

    if (-not $ExtendedForestInformation) {

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

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

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

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

            $Domain
        }

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

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

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

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

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

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

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

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

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

        $Findings
    } else {

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

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

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

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

            [Array] $Findings['DomainDomainControllers'][$Domain]
        }
        $Findings
    }
}
function Remove-EmptyValue { 
    <#
    .SYNOPSIS
    Removes empty values from a hashtable recursively.
 
    .DESCRIPTION
    This function removes empty values from a given hashtable. It can be used to clean up a hashtable by removing keys with null, empty string, empty array, or empty dictionary values. The function supports recursive removal of empty values.
 
    .PARAMETER Hashtable
    The hashtable from which empty values will be removed.
 
    .PARAMETER ExcludeParameter
    An array of keys to exclude from the removal process.
 
    .PARAMETER Recursive
    Indicates whether to recursively remove empty values from nested hashtables.
 
    .PARAMETER Rerun
    Specifies the number of times to rerun the removal process recursively.
 
    .PARAMETER DoNotRemoveNull
    If specified, null values will not be removed.
 
    .PARAMETER DoNotRemoveEmpty
    If specified, empty string values will not be removed.
 
    .PARAMETER DoNotRemoveEmptyArray
    If specified, empty array values will not be removed.
 
    .PARAMETER DoNotRemoveEmptyDictionary
    If specified, empty dictionary values will not be removed.
 
    .EXAMPLE
    $hashtable = @{
        'Key1' = '';
        'Key2' = $null;
        'Key3' = @();
        'Key4' = @{}
    }
    Remove-EmptyValue -Hashtable $hashtable -Recursive
 
    Description
    -----------
    This example removes empty values from the $hashtable recursively.
 
    #>

    [alias('Remove-EmptyValues')]
    [CmdletBinding()]
    param(
        [alias('Splat', 'IDictionary')][Parameter(Mandatory)][System.Collections.IDictionary] $Hashtable,
        [string[]] $ExcludeParameter,
        [switch] $Recursive,
        [int] $Rerun,
        [switch] $DoNotRemoveNull,
        [switch] $DoNotRemoveEmpty,
        [switch] $DoNotRemoveEmptyArray,
        [switch] $DoNotRemoveEmptyDictionary
    )
    foreach ($Key in [string[]] $Hashtable.Keys) {
        if ($Key -notin $ExcludeParameter) {
            if ($Recursive) {
                if ($Hashtable[$Key] -is [System.Collections.IDictionary]) {
                    if ($Hashtable[$Key].Count -eq 0) {
                        if (-not $DoNotRemoveEmptyDictionary) {
                            $Hashtable.Remove($Key)
                        }
                    } else {
                        Remove-EmptyValue -Hashtable $Hashtable[$Key] -Recursive:$Recursive
                    }
                } else {
                    if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) {
                        $Hashtable.Remove($Key)
                    } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') {
                        $Hashtable.Remove($Key)
                    } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) {
                        $Hashtable.Remove($Key)
                    }
                }
            } else {
                if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) {
                    $Hashtable.Remove($Key)
                } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') {
                    $Hashtable.Remove($Key)
                } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) {
                    $Hashtable.Remove($Key)
                }
            }
        }
    }
    if ($Rerun) {
        for ($i = 0; $i -lt $Rerun; $i++) {
            Remove-EmptyValue -Hashtable $Hashtable -Recursive:$Recursive
        }
    }
}
function Start-TimeLog { 
    <#
    .SYNOPSIS
    Starts a new stopwatch for logging time.
 
    .DESCRIPTION
    This function starts a new stopwatch that can be used for logging time durations.
 
    .EXAMPLE
    Start-TimeLog
    Starts a new stopwatch for logging time.
 
    #>

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

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

    [alias('Write-Colour')]
    [CmdletBinding()]
    param (
        [alias ('T')] [String[]]$Text,
        [alias ('C', 'ForegroundColor', 'FGC')] [ConsoleColor[]]$Color = [ConsoleColor]::White,
        [alias ('B', 'BGC')] [ConsoleColor[]]$BackGroundColor = $null,
        [alias ('Indent')][int] $StartTab = 0,
        [int] $LinesBefore = 0,
        [int] $LinesAfter = 0,
        [int] $StartSpaces = 0,
        [alias ('L')] [string] $LogFile = '',
        [Alias('DateFormat', 'TimeFormat')][string] $DateTimeFormat = 'yyyy-MM-dd HH:mm:ss',
        [alias ('LogTimeStamp')][bool] $LogTime = $true,
        [int] $LogRetry = 2,
        [ValidateSet('unknown', 'string', 'unicode', 'bigendianunicode', 'utf8', 'utf7', 'utf32', 'ascii', 'default', 'oem')][string]$Encoding = 'Unicode',
        [switch] $ShowTime,
        [switch] $NoNewLine,
        [alias('HideConsole')][switch] $NoConsoleOutput
    )
    if (-not $NoConsoleOutput) {
        $DefaultColor = $Color[0]
        if ($null -ne $BackGroundColor -and $BackGroundColor.Count -ne $Color.Count) {
            Write-Error "Colors, BackGroundColors parameters count doesn't match. Terminated."
            return
        }
        if ($LinesBefore -ne 0) {
            for ($i = 0; $i -lt $LinesBefore; $i++) {
                Write-Host -Object "`n" -NoNewline 
            } 
        } 
        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) {
                    Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Tried ($Retry/$LogRetry))"
                } else {
                    Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Retrying... ($Retry/$LogRetry)"
                }
            }
        } Until ($Saved -eq $true -or $Retry -ge $LogRetry)
    }
}
function ConvertTo-OperatingSystem { 
    <#
    .SYNOPSIS
    Allows easy conversion of OperatingSystem, Operating System Version to proper Windows 10 naming based on WMI or AD
 
    .DESCRIPTION
    Allows easy conversion of OperatingSystem, Operating System Version to proper Windows 10 naming based on WMI or AD
 
    .PARAMETER OperatingSystem
    Operating System as returned by Active Directory
 
    .PARAMETER OperatingSystemVersion
    Operating System Version as returned by Active Directory
 
    .EXAMPLE
    $Computers = Get-ADComputer -Filter * -Properties OperatingSystem, OperatingSystemVersion | ForEach-Object {
        $OPS = ConvertTo-OperatingSystem -OperatingSystem $_.OperatingSystem -OperatingSystemVersion $_.OperatingSystemVersion
        Add-Member -MemberType NoteProperty -Name 'OperatingSystemTranslated' -Value $OPS -InputObject $_ -Force
        $_
    }
    $Computers | Select-Object DNS*, Name, SamAccountName, Enabled, OperatingSystem*, DistinguishedName | Format-Table
 
    .EXAMPLE
    $Registry = Get-PSRegistry -ComputerName 'AD1' -RegistryPath 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
    ConvertTo-OperatingSystem -OperatingSystem $Registry.ProductName -OperatingSystemVersion $Registry.CurrentBuildNumber
 
    .NOTES
    General notes
    #>

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

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

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

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

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

        $Systems = @{

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

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

            '20348'        = 'Windows Server 2022'
            '19042'        = 'Windows Server 2019 20H2'
            '19041'        = 'Windows Server 2019 2004'
            '18363'        = 'Windows Server 2019 1909'
            '18362'        = "Windows Server 2019 1903" 
            '17763'        = "Windows Server 2019 1809" 
            '17134'        = "Windows Server 2016 1803" 
            '14393'        = "Windows Server 2016 1607"
            '9600'         = 'Windows Server 2012 R2'
            '7601'         = 'Windows Server 2008 R2'
            '3790'         = 'Windows Server 2003'
        }
        $System = $Systems[$OperatingSystemVersion]
        if (-not $System) {
            $System = $OperatingSystemVersion
        }
    } else {
        $System = $OperatingSystem
    }
    if ($System) {
        $System
    } else {
        'Unknown'
    }
}
function Copy-DictionaryManual { 
    <#
    .SYNOPSIS
    Copies a dictionary recursively, handling nested dictionaries and lists.
 
    .DESCRIPTION
    This function copies a dictionary recursively, handling nested dictionaries and lists. It creates a deep copy of the input dictionary, ensuring that modifications to the copied dictionary do not affect the original dictionary.
 
    .PARAMETER Dictionary
    The dictionary to be copied.
 
    .EXAMPLE
    $originalDictionary = @{
        'Key1' = 'Value1'
        'Key2' = @{
            'NestedKey1' = 'NestedValue1'
        }
    }
    $copiedDictionary = Copy-DictionaryManual -Dictionary $originalDictionary
 
    This example demonstrates how to copy a dictionary with nested values.
 
    #>

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

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

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

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

    $clone
}
function Get-FileSize { 
    <#
    .SYNOPSIS
    Get-FileSize function calculates the file size in human-readable format.
 
    .DESCRIPTION
    This function takes a file size in bytes and converts it into a human-readable format (e.g., KB, MB, GB, etc.).
 
    .PARAMETER Bytes
    Specifies the size of the file in bytes.
 
    .EXAMPLE
    Get-FileSize -Bytes 1024
    Output: 1 KB
 
    .EXAMPLE
    Get-FileSize -Bytes 1048576
    Output: 1 MB
    #>

    [CmdletBinding()]
    param(
        $Bytes
    )
    $sizes = 'Bytes,KB,MB,GB,TB,PB,EB,ZB' -split ','
    for ($i = 0; ($Bytes -ge 1kb) -and ($i -lt $sizes.Count); $i++) {
        $Bytes /= 1kb
    }
    $N = 2;
    if ($i -eq 0) {
        $N = 0
    }
    return "{0:N$($N)} {1}" -f $Bytes, $sizes[$i]
}
function Get-GitHubLatestRelease { 
    <#
    .SYNOPSIS
    Gets one or more releases from GitHub repository
 
    .DESCRIPTION
    Gets one or more releases from GitHub repository
 
    .PARAMETER Url
    Url to github repository
 
    .EXAMPLE
    Get-GitHubLatestRelease -Url "https://api.github.com/repos/evotecit/Testimo/releases" | Format-Table
 
    .NOTES
    General notes
    #>

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

    $Responds = Test-Connection -ComputerName $URl.Host -Quiet -Count 1
    if ($Responds) {
        Try {
            [Array] $JsonOutput = (Invoke-WebRequest -Uri $Url -ErrorAction Stop | ConvertFrom-Json)
            foreach ($JsonContent in $JsonOutput) {
                [PSCustomObject] @{
                    PublishDate = [DateTime]  $JsonContent.published_at
                    CreatedDate = [DateTime] $JsonContent.created_at
                    PreRelease  = [bool] $JsonContent.prerelease
                    Version     = [version] ($JsonContent.name -replace 'v', '')
                    Tag         = $JsonContent.tag_name
                    Branch      = $JsonContent.target_commitish
                    Errors      = ''
                }
            }
        } catch {
            [PSCustomObject] @{
                PublishDate = $null
                CreatedDate = $null
                PreRelease  = $null
                Version     = $null
                Tag         = $null
                Branch      = $null
                Errors      = $_.Exception.Message
            }
        }
    } else {
        [PSCustomObject] @{
            PublishDate = $null
            CreatedDate = $null
            PreRelease  = $null
            Version     = $null
            Tag         = $null
            Branch      = $null
            Errors      = "No connection (ping) to $($Url.Host)"
        }
    }
    $ProgressPreference = 'Continue'
}
function Test-ComputerPort { 
    <#
    .SYNOPSIS
    Tests the connectivity of a computer on specified TCP and UDP ports.
 
    .DESCRIPTION
    The Test-ComputerPort function tests the connectivity of a computer on specified TCP and UDP ports. It checks if the specified ports are open and reachable on the target computer.
 
    .PARAMETER ComputerName
    Specifies the name of the computer to test the port connectivity.
 
    .PARAMETER PortTCP
    Specifies an array of TCP ports to test connectivity.
 
    .PARAMETER PortUDP
    Specifies an array of UDP ports to test connectivity.
 
    .PARAMETER Timeout
    Specifies the timeout value in milliseconds for the connection test. Default is 5000 milliseconds.
 
    .EXAMPLE
    Test-ComputerPort -ComputerName "Server01" -PortTCP 80,443 -PortUDP 53 -Timeout 3000
    Tests the connectivity of Server01 on TCP ports 80 and 443, UDP port 53 with a timeout of 3000 milliseconds.
 
    .EXAMPLE
    Test-ComputerPort -ComputerName "Server02" -PortTCP 3389 -PortUDP 123
    Tests the connectivity of Server02 on TCP port 3389, UDP port 123 with the default timeout of 5000 milliseconds.
    #>

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

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

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

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

    [CmdletBinding()]
    param (
        [alias('Server')][string[]] $ComputerName
    )
    $Output = foreach ($Computer in $ComputerName) {
        $Test = [PSCustomObject] @{
            Output       = $null
            Status       = $null
            ComputerName = $Computer
        }
        try {
            $Test.Output = Test-WSMan -ComputerName $Computer -ErrorAction Stop
            $Test.Status = $true
        } catch {
            $Test.Status = $false
        }
        $Test
    }
    $Output
}
function Add-ManagerInformation {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $SummaryDictionary,
        [string] $Type,
        [string] $ManagerType,
        [Object] $Key,
        [PSCustomObject] $User,
        [PSCustomObject] $Rule
    )
    if ($Key) {
        if ($Key -is [string]) {
            $KeyDN = $Key
        } else {
            $KeyDN = $Key.DisplayName
        }

        if (-not $SummaryDictionary[$KeyDN]) {
            $SummaryDictionary[$KeyDN] = [ordered] @{
                Manager             = $Key
                ManagerDefault      = [ordered] @{}
                ManagerNotCompliant = [ordered] @{}
                Security            = [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] @{
                'Status'        = $ManagerType
                'Manager'       = $User.Manager
                'Manager Email' = $User.ManagerEmail
            }
            $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 Export-SearchInformation {
    [CmdletBinding()]
    param(
        [string] $SearchPath,
        [System.Collections.IDictionary] $SummarySearch,
        [string] $Today,
        [Array] $SummaryUsersEmails,
        [Array] $SummaryManagersEmails,
        [Array] $SummaryEscalationEmails
    )

    if ($SearchPath) {
        Write-Color -Text "[i]" , " Saving Search report " -Color White, Yellow, Green
        if ($SummaryUsersEmails) {
            $SummarySearch['EmailSent'][$Today] += $SummaryUsersEmails
        }
        if ($SummaryEscalationEmails) {
            $SummarySearch['EmailEscalations'][$Today] += $SummaryEscalationEmails
        }
        if ($SummaryManagersEmails) {
            $SummarySearch['EmailManagers'][$Today] += $SummaryManagersEmails
        }
        try {
            $SummarySearch | Export-Clixml -LiteralPath $SearchPath -ErrorAction Stop
        } catch {
            Write-Color -Text "[e]", " Couldn't save to file $SearchPath", ". Error: ", $_.Exception.Message -Color White, Yellow, White, Yellow, White, Yellow, White
        }
        Write-Color -Text "[i]" , " Saving Search report ", "Done" -Color White, Yellow, Green
    }
}
function Import-SearchInformation {
    [CmdletBinding()]
    param(
        [string] $SearchPath
    )
    if ($SearchPath) {
        if (Test-Path -LiteralPath $SearchPath) {
            try {
                Write-Color -Text "[i]", " Loading file ", $SearchPath -Color White, Yellow, White, Yellow, White, Yellow, White
                $SummarySearch = Import-Clixml -LiteralPath $SearchPath -ErrorAction Stop
            } catch {
                Write-Color -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] @{}
            EmailManagers    = [ordered] @{}
            EmailEscalations = [ordered] @{}
        }
    }
    $SummarySearch

}
function Invoke-PasswordRuleProcessing {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Rule,
        [System.Collections.IDictionary] $Summary,
        [System.Collections.IDictionary] $CachedUsers,
        [System.Collections.IDictionary] $AllSkipped,
        [System.Collections.IDictionary] $Locations,
        [System.Collections.IDictionary] $Logging,
        [System.Collections.IDictionary] $UsersExternalSystem,
        [DateTime] $TodayDate
    )

    if ($Rule.Enable -eq $true) {
        Write-Color -Text "[i]", " Processing rule ", $Rule.Name, ' status: ', $Rule.Enable -Color Yellow, White, Green, White, Green, White, Green, White

        if (-not $Summary['Rules'][$Rule.Name] ) {
            $Summary['Rules'][$Rule.Name] = [ordered] @{}
        }

        $Rule.Reminders = $Rule.Reminders | ForEach-Object { $_ }
        foreach ($User in $CachedUsers.Values) {
            if ($User.Enabled -eq $false) {

                continue
            }
            if ($Rule.ExcludeOU.Count -gt 0) {
                $FoundOU = $false
                foreach ($OU in $Rule.ExcludeOU) {
                    if ($User.OrganizationalUnit -like $OU) {
                        $FoundOU = $true
                        break
                    }
                }

                if ($FoundOU) {
                    continue
                }
            }
            if ($Rule.IncludeOU.Count -gt 0) {

                $FoundOU = $false
                foreach ($OU in $Rule.IncludeOU) {
                    if ($User.OrganizationalUnit -like $OU) {
                        $FoundOU = $true
                        break
                    }
                }
                if (-not $FoundOU) {
                    continue
                }
            }
            if ($Rule.ExcludeGroup.Count -gt 0) {

                $FoundGroup = $false
                foreach ($Group in $Rule.ExcludeGroup) {
                    if ($User.MemberOf -contains $Group) {
                        $FoundGroup = $true
                        break
                    }
                }

                if ($FoundGroup) {
                    continue
                }
            }
            if ($Rule.IncludeGroup.Count -gt 0) {

                $FoundGroup = $false
                foreach ($Group in $Rule.IncludeGroup) {
                    if ($User.MemberOf -contains $Group) {
                        $FoundGroup = $true
                        break
                    }
                }
                if (-not $FoundGroup) {
                    continue
                }
            }
            if ($Rule.IncludeName.Count -gt 0) {
                $IncludeName = $false
                foreach ($Name in $Rule.IncludeName) {
                    foreach ($Property in $Rule.IncludeNameProperties) {
                        if ($User.$Property -like $Name) {
                            $IncludeName = $true
                            break
                        }
                    }
                    if ($IncludeName) {
                        break
                    }
                }
                if (-not $IncludeName) {
                    continue
                }
            }
            if ($Rule.ExcludeName.Count -gt 0) {
                $ExcludeName = $false
                foreach ($Name in $Rule.ExcludeName) {
                    foreach ($Property in $Rule.ExcludeNameProperties) {
                        if ($User.$Property -like $Name) {
                            $ExcludeName = $true
                            break
                        }
                    }
                    if ($ExcludeName) {
                        break
                    }
                }
                if ($ExcludeName) {
                    continue
                }
            }
            if ($Summary['Notify'][$User.DistinguishedName] -and $Summary['Notify'][$User.DistinguishedName].ProcessManagersOnly -ne $true) {

                continue
            }
            if ($Rule.IncludePasswordNeverExpires -and $Rule.IncludeExpiring) {
                if ($User.PasswordNeverExpires -eq $true) {
                    $DaysToPasswordExpiry = $Rule.PasswordNeverExpiresDays - $User.PasswordDays
                    $User.DaysToExpire = $DaysToPasswordExpiry
                }
            } elseif ($Rule.IncludeExpiring) {
                if ($User.PasswordNeverExpires -eq $true) {

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

                    continue
                }
            } else {
                Write-Color -Text "[i]", " Processing rule ", $Rule.Name, " doesn't include IncludePasswordNeverExpires nor IncludeExpiring so skipping." -Color Yellow, White, Green, White, Green, White, Green, White
                continue
            }

            if ($null -eq $User.DaysToExpire) {

                if ($Logging.NotifyOnUserDaysToExpireNull) {
                    Write-Color -Text @(
                        "[i]",
                        " User ",
                        $User.DisplayName,
                        " (",
                        $User.UserPrincipalName,
                        ")",
                        " days to expire not set. ",
                        "(",
                        "Password Last Set: ",
                        $User.PasswordLastSet,
                        ")",
                        " (Password at next logon: ",
                        $User.PasswordAtNextLogon, ")"
                    ) -Color Yellow, White, Yellow, White, Yellow, White, White, White, Yellow, DarkCyan, White, Yellow, DarkCyan, White
                }

                $AllSkipped[$User.DistinguishedName] = $User

                $Location = $User.OrganizationalUnit
                if (-not $Location) {
                    $Location = 'Default'
                }
                if (-not $Locations[$Location]) {
                    $Locations[$Location] = [PSCustomObject] @{
                        Location     = $Location
                        Count        = 0
                        CountExpired = 0
                        Names        = [System.Collections.Generic.List[string]]::new()
                        NamesExpired = [System.Collections.Generic.List[string]]::new()
                    }
                }
                if ($User.PasswordExpired) {
                    $Locations[$Location].CountExpired++
                    $Locations[$Location].NamesExpired.Add($User.SamAccountName)
                } else {
                    $Locations[$Location].Count++
                    $Locations[$Location].Names.Add($User.SamAccountName)
                }
            }

            if ($null -ne $User.DaysToExpire -and $User.DaysToExpire -in $Rule.Reminders) {

                if (-not $Rule.ProcessManagersOnly) {
                    if ($Logging.NotifyOnUserMatchingRule) {
                        Write-Color -Text "[i]", " User ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire, " " -Color Yellow, White, Yellow, White, Yellow, White, White, Blue
                    }

                    if ($Rule.OverwriteEmailProperty) {
                        $NewPropertyWithEmail = $Rule.OverwriteEmailProperty
                        if ($NewPropertyWithEmail -and $User.$NewPropertyWithEmail) {
                            $User.EmailAddress = $User.$NewPropertyWithEmail
                        }
                    }

                    if ($Rule.OverwriteEmailFromExternalUsers) {
                        $ExternalUser = $null
                        $ADProperty = $UsersExternalSystem.ActiveDirectoryProperty
                        $EmailProperty = $UsersExternalSystem.EmailProperty
                        $ExternalUser = $UsersExternalSystem['Users'][$User.$ADProperty]
                        if ($ExternalUser -and $ExternalUser.$EmailProperty -like '*@*') {
                            $User.EmailAddress = $ExternalUser.$EmailProperty
                        }
                    }

                    $Summary['Notify'][$User.DistinguishedName] = [ordered] @{
                        User                = $User
                        Rule                = $Rule
                        ProcessManagersOnly = $Rule.ProcessManagersOnly
                    }

                    if ($Summary['Rules'][$Rule.Name][$User.DistinguishedName]) {

                        $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleOptions.Add('User')
                        $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleName = $Rule.Name
                    } else {

                        $Summary['Rules'][$Rule.Name][$User.DistinguishedName] = [ordered] @{
                            User                = $User
                            Rule                = $Rule
                            ProcessManagersOnly = $Rule.ProcessManagersOnly
                        }
                        $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleOptions.Add('User')
                        $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleName = $Rule.Name
                    }
                }
            }

            if ($Rule.OverwriteManagerProperty) {
                $NewPropertyWithManager = $Rule.OverwriteManagerProperty
                if ($NewPropertyWithManager -and $User.$NewPropertyWithManager) {
                    $NewManager = $CachedUsers[$User.$NewPropertyWithManager]
                    if ($NewManager -and $NewManager.Mail -like "*@*") {
                        $User.ManagerEmail = $NewManager.Mail
                        $User.Manager = $NewManager.DisplayName
                        $User.ManagerSamAccountName = $NewManager.SamAccountName
                        $User.ManagerEnabled = $NewManager.Enabled
                        $User.ManagerLastLogon = $NewManager.LastLogonDate
                        if ($User.ManagerLastLogon) {
                            $User.ManagerLastLogonDays = $( - $($User.ManagerLastLogon - $Today).Days)
                        } else {
                            $User.ManagerLastLogonDays = $null
                        }
                        $User.ManagerType = $NewManager.ObjectClass
                        $User.ManagerDN = $NewManager.DistinguishedName
                    }
                }
            }

            if ($null -ne $User.DaysToExpire -and $Rule.SendToManager) {
                if ($Rule.SendToManager.Manager -and $Rule.SendToManager.Manager.Enable -eq $true -and $User.ManagerStatus -eq 'Enabled' -and $User.ManagerEmail -like "*@*") {
                    $SendToManager = $true

                    if ($Rule.SendToManager.Manager.IncludeOU.Count -gt 0) {

                        $FoundOU = $false
                        foreach ($OU in $Rule.SendToManager.Manager.IncludeOU) {
                            if ($User.OrganizationalUnit -like $OU) {
                                $FoundOU = $true
                                break
                            }
                        }
                        if (-not $FoundOU) {
                            $SendToManager = $false
                        }
                    }
                    if ($SendToManager -and $Rule.SendToManager.Manager.ExcludeOU.Count -gt 0) {
                        $FoundOU = $false
                        foreach ($OU in $Rule.SendToManager.Manager.ExcludeOU) {
                            if ($User.OrganizationalUnit -like $OU) {
                                $FoundOU = $true
                                break
                            }
                        }

                        if ($FoundOU) {
                            $SendToManager = $false
                        }
                    }
                    if ($SendToManager -and $Rule.SendToManager.Manager.ExcludeGroup.Count -gt 0) {

                        $FoundGroup = $false
                        foreach ($Group in $Rule.SendToManager.Manager.ExcludeGroup) {
                            if ($User.MemberOf -contains $Group) {
                                $FoundGroup = $true
                                break
                            }
                        }

                        if ($FoundGroup) {
                            $SendToManager = $false
                        }
                    }
                    if ($SendToManager -and $Rule.SendToManager.Manager.IncludeGroup.Count -gt 0) {

                        $FoundGroup = $false
                        foreach ($Group in $Rule.SendToManager.Manager.IncludeGroup) {
                            if ($User.MemberOf -contains $Group) {
                                $FoundGroup = $true
                                break
                            }
                        }
                        if (-not $FoundGroup) {
                            $SendToManager = $false
                        }
                    }
                    if ($SendToManager) {
                        $SendToManager = $false
                        if ($Rule.SendToManager.Manager.Reminders.Default.Enable -eq $true -and $null -eq $Rule.SendToManager.Manager.Reminders.Default.Reminder -and $User.DaysToExpire -in $Rule.Reminders) {

                            $SendToManager = $true
                        } elseif ($Rule.SendToManager.Manager.Reminders.Default.Enable -eq $true -and $Rule.SendToManager.Manager.Reminders.Default.Reminder -and $User.DaysToExpire -in $Rule.SendToManager.Manager.Reminders.Default.Reminder) {

                            $SendToManager = $true
                        }
                        if (-not $SendToManager -and $Rule.SendToManager.Manager.Reminders.OnDay -and $Rule.SendToManager.Manager.Reminders.OnDay.Enable -eq $true) {
                            foreach ($Day in $Rule.SendToManager.Manager.Reminders.OnDay.Days) {
                                if ($Day -eq "$($TodayDate.DayOfWeek)") {
                                    if ($Rule.SendToManager.Manager.Reminders.OnDay.ComparisonType -eq 'lt') {
                                        if ($User.DaysToExpire -lt $Rule.SendToManager.Manager.Reminders.OnDay.Reminder) {
                                            $SendToManager = $true
                                            break
                                        }
                                    } elseif ($Rule.SendToManager.Manager.Reminders.OnDay.ComparisonType -eq 'gt') {
                                        if ($User.DaysToExpire -gt $Rule.SendToManager.Manager.Reminders.OnDay.Reminder) {
                                            $SendToManager = $true
                                            break
                                        }
                                    } elseif ($Rule.SendToManager.Manager.Reminders.OnDay.ComparisonType -eq 'eq') {
                                        if ($User.DaysToExpire -eq $Rule.SendToManager.Manager.Reminders.OnDay.Reminder) {
                                            $SendToManager = $true
                                            break
                                        }
                                    } elseif ($Rule.SendtoManager.Manager.Reminders.OnDay.ComparisonType -eq 'in') {
                                        if ($User.DaysToExpire -in $Rule.SendToManager.Manager.Reminders.OnDay.Reminder) {
                                            $SendToManager = $true
                                            break
                                        }
                                    }
                                }
                            }
                        }
                        if (-not $SendToManager -and $Rule.SendToManager.Manager.Reminders.OnDayOfMonth -and $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Enable -eq $true) {
                            foreach ($Day in $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Days) {
                                if ($Day -eq $TodayDate.Day) {
                                    if ($Rule.SendToManager.Manager.Reminders.OnDayOfMonth.ComparisonType -eq 'lt') {
                                        if ($User.DaysToExpire -lt $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Reminder) {
                                            $SendToManager = $true
                                            break
                                        }
                                    } elseif ($Rule.SendToManager.Manager.Reminders.OnDayOfMonth.ComparisonType -eq 'gt') {
                                        if ($User.DaysToExpire -gt $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Reminder) {
                                            $SendToManager = $true
                                            break
                                        }
                                    } elseif ($Rule.SendToManager.Manager.Reminders.OnDayOfMonth.ComparisonType -eq 'eq') {
                                        if ($User.DaysToExpire -eq $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Reminder) {
                                            $SendToManager = $true
                                            break
                                        }
                                    } elseif ($Rule.SendtoManager.Manager.Reminders.OnDayOfMonth.ComparisonType -eq 'in') {
                                        if ($User.DaysToExpire -in $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Reminder) {
                                            $SendToManager = $true
                                            break
                                        }
                                    }
                                }
                            }
                        }
                        if ($SendToManager) {
                            if ($Logging.NotifyOnUserMatchingRuleForManager) {
                                Write-Color -Text "[i]", " User (manager rule) ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire, " " -Color Yellow, White, Yellow, White, Yellow, White, White, Blue
                            }

                            if ($Summary['Rules'][$Rule.Name][$User.DistinguishedName]) {

                                $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleOptions.Add('Manager')
                                $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleName = $Rule.Name
                            } else {

                                $Summary['Rules'][$Rule.Name][$User.DistinguishedName] = [ordered] @{
                                    User                = $User
                                    Rule                = $Rule
                                    ProcessManagersOnly = $Rule.ProcessManagersOnly
                                }
                                $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleOptions.Add('Manager')
                                $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleName = $Rule.Name
                            }

                            $Splat = [ordered] @{
                                SummaryDictionary = $Summary['NotifyManager']
                                Type              = 'ManagerDefault'
                                ManagerType       = 'Ok'
                                Key               = $User.ManagerDN
                                User              = $User
                                Rule              = $Rule
                            }
                            Add-ManagerInformation @Splat
                        }
                    }
                }
            }

            if ($Rule.SendToManager -and $Rule.SendToManager.ManagerNotCompliant -and $Rule.SendToManager.ManagerNotCompliant.Enable -eq $true -and $Rule.SendToManager.ManagerNotCompliant.Manager) {

                if ($Rule.SendToManager.ManagerNotCompliant -and $Rule.SendToManager.ManagerNotCompliant.Enable -and $Rule.SendToManager.ManagerNotCompliant.Manager) {
                    $ManagerNotCompliant = $true

                    if ($Rule.SendToManager.ManagerNotCompliant.IncludeOU.Count -gt 0) {

                        $FoundOU = $false
                        foreach ($OU in $Rule.SendToManager.ManagerNotCompliant.IncludeOU) {
                            if ($User.OrganizationalUnit -like $OU) {
                                $FoundOU = $true
                                break
                            }
                        }
                        if (-not $FoundOU) {
                            $ManagerNotCompliant = $false
                        }
                    }
                    if ($ManagerNotCompliant -and $Rule.SendToManager.ManagerNotCompliant.ExcludeOU.Count -gt 0) {
                        $FoundOU = $false
                        foreach ($OU in $Rule.SendToManager.ManagerNotCompliant.ExcludeOU) {
                            if ($User.OrganizationalUnit -like $OU) {
                                $FoundOU = $true
                                break
                            }
                        }

                        if ($FoundOU) {
                            $ManagerNotCompliant = $false
                        }
                    }
                    if ($ManagerNotCompliant -and $Rule.SendToManager.ManagerNotCompliant.ExcludeGroup.Count -gt 0) {

                        $FoundGroup = $false
                        foreach ($Group in $Rule.SendToManager.ManagerNotCompliant.ExcludeGroup) {
                            if ($User.MemberOf -contains $Group) {
                                $FoundGroup = $true
                                break
                            }
                        }

                        if ($FoundGroup) {
                            $ManagerNotCompliant = $false
                        }
                    }
                    if ($ManagerNotCompliant -and $Rule.SendToManager.ManagerNotCompliant.IncludeGroup.Count -gt 0) {

                        $FoundGroup = $false
                        foreach ($Group in $Rule.SendToManager.ManagerNotCompliant.IncludeGroup) {
                            if ($User.MemberOf -contains $Group) {
                                $FoundGroup = $true
                                break
                            }
                        }
                        if (-not $FoundGroup) {
                            $ManagerNotCompliant = $false
                        }
                    }

                    if ($Rule.SendToManager.ManagerNotCompliant.Reminders) {
                        $ManagerNotCompliant = $false
                        if ($Rule.SendToManager.ManagerNotCompliant.Reminders.Default -and $Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Enable -eq $true) {
                            $Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Reminder = $Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Reminder | ForEach-Object { $_ }
                            if ($User.DaysToExpire -in $Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Reminder) {
                                $ManagerNotCompliant = $true
                            }
                        }
                        if ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay -and $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Enable -eq $true) {
                            foreach ($Day in $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Days) {
                                if ($Day -eq "$($TodayDate.DayOfWeek)") {
                                    if ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.ComparisonType -eq 'lt') {
                                        if ($User.DaysToExpire -lt $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Reminder) {
                                            $ManagerNotCompliant = $true
                                            break
                                        }
                                    } elseif ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.ComparisonType -eq 'gt') {
                                        if ($User.DaysToExpire -gt $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Reminder) {
                                            $ManagerNotCompliant = $true
                                            break
                                        }
                                    } elseif ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.ComparisonType -eq 'eq') {
                                        if ($User.DaysToExpire -eq $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Reminder) {
                                            $ManagerNotCompliant = $true
                                            break
                                        }
                                    } elseif ($Rule.SendtoManager.ManagerNotCompliant.Reminders.OnDay.ComparisonType -eq 'in') {
                                        if ($User.DaysToExpire -in $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Reminder) {
                                            $ManagerNotCompliant = $true
                                            break
                                        }
                                    }
                                }
                            }
                        }
                        if ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth -and $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Enable -eq $true) {
                            foreach ($Day in $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Days) {
                                if ($Day -eq $TodayDate.Day) {
                                    if ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.ComparisonType -eq 'lt') {
                                        if ($User.DaysToExpire -lt $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Reminder) {
                                            $ManagerNotCompliant = $true
                                            break
                                        }
                                    } elseif ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.ComparisonType -eq 'gt') {
                                        if ($User.DaysToExpire -gt $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Reminder) {
                                            $ManagerNotCompliant = $true
                                            break
                                        }
                                    } elseif ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.ComparisonType -eq 'eq') {
                                        if ($User.DaysToExpire -eq $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Reminder) {
                                            $ManagerNotCompliant = $true
                                            break
                                        }
                                    } elseif ($Rule.SendtoManager.ManagerNotCompliant.Reminders.OnDayOfMonth.ComparisonType -eq 'in') {
                                        if ($User.DaysToExpire -in $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Reminder) {
                                            $ManagerNotCompliant = $true
                                            break
                                        }
                                    }
                                }
                            }
                        }
                    }
                    if ($ManagerNotCompliant -eq $true) {
                        $ManagerNotCompliantMatched = $false
                        if ($Rule.SendToManager.ManagerNotCompliant.MissingEmail -and $User.ManagerStatus -in 'Enabled, bad email', 'No email') {

                            $Splat = [ordered] @{
                                SummaryDictionary = $Summary['NotifyManager']
                                Type              = 'ManagerNotCompliant'
                                ManagerType       = if ($User.ManagerStatus -eq 'Enabled, bad email') {
                                    'Manager has bad email' } else {
                                    'Manager has no email' }
                                Key               = $Rule.SendToManager.ManagerNotCompliant.Manager
                                User              = $User
                                Rule              = $Rule

                            }
                            Add-ManagerInformation @Splat

                            $ManagerNotCompliantMatched = $true
                        } elseif ($Rule.SendToManager.ManagerNotCompliant.Disabled -and $User.ManagerStatus -eq 'Disabled') {

                            $Splat = [ordered] @{
                                SummaryDictionary = $Summary['NotifyManager']
                                Type              = 'ManagerNotCompliant'
                                ManagerType       = 'Manager disabled'
                                Key               = $Rule.SendToManager.ManagerNotCompliant.Manager
                                User              = $User
                                Rule              = $Rule

                            }
                            Add-ManagerInformation @Splat

                            $ManagerNotCompliantMatched = $true
                        } elseif ($Rule.SendToManager.ManagerNotCompliant.LastLogon -and $User.ManagerLastLogonDays -ge $Rule.SendToManager.ManagerNotCompliant.LastLogonDays) {

                            $Splat = [ordered] @{
                                SummaryDictionary = $Summary['NotifyManager']
                                Type              = 'ManagerNotCompliant'
                                ManagerType       = 'Manager not logging in'
                                Key               = $Rule.SendToManager.ManagerNotCompliant.Manager
                                User              = $User
                                Rule              = $Rule

                            }
                            Add-ManagerInformation @Splat

                            $ManagerNotCompliantMatched = $true
                        } elseif ($Rule.SendToManager.ManagerNotCompliant.Missing -and $User.ManagerStatus -eq 'Missing') {

                            $Splat = [ordered] @{
                                SummaryDictionary = $Summary['NotifyManager']
                                Type              = 'ManagerNotCompliant'
                                ManagerType       = 'Manager not set'
                                Key               = $Rule.SendToManager.ManagerNotCompliant.Manager
                                User              = $User
                                Rule              = $Rule

                            }
                            Add-ManagerInformation @Splat

                            $ManagerNotCompliantMatched = $true
                        }

                        if ($ManagerNotCompliantMatched) {
                            if ($Logging.NotifyOnUserMatchingRuleForManagerNotCompliant) {
                                Write-Color -Text "[i]", " User (manager not compliant rule) ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire, " " -Color Yellow, White, Yellow, White, Yellow, White, White, Blue
                            }

                            if ($Summary['Rules'][$Rule.Name][$User.DistinguishedName]) {

                                $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleOptions.Add('Manager Not Compliant')
                                $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleName = $Rule.Name
                            } else {

                                $Summary['Rules'][$Rule.Name][$User.DistinguishedName] = [ordered] @{
                                    User                = $User
                                    Rule                = $Rule
                                    ProcessManagersOnly = $Rule.ProcessManagersOnly
                                }
                                $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleOptions.Add('Manager Not Compliant')
                                $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleName = $Rule.Name
                            }
                        } else {
                            if ($User.ManagerStatus -eq 'Enabled') {

                            } else {

                                if ($Logging.NotifyOnUserMatchingRuleForManagerNotCompliant) {
                                    Write-Color -Text "[i]", " User (manager not compliant rule not processed) ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire, " manager status: ", $User.ManagerStatus -Color Yellow, White, Yellow, White, Yellow, White, White, Blue
                                }
                            }
                        }
                    }
                }
            }

            if ($null -ne $User.DaysToExpire -and $Rule.SendToManager -and $Rule.SendToManager.SecurityEscalation -and $Rule.SendToManager.SecurityEscalation.Enable -eq $true -and $Rule.SendToManager.SecurityEscalation.Manager) {
                $SecurityEscalation = $true
                if ($Rule.SendToManager.SecurityEscalation.IncludeOU.Count -gt 0) {

                    $FoundOU = $false
                    foreach ($OU in $Rule.SendToManager.SecurityEscalation.IncludeOU) {
                        if ($User.OrganizationalUnit -like $OU) {
                            $FoundOU = $true
                            break
                        }
                    }
                    if (-not $FoundOU) {
                        $SecurityEscalation = $false
                    }
                }
                if ($SecurityEscalation -and $Rule.SendToManager.SecurityEscalation.ExcludeOU.Count -gt 0) {
                    $FoundOU = $false
                    foreach ($OU in $Rule.SendToManager.SecurityEscalation.ExcludeOU) {
                        if ($User.OrganizationalUnit -like $OU) {
                            $FoundOU = $true
                            break
                        }
                    }

                    if ($FoundOU) {
                        $SecurityEscalation = $false
                    }
                }
                if ($SecurityEscalation -and $Rule.SendToManager.SecurityEscalation.ExcludeGroup.Count -gt 0) {

                    $FoundGroup = $false
                    foreach ($Group in $Rule.SendToManager.SecurityEscalation.ExcludeGroup) {
                        if ($User.MemberOf -contains $Group) {
                            $FoundGroup = $true
                            break
                        }
                    }

                    if ($FoundGroup) {
                        $SecurityEscalation = $false
                    }
                }
                if ($SecurityEscalation -and $Rule.SendToManager.SecurityEscalation.IncludeGroup.Count -gt 0) {

                    $FoundGroup = $false
                    foreach ($Group in $Rule.SendToManager.SecurityEscalation.IncludeGroup) {
                        if ($User.MemberOf -contains $Group) {
                            $FoundGroup = $true
                            break
                        }
                    }
                    if (-not $FoundGroup) {
                        $SecurityEscalation = $false
                    }
                }
                if ($Rule.SendToManager.SecurityEscalation.Reminders) {
                    $SecurityEscalation = $false
                    if ($Rule.SendToManager.SecurityEscalation.Reminders.Default -and $Rule.SendToManager.SecurityEscalation.Reminders.Default.Enable -eq $true) {
                        $Rule.SendToManager.SecurityEscalation.Reminders.Default.Reminder = $Rule.SendToManager.SecurityEscalation.Reminders.Default.Reminder | ForEach-Object { $_ }
                        if ($User.DaysToExpire -in $Rule.SendToManager.SecurityEscalation.Reminders.Default.Reminder) {
                            $SecurityEscalation = $true
                        }
                    }
                    if ($Rule.SendToManager.SecurityEscalation.Reminders.OnDay -and $Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Enable -eq $true) {
                        foreach ($Day in $Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Days) {
                            if ($Day -eq "$($TodayDate.DayOfWeek)") {
                                if ($Rule.SendToManager.SecurityEscalation.Reminders.OnDay.ComparisonType -eq 'lt') {
                                    if ($User.DaysToExpire -lt $Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Reminder) {
                                        $SecurityEscalation = $true
                                        break
                                    }
                                } elseif ($Rule.SendToManager.SecurityEscalation.Reminders.OnDay.ComparisonType -eq 'gt') {
                                    if ($User.DaysToExpire -gt $Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Reminder) {
                                        $SecurityEscalation = $true
                                        break
                                    }
                                } elseif ($Rule.SendToManager.SecurityEscalation.Reminders.OnDay.ComparisonType -eq 'eq') {
                                    if ($User.DaysToExpire -eq $Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Reminder) {
                                        $SecurityEscalation = $true
                                        break
                                    }
                                } elseif ($Rule.SendtoManager.SecurityEscalation.Reminders.OnDay.ComparisonType -eq 'in') {
                                    if ($User.DaysToExpire -in $Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Reminder) {
                                        $SecurityEscalation = $true
                                        break
                                    }
                                }
                            }
                        }
                    }
                    if ($Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth -and $Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Enable -eq $true) {
                        foreach ($Day in $Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Days) {
                            if ($Day -eq $TodayDate.Day) {
                                if ($Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.ComparisonType -eq 'lt') {
                                    if ($User.DaysToExpire -lt $Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Reminder) {
                                        $SecurityEscalation = $true
                                        break
                                    }
                                } elseif ($Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.ComparisonType -eq 'gt') {
                                    if ($User.DaysToExpire -gt $Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Reminder) {
                                        $SecurityEscalation = $true
                                        break
                                    }
                                } elseif ($Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.ComparisonType -eq 'eq') {
                                    if ($User.DaysToExpire -eq $Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Reminder) {
                                        $SecurityEscalation = $true
                                        break
                                    }
                                } elseif ($Rule.SendtoManager.SecurityEscalation.Reminders.OnDayOfMonth.ComparisonType -eq 'in') {
                                    if ($User.DaysToExpire -in $Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Reminder) {
                                        $SecurityEscalation = $true
                                        break
                                    }
                                }
                            }
                        }
                    }
                }
                if ($SecurityEscalation) {
                    if ($Logging.NotifyOnUserMatchingRuleForSecurityEscalation) {
                        Write-Color -Text "[i]", " User (security escalation) ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire, " " -Color Yellow, White, Yellow, White, Yellow, White, White, Blue
                    }

                    if ($Summary['Rules'][$Rule.Name][$User.DistinguishedName]) {

                        $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleOptions.Add('Security esclation')
                        $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleName = $Rule.Name
                    } else {

                        $Summary['Rules'][$Rule.Name][$User.DistinguishedName] = [ordered] @{
                            User                = $User
                            Rule                = $Rule
                            ProcessManagersOnly = $Rule.ProcessManagersOnly
                        }
                        $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleOptions.Add('Security esclation')
                        $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleName = $Rule.Name
                    }

                    $Splat = [ordered] @{
                        SummaryDictionary = $Summary['NotifySecurity']
                        Type              = 'Security'
                        ManagerType       = 'Escalation'
                        Key               = $Rule.SendToManager.SecurityEscalation.Manager
                        User              = $User
                        Rule              = $Rule
                    }
                    Add-ManagerInformation @Splat
                }
            }
        }
    } else {
        if ($null -ne $Rule.Name -and $null -ne $Rule.Enable) {
            Write-Color -Text "[i]", " Processing rule ", $Rule.Name, ' status: ', $Rule.Enable -Color Red, White, Red, White, Red, White, Red, White
        }
    }
}
function New-HTMLReport {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Report,
        [System.Collections.IDictionary] $EmailParameters,
        [System.Collections.IDictionary] $Logging,
        [string] $SearchPath,
        [Array] $Rules,
        [System.Collections.IDictionary]  $UserSection,
        [System.Collections.IDictionary]  $ManagerSection,
        [System.Collections.IDictionary]  $SecuritySection,
        [System.Collections.IDictionary] $AdminSection,
        [System.Collections.IDictionary] $CachedUsers,
        [System.Collections.IDictionary] $Summary,
        [Array] $SummaryUsersEmails,
        [Array] $SummaryManagersEmails,
        [Array] $SummaryEscalationEmails,
        [System.Collections.IDictionary] $SummarySearch,
        [System.Collections.IDictionary] $Locations,
        [System.Collections.IDictionary] $AllSkipped,
        [System.Collections.IDictionary] $ExternalSystemReplacements
    )
    $TranslateOperators = @{
        'lt' = 'Less than'
        'gt' = 'Greater than'
        'eq' = 'Equal to'
        'ne' = 'Not equal to'
        'le' = 'Less than or equal to'
        'ge' = 'Greater than or equal to'
        'in' = 'In'
    }

    Write-Color -Text "[i]", " Generating HTML report ", $Report.Title -Color White, Yellow, Green
    if ($Report.DisableWarnings -eq $true) {
        $WarningAction = 'SilentlyContinue'
    } else {
        $WarningAction = 'Continue'
    }
    if (-not $Report.Title) {
        $Report.Title = "Password Solution Report"
    }

    New-HTML {
        New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey
        New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow
        New-HTMLPanelStyle -BorderRadius 0px
        New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ', ' -ArrayJoin

        New-HTMLHeader {
            New-HTMLSection -Invisible {
                New-HTMLSection {
                    New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue
                } -JustifyContent flex-start -Invisible
                New-HTMLSection {
                    New-HTMLText -Text "Password Solution - $($Script:Reporting['Version'])" -Color Blue
                } -JustifyContent flex-end -Invisible
            }
        }
        if ($Report.ShowConfiguration) {
            New-HTMLTab -Name "About" {
                New-HTMLTab -Name "Configuration" {
                    New-HTMLSection -Invisible {
                        New-HTMLSection -HeaderText "Email Configuration" {
                            New-HTMLList {
                                foreach ($Key in $EmailParameters.Keys) {
                                    if ($Key -eq 'Body') {

                                    } elseif ($Key -ne 'Password') {
                                        New-HTMLListItem -Text $Key, ": ", $EmailParameters[$Key] -FontWeight normal, normal, bold
                                    } else {
                                        New-HTMLListItem -Text $Key, ": ", "REDACTED" -FontWeight normal, normal, bold
                                    }
                                }
                            }
                        }
                        New-HTMLSection -HeaderText "Logging" {
                            New-HTMLList {
                                foreach ($Key in $Logging.Keys) {
                                    if ($Key -ne 'Password') {
                                        New-HTMLListItem -Text $Key, ": ", $Logging[$Key] -FontWeight normal, normal, bold
                                    } else {
                                        New-HTMLListItem -Text $Key, ": ", "REDACTED" -FontWeight normal, normal, bold
                                    }
                                }
                            }
                        }
                        New-HTMLSection -HeaderText "Other" {
                            New-HTMLList {
                                New-HTMLListItem -Text 'FilePath', ": ", $Report.FilePath -FontWeight normal, normal, bold
                                New-HTMLListItem -Text 'SearchPath', ": ", $SearchPath -FontWeight normal, normal, bold
                            }
                        }
                    }

                    New-HTMLSection -Invisible {
                        New-HTMLSection -HeaderText "User Section" {
                            New-HTMLList {
                                New-HTMLListItem -Text "Enabled: ", $UserSection.Enable -FontWeight normal, bold -TextDecoration underline, none
                                New-HTMLListItem -Text "SendCountMaximum: ", $UserSection.SendCountMaximum -FontWeight normal, bold -TextDecoration underline, none
                                New-HTMLListItem -Text "SendToDefaultEmail: ", $UserSection.SendToDefaultEmail -FontWeight normal, bold -TextDecoration underline, none
                                New-HTMLListItem -Text "DefaultEmail: ", ($UserSection.DefaultEmail -join ", ") -FontWeight normal, bold -TextDecoration underline, none
                            }
                        }
                        New-HTMLSection -HeaderText "Manager Section" {
                            New-HTMLList {
                                New-HTMLListItem -Text "Enabled: ", $ManagerSection.Enable -FontWeight normal, bold -TextDecoration underline, none
                                New-HTMLListItem -Text "SendCountMaximum: ", $ManagerSection.SendCountMaximum -FontWeight normal, bold -TextDecoration underline, none
                                New-HTMLListItem -Text "SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail -FontWeight normal, bold -TextDecoration underline, none
                                New-HTMLListItem -Text "DefaultEmail: ", ($ManagerSection.DefaultEmail -join ", ") -FontWeight normal, bold -TextDecoration underline, none
                            }
                        }
                        New-HTMLSection -HeaderText "Security Section" {
                            New-HTMLList {
                                New-HTMLListItem -Text "Enabled: ", $SecuritySection.Enable -FontWeight normal, bold -TextDecoration underline, none
                                New-HTMLListItem -Text "SendCountMaximum: ", $SecuritySection.SendCountMaximum -FontWeight normal, bold -TextDecoration underline, none
                                New-HTMLListItem -Text "SendToDefaultEmail: ", $SecuritySection.SendToDefaultEmail -FontWeight normal, bold -TextDecoration underline, none
                                New-HTMLListItem -Text "DefaultEmail: ", ($SecuritySection.DefaultEmail -join ", ") -FontWeight normal, bold -TextDecoration underline, none
                                New-HTMLListItem -Text "Attach CSV: ", ($SecuritySection.AttachCSV -join ",") -FontWeight normal, bold -TextDecoration underline, none
                            }
                        }
                        New-HTMLSection -HeaderText "Admin Section" {
                            New-HTMLList {
                                New-HTMLListItem -Text "Enabled: ", $AdminSection.Enable -FontWeight normal, bold -TextDecoration underline, none
                                New-HTMLListItem -Text "Subject: ", $AdminSection.Subject -FontWeight normal, bold -TextDecoration underline, none
                                New-HTMLListItem -Text "Manager: ", $AdminSection.Manager.DisplayName -FontWeight normal, bold -TextDecoration underline, none
                                New-HTMLListItem -Text "Manager Email: ", ($AdminSection.Manager.EmailAddress -join ", ") -FontWeight normal, bold -TextDecoration underline, none
                            }
                        }
                    }
                }
                New-HTMLTab -Name 'Rules Configuration' {
                    New-HTMLText -Text "There are ", $Rules.Count, " rules defined in the Password Solution. ", "Please keep in mind that order of the rules matter." -FontWeight normal, bold, normal -Color None, Blue, None

                    foreach ($Rule in $Rules) {
                        if ($Rule.Enable) {
                            $SectionColor = 'SpringGreen'
                        } else {
                            $SectionColor = 'Coral'
                        }
                        New-HTMLSection -HeaderText "Rule $($Rule.Name)" -CanCollapse -HeaderBackGroundColor $SectionColor {
                            New-HTMLList {
                                if ($Rule.Enable) {
                                    New-HTMLListItem -Text "Rule ", $Rule.Name, " is ", "enabled" -FontWeight normal, bold, normal, bold, normal, normal -Color None, None, None, Green
                                } else {
                                    New-HTMLListItem -Text "Rule ", $Rule.Name, " is ", "disabled" -FontWeight normal, bold, normal, bold, normal, normal -Color None, None, None, Red
                                }
                                New-HTMLList {
                                    New-HTMLListItem -Text "Notify till expiry on ", $($Rule.Reminders -join ","), " day " -FontWeight normal, bold, normal
                                    if ($Rule.IncludeExpiring) {
                                        New-HTMLListItem -Text "Include expiring accounts is ", "enabled" -FontWeight bold, bold -Color None, Green
                                    } else {
                                        New-HTMLListItem -Text "Include expiring accounts is ", "disabled" -FontWeight bold, bold -Color None, Red
                                    }
                                    if ($Rule.IncludePasswordNeverExpires) {
                                        New-HTMLListItem -Text "Include passwords never expiring with ", $Rule.PasswordNeverExpiresDays, " days rule" -FontWeight bold -Color Amethyst
                                    } else {
                                        New-HTMLListItem -Text "Do not include passwords that never expire." -FontWeight bold -Color Blue
                                    }
                                    if ($Rule.IncludeName.Count -gt 0 -and $Rule.IncludeNameProperties.Count -gt 0) {
                                        New-HTMLListItem -Text "Apply naming rule to require that account contains of of names ", $($Rule.IncludeName -join ", "), " in at least one property ", ($Rule.IncludeNameProperties -join ", ") -FontWeight normal, bold, normal, bold, normal -Color None, Blue, None, Blue
                                    } else {
                                        New-HTMLListItem -Text "Do not apply special name rules" -Color Blue -FontWeight bold
                                    }
                                    if ($Rule.IncludeOU) {
                                        New-HTMLListItem -Text "Apply Organizational Unit inclusion on ", ($Rule.IncludeOU -join ", ") -FontWeight normal, bold -Color None, Blue
                                    } else {
                                        New-HTMLListItem -Text "Do not apply Organizational Unit limit" -Color Blue -FontWeight bold
                                    }
                                    if ($Rule.ExcludeOU) {
                                        New-HTMLListItem -Text "Apply Organizational Unit exclusion on ", $Rule.ExcludeOU -FontWeight normal, bold -Color None, Green
                                    } else {
                                        New-HTMLListItem -Text "Do not exclude any Organizational Unit" -Color Blue -FontWeight bold
                                    }
                                    if ($Rule.IncludeGroup) {
                                        New-HTMLListItem -Text "Appply Group Membership inclusion (direct only) ", ($Rule.IncludeGroup -join ", ")
                                    } else {
                                        New-HTMLListItem -Text "Do not apply Group Membership limit"
                                    }
                                    if ($Rule.ExcludeGroup) {
                                        New-HTMLListItem -Text "Apply Group Membership exclusion (direct only): ", ($Rule.ExcludeGroup -join ", ")
                                    } else {
                                        New-HTMLListItem -Text "Do not apply Group Membership exclusion"
                                    }
                                    New-HTMLListItem -Text "Send to manager" -NestedListItems {
                                        New-HTMLList {
                                            if ($Rule.SendToManager.Manager.Enable) {
                                                New-HTMLListItem -Text "Manager ", " is ", 'enabled' -FontWeight bold, normal, bold -Color None, None, Green {
                                                    New-HTMLList {
                                                        New-HTMLListItem -Text "Rules: " {
                                                            New-HTMLList {
                                                                if ($Rule.SendToManager.Manager.Reminders.Default.Enable) {
                                                                    if ($Rule.SendToManager.Manager.Reminders.Default.Reminder) {
                                                                        New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $($Rule.SendToManager.Manager.Reminders.Default.Reminder -join ", "), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green
                                                                    } else {
                                                                        New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $($Rule.Reminders -join ", "), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green
                                                                    }
                                                                } else {
                                                                    New-HTMLListItem -Text "Default rule is ", "disabled" -FontWeight bold, bold -Color None, Red
                                                                }
                                                                if ($Rule.SendToManager.Manager.Reminders.OnDay.Enable) {
                                                                    New-HTMLListItem -Text @(
                                                                        "On day of the week ", "is ", "enabled"
                                                                        " on days: ", ($Rule.SendToManager.Manager.Reminders.OnDay.Days -join ", "),
                                                                        " with comparison ", $TranslateOperators[$Rule.SendToManager.Manager.Reminders.OnDay.ComparisonType],
                                                                        ' value ', $Rule.SendToManager.Manager.Reminders.OnDay.Reminder
                                                                    ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green
                                                                } else {
                                                                    New-HTMLListItem -Text "On day of week rule is ", "disabled" -FontWeight bold, bold -Color None, Red
                                                                }
                                                                if ($Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Enable) {
                                                                    New-HTMLListItem -Text @(
                                                                        "On day of the month rule ", "is", " enabled",
                                                                        " on days ", ($Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Days -join ","),
                                                                        " with comparison ", $TranslateOperators[$Rule.SendToManager.Manager.Reminders.OnDayOfMonth.ComparisonType],
                                                                        ' value ', $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Reminder
                                                                    ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green
                                                                } else {
                                                                    New-HTMLListItem -Text "On day of month rule is ", "disabled" -FontWeight bold, bold -Color None, Red
                                                                }
                                                            }
                                                        }
                                                    }
                                                }
                                            } else {
                                                New-HTMLListItem -Text "Manager ", " is ", 'disabled' -FontWeight bold, normal, bold -Color None, None, Red
                                            }
                                            if ($Rule.SendToManager.ManagerNotCompliant.Enable) {
                                                New-HTMLListItem -Text "Manager Escalation", " is ", 'enabled' -FontWeight bold, normal, bold -Color None, None, Green {
                                                    New-HTMLList {
                                                        New-HTMLListItem -Text "Manager Name: ", $Rule.SendToManager.ManagerNotCompliant.Manager.DisplayName -FontWeight normal, bold -TextDecoration underline, none
                                                        New-HTMLListItem -Text "Manager Email Address: ", $Rule.SendToManager.ManagerNotCompliant.Manager.EmailAddress -FontWeight normal, bold -TextDecoration underline, none
                                                    }
                                                    New-HTMLList {
                                                        New-HTMLListItem -Text "Rules: " {
                                                            New-HTMLList {
                                                                if ($Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Enable) {
                                                                    if ($Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Reminder) {
                                                                        New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $($Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Reminder -join ", "), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green
                                                                    } else {
                                                                        New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $($Rule.Reminders -join ", "), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green
                                                                    }
                                                                } else {
                                                                    New-HTMLListItem -Text "Default rule is ", "disabled" -FontWeight bold, bold -Color None, Red
                                                                }
                                                                if ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Enable) {
                                                                    New-HTMLListItem -Text @(
                                                                        "On day of the week ", "is ", "enabled"
                                                                        " on days: ", ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Days -join ", "),
                                                                        " with comparison ", $TranslateOperators[$Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.ComparisonType],
                                                                        ' value ', $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Reminder
                                                                    ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green
                                                                } else {
                                                                    New-HTMLListItem -Text "On day of week rule is ", "disabled" -FontWeight bold, bold -Color None, Red
                                                                }
                                                                if ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Enable) {
                                                                    New-HTMLListItem -Text @(
                                                                        "On day of the month rule ", "is", " enabled",
                                                                        " on days ", ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Days -join ", "),
                                                                        " with comparison ", $TranslateOperators[$Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.ComparisonType],
                                                                        ' value ', $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Reminder
                                                                    ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green
                                                                } else {
                                                                    New-HTMLListItem -Text "On day of month rule is ", "disabled" -FontWeight bold, bold -Color None, Red
                                                                }
                                                            }
                                                        }
                                                    }
                                                }
                                            } else {
                                                New-HTMLListItem -Text "Manager Escalation", " is ", "disabled" -FontWeight bold, normal, bold -Color None, None, Red
                                            }
                                            if ($Rule.SendToManager.SecurityEscalation.Enable) {
                                                New-HTMLListItem -Text "Security Escalation ", "is", " enabled" -FontWeight bold, normal, bold -Color None, None, Green {
                                                    New-HTMLList {
                                                        New-HTMLListItem -Text "Manager Name: ", $Rule.SendToManager.SecurityEscalation.Manager.DisplayName -FontWeight normal, bold -TextDecoration underline, none
                                                        New-HTMLListItem -Text "Manager Email Address: ", $Rule.SendToManager.SecurityEscalation.Manager.EmailAddress -FontWeight normal, bold -TextDecoration underline, none
                                                    }
                                                    New-HTMLList {
                                                        New-HTMLListItem -Text "Rules: " {
                                                            New-HTMLList {

                                                                if ($Rule.SendToManager.SecurityEscalation.Reminders.Default.Enable) {
                                                                    if ($Rule.SendToManager.SecurityEscalation.Reminders.Default.Reminder) {
                                                                        New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $($Rule.SendToManager.SecurityEscalation.Reminders.Default.Reminder -join ", "), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green
                                                                    } else {
                                                                        New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $($Rule.Reminders -join ", "), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green
                                                                    }
                                                                } else {
                                                                    New-HTMLListItem -Text "Default rule is ", "disabled" -FontWeight bold, bold -Color None, Red
                                                                }
                                                                if ($Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Enable) {
                                                                    New-HTMLListItem -Text @(
                                                                        "On day of the week ", "is ", "enabled"
                                                                        " on days: ", ($Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Days -join ", "),
                                                                        " with comparison ", $TranslateOperators[$Rule.SendToManager.SecurityEscalation.Reminders.OnDay.ComparisonType],
                                                                        ' value ', $Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Reminder
                                                                    ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green
                                                                } else {
                                                                    New-HTMLListItem -Text "On day of week rule is ", "disabled" -FontWeight bold, bold -Color None, Red
                                                                }
                                                                if ($Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Enable) {
                                                                    New-HTMLListItem -Text @(
                                                                        "On day of the month rule ", "is", " enabled",
                                                                        " on days ", ($Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Days -join ", "),
                                                                        " with comparison ", $TranslateOperators[$Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.ComparisonType],
                                                                        ' value ', $Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Reminder
                                                                    ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green
                                                                } else {
                                                                    New-HTMLListItem -Text "On day of month rule is ", "disabled" -FontWeight bold, bold -Color None, Red
                                                                }
                                                            }
                                                        }
                                                    }
                                                }
                                            } else {
                                                New-HTMLListItem -Text "Security Escalation", " is ", "disabled" -FontWeight bold, normal, bold -Color None, None, Red
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        if ($Report.ShowAllUsers) {
            $AllUsers = foreach ($User in $CachedUsers.Values) {
                if ($User.Type -eq 'Contact') {
                    continue
                }
                $User
            }
            New-HTMLTab -Name 'All Users' {
                New-HTMLTable -DataTable $AllUsers -Filtering {
                    New-TableCondition -Name 'Enabled' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq
                    New-TableCondition -Name 'HasMailbox' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq
                    New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -Value $false -ComparisonType string
                    New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string
                    New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string
                    New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor BlueSmoke -Value $true -ComparisonType string
                    New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor LawnGreen -Value $false -ComparisonType string
                    New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Missing', 'Disabled' -BackgroundColor Salmon -Operator in
                    New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Enabled' -BackgroundColor LawnGreen
                    New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Not available' -BackgroundColor BlueSmoke
                }
            }
        }
        if ($Report.ShowRules) {
            if ($Report.NestedRules) {

                if ($Summary['Rules'].Keys.Count -gt 0) {
                    New-HTMLTab -Name 'Rules Information' {
                        foreach ($Rule in  $Summary['Rules'].Keys) {
                            if ((Measure-Object -InputObject $Summary['Rules'][$Rule].Values.User).Count -gt 0) {
                                $Color = 'LawnGreen'
                                $IconSolid = 'Star'
                            } else {
                                $Color = 'Salmon'
                                $IconSolid = 'Stop'
                            }
                            New-HTMLTab -Name $Rule -TextColor $Color -IconColor $Color -IconSolid $IconSolid {
                                New-HTMLTable -DataTable $Summary['Rules'][$Rule].Values.User -Filtering {
                                    New-TableCondition -Name 'Enabled' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string
                                    New-TableCondition -Name 'HasMailbox' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq
                                    New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -Value $false -ComparisonType string
                                    New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string
                                    New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string
                                    New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor BlueSmoke -Value $true -ComparisonType string
                                    New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor LawnGreen -Value $false -ComparisonType string
                                    New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Missing', 'Disabled' -BackgroundColor Salmon -Operator in
                                    New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Enabled' -BackgroundColor LawnGreen
                                    New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Not available' -BackgroundColor BlueSmoke
                                }
                            }
                        }
                    }
                }
            } else {
                foreach ($Rule in  $Summary['Rules'].Keys) {
                    if ((Measure-Object -InputObject $Summary['Rules'][$Rule].Values.User).Count -gt 0) {
                        $Color = 'LawnGreen'
                        $IconSolid = 'Star'
                    } else {
                        $Color = 'Salmon'
                        $IconSolid = 'Stop'
                    }
                    New-HTMLTab -Name $Rule -TextColor $Color -IconColor $Color -IconSolid $IconSolid {
                        New-HTMLTable -DataTable $Summary['Rules'][$Rule].Values.User -Filtering {
                            New-TableCondition -Name 'Enabled' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string
                            New-TableCondition -Name 'HasMailbox' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq
                            New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -Value $false -ComparisonType string
                            New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string
                            New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string
                            New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor BlueSmoke -Value $true -ComparisonType string
                            New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor LawnGreen -Value $false -ComparisonType string
                            New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Missing', 'Disabled' -BackgroundColor Salmon -Operator in
                            New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Enabled' -BackgroundColor LawnGreen
                            New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Not available' -BackgroundColor BlueSmoke
                        }
                    }
                }
            }
        }
        if ($Report.ShowUsersSent) {
            if ((Measure-Object -InputObject $SummaryUsersEmails).Count -gt 0) {
                $Color = 'BrightTurquoise'
                $IconSolid = 'sticky-note'
            } else {
                $Color = 'Amaranth'
                $IconSolid = 'stop-circle'
            }
            New-HTMLTab -Name 'Email sent to users' -TextColor $Color -IconColor $Color -IconSolid $IconSolid {
                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 -Value $false -ComparisonType string
                    New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string
                    New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string
                } -Filtering
            }
        }
        if ($Report.ShowManagersSent) {
            if ((Measure-Object -InputObject $SummaryManagersEmails).Count -gt 0) {
                $Color = 'BrightTurquoise'
                $IconSolid = 'sticky-note'
            } else {
                $Color = 'Amaranth'
                $IconSolid = 'stop-circle'
            }
            New-HTMLTab -Name 'Email sent to manager' -TextColor $Color -IconColor $Color -IconSolid $IconSolid {
                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'
                } -Filtering
            }
        }
        if ($Report.ShowEscalationSent) {
            if ((Measure-Object -InputObject $SummaryEscalationEmails).Count -gt 0) {
                $Color = 'BrightTurquoise'
                $IconSolid = 'sticky-note'
            } else {
                $Color = 'Amaranth'
                $IconSolid = 'stop-circle'
            }
            New-HTMLTab -Name 'Email sent to Security' -TextColor $Color -IconColor $Color -IconSolid $IconSolid {
                New-HTMLTable -DataTable $SummaryEscalationEmails {
                    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'
                } -Filtering
            }
        }
        if ($Report.ShowExternalSystemReplacementsUsers) {
            if ($ExternalSystemReplacements.Users.Count -gt 0) {
                $Color = 'BrightTurquoise'
                $IconSolid = 'sticky-note'
            } else {
                $Color = 'Amaranth'
                $IconSolid = 'stop-circle'
            }
            New-HTMLTab -Name 'External System Users' -TextColor $Color -IconColor $Color -IconSolid $IconSolid {
                New-HTMLTable -DataTable $ExternalSystemReplacements.Users {

                } -Filtering
            }
        }
        if ($Report.ShowExternalSystemReplacementsManagers) {
            if ($ExternalSystemReplacements.Managers.Count -gt 0) {
                $Color = 'BrightTurquoise'
                $IconSolid = 'sticky-note'
            } else {
                $Color = 'Amaranth'
                $IconSolid = 'stop-circle'
            }
            New-HTMLTab -Name 'External System Managers' -TextColor $Color -IconColor $Color -IconSolid $IconSolid {
                New-HTMLTable -DataTable $ExternalSystemReplacements.Managers {

                } -Filtering
            }
        }
        if ($Report.ShowSearchUsers) {
            [Array] $UsersSent = $SummarySearch['EmailSent'].Values 
            if ($UsersSent.Count -gt 0) {
                $Color = 'BrightTurquoise'
                $IconSolid = 'sticky-note'
            } else {
                $Color = 'Amaranth'
                $IconSolid = 'stop-circle'
            }
            New-HTMLTab -Name 'History Emails To Users' -TextColor $Color -IconColor $Color -IconSolid $IconSolid {
                New-HTMLTable -DataTable $UsersSent {
                    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 -Value $false -ComparisonType string
                    New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string
                    New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string
                } -Filtering
            }
        }
        if ($Report.ShowSearchManagers) {
            [Array] $ShowSearchManagers = $SummarySearch['EmailManagers'].Values 
            if ($ShowSearchManagers.Count -gt 0) {
                $Color = 'BrightTurquoise'
                $IconSolid = 'sticky-note'
            } else {
                $Color = 'Amaranth'
                $IconSolid = 'stop-circle'
            }
            New-HTMLTab -Name 'History Emails To Managers' -TextColor $Color -IconColor $Color -IconSolid $IconSolid {
                New-HTMLTable -DataTable $ShowSearchManagers {
                    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'
                } -Filtering
            }
        }
        if ($Report.ShowSearchEscalations) {
            [Array] $ShowSearchEscalations = $SummarySearch['EmailEscalations'].Values 
            if ($ShowSearchEscalations.Count -gt 0) {
                $Color = 'BrightTurquoise'
                $IconSolid = 'sticky-note'
            } else {
                $Color = 'Amaranth'
                $IconSolid = 'stop-circle'
            }
            New-HTMLTab -Name 'History Email To Security' -TextColor $Color -IconColor $Color -IconSolid $IconSolid {
                New-HTMLTable -DataTable $ShowSearchEscalations {
                    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'
                } -Filtering
            }
        }
        if ($Report.ShowSkippedUsers) {
            New-HTMLTab -Name 'Skipped Users' -IconSolid users {
                $SkippedUsers = foreach ($User in  $AllSkipped.Values) {
                    if ($User.Type -ne 'Contact') {
                        $User
                    }
                }
                New-HTMLPanel -AlignContentText center {
                    New-HTMLText -FontSize 15pt -Text "Those users have no password date set. This means account running expiration checks doesn't have permissions or acccout never had password set or account is set to change password on logon. "
                } -Invisible
                New-HTMLTable -DataTable $SkippedUsers -Filtering {
                    New-TableCondition -Name 'Enabled' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq
                    New-TableCondition -Name 'HasMailbox' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq
                    New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -Value $false -ComparisonType string
                    New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string
                    New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string
                    New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor BlueSmoke -Value $true -ComparisonType string
                    New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor LawnGreen -Value $false -ComparisonType string
                    New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Missing', 'Disabled' -BackgroundColor Salmon -Operator in
                    New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Enabled' -BackgroundColor LawnGreen
                    New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Not available' -BackgroundColor BlueSmoke
                }
            }
        }
        if ($Report.ShowSkippedLocations) {
            New-HTMLTab -Name 'Skipped Locations' -IconSolid building {
                New-HTMLPanel -AlignContentText center {
                    New-HTMLText -FontSize 15pt -Text "Users in those Organizational Units have no password date set. This means account running expiration checks doesn't have permissions or acccout never had password set or account is set to change password on logon. "
                } -Invisible
                New-HTMLTable -DataTable $Locations.Values -Filtering {
                    New-TableHeader -ResponsiveOperations none -Names 'Names', 'NamesExpired'
                }
            }
        }
    } -ShowHTML:$Report.ShowHTML -FilePath $Report.FilePath -Online:$Report.Online -WarningAction $WarningAction -TitleText $Report.Title

    Write-Color -Text "[i]" , " Generating HTML report ", $Report.Title, ". Done" -Color White, Yellow, Green
}
function Send-PasswordAdminNotifications {
    [CmdletBinding()]
    param(
        $AdminSection,
        $TemplateAdmin,
        $TemplateAdminSubject,
        $TimeEnd,
        $EmailParameters,
        $HtmlAttachments,
        $Logging
    )

    if ($AdminSection.Enable) {
        Write-Color -Text "[i] Sending summary information " -Color White, Yellow, White, Yellow, White, Yellow, White
        $CountSecurity = 0
        [Array] $SummaryEmail = @(
            $CountSecurity++

            $ManagerUser = $AdminSection.Manager

            $EmailSplat = [ordered] @{}

            $EmailSplat.Template = $TemplateAdmin
            $EmailSplat.Subject = $TemplateAdminSubject
            $EmailSplat.User = $ManagerUser

            $EmailSplat.SummaryUsersEmails = $SummaryUsersEmails
            $EmailSplat.SummaryManagersEmails = $SummaryManagersEmails
            $EmailSplat.SummaryEscalationEmails = $SummaryEscalationEmails
            $EmailSplat.TimeToProcess = $TimeEnd

            $EmailSplat.EmailParameters = $EmailParameters

            $EmailSplat.EmailParameters.To = $AdminSection.Manager.EmailAddress

            $EmailSplat.EmailDateFormat = $Logging.EmailDateFormat
            $EmailSplat.EmailDateFormatUTCConversion = $Logging.EmailDateFormatUTCConversion

            if ($HtmlAttachments.Count -gt 0) {
                $EmailSplat.Attachments = $HtmlAttachments
            }
            Write-Color -Text "[i] Sending summary information ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow

            $EmailResult = Send-PasswordEmail @EmailSplat

            Write-Color -Text "[r] Sending summary information ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ") (status: ", $EmailResult.Status, " sent to: ", $EmailResult.SentTo, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow
            if ($EmailResult.Error) {
                Write-Color -Text "[r] Error: ", $EmailResult.Error -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow
            }
            [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
                Template       = 'Unknown'
            }
        )
        Write-Color -Text "[i] Sending summary information (sent: ", $SummaryEmail.Count, ")" -Color White, Yellow, White, Yellow, White, Yellow, White
    } else {
        Write-Color -Text "[i] Sending summary information is ", "disabled!" -Color White, Yellow, DarkMagenta
    }
}
function Send-PasswordEmail {
    [CmdletBinding()]
    param(
        [scriptblock] $Template,
        [PSCustomObject] $User,
        [Array] $ManagedUsers,
        [Array] $ManagedUsersManagerNotCompliant,
        [Array] $SummaryUsersEmails,
        [Array] $SummaryManagersEmails,
        [Array] $SummaryEscalationEmails,
        [string] $TimeToProcess,
        [Array] $Attachments,
        [System.Collections.IDictionary] $EmailParameters,
        [string] $Subject,
        [string] $EmailDateFormat,
        [switch] $EmailDateFormatUTCConversion
    )

    if ($Template) {
        if ($User.PasswordLastSet) {
            if ($EmailDateFormat) {
                if ($EmailDateFormatUTCConversion) {
                    $PasswordLastSet = $User.PasswordLastSet.ToUniversalTime().ToString($EmailDateFormat)
                } else {
                    $PasswordLastSet = $User.PasswordLastSet.ToString($EmailDateFormat)
                }
            } else {
                if ($EmailDateFormatUTCConversion) {
                    $PasswordLastSet = $User.PasswordLastSet.ToUniversalTime()
                } else {
                    $PasswordLastSet = $User.PasswordLastSet
                }
            }
        } else {
            $PasswordLastSet = $User.PasswordLastSet
        }
        if ($User.DateExpiry) {
            if ($EmailDateFormat) {
                if ($EmailDateFormatUTCConversion) {
                    $ExpiryDate = $User.DateExpiry.ToUniversalTime().ToString($EmailDateFormat)
                } else {
                    $ExpiryDate = $User.DateExpiry.ToString($EmailDateFormat)
                }
            } else {
                if ($EmailDateFormatUTCConversion) {
                    $ExpiryDate = $User.DateExpiry.ToUniversalTime()
                } else {
                    $ExpiryDate = $User.DateExpiry
                }
            }
        } else {
            $ExpiryDate = $User.DateExpiry
        }

        $SourceParameters = [ordered] @{
            ManagerDisplayName                   = $User.DisplayName
            ManagerUsersTable                    = $ManagedUsers
            ManagerUsersTableManagerNotCompliant = $ManagedUsersManagerNotCompliant
            SummaryEscalationEmails              = $SummaryEscalationEmails
            SummaryManagersEmails                = $SummaryManagersEmails
            SummaryUsersEmails                   = $SummaryUsersEmails
            TimeToProcess                        = $TimeToProcess

            UserPrincipalName                    = $User.UserPrincipalName     
            SamAccountName                       = $User.SamAccountName        
            Domain                               = $User.Domain                
            Enabled                              = $User.Enabled
            EmailAddress                         = $User.EmailAddress          
            DateExpiry                           = $ExpiryDate            
            DaysToExpire                         = $User.DaysToExpire          
            PasswordExpired                      = $User.PasswordExpired       
            PasswordLastSet                      = $PasswordLastSet     
            PasswordNotRequired                  = $User.PasswordNotRequired   
            PasswordNeverExpires                 = $User.PasswordNeverExpires  
            ManagerSamAccountName                = $User.ManagerSamAccountName 
            ManagerEmail                         = $User.ManagerEmail          
            ManagerStatus                        = $User.ManagerStatus         
            ManagerLastLogonDays                 = $User.ManagerLastLogonDays  
            Manager                              = $User.Manager               
            DisplayName                          = $User.DisplayName           
            GivenName                            = $User.GivenName             
            Surname                              = $User.Surname               
            OrganizationalUnit                   = $User.OrganizationalUnit    
            MemberOf                             = $User.MemberOf              
            DistinguishedName                    = $User.DistinguishedName     
            ManagerDN                            = $User.ManagerDN             
        }
        $Body = EmailBody -EmailBody $Template -Parameter $SourceParameters

        $EmailParameters.Subject = Add-ParametersToString -String $Subject -Parameter $SourceParameters
        $EmailParameters.Body = $Body
        if ($Attachments) {
            $EmailParameters.Attachment = $Attachments
        } else {
            $EmailParameters.Attachment = @()
        }
        try {
            Send-EmailMessage @EmailParameters -ErrorAction Stop -WarningAction SilentlyContinue
        } catch {
            if ($_.Exception.Message -like "*Credential*") {
                Write-Color -Text "[e] " , "Failed to send email to $($EmailParameters.EmailParameters) because error: $($_.Exception.Message)" -Color Yellow, White, Red
                Write-Color -Text "[i] " , "Please make sure you have valid credentials in your configuration file (graph encryption issue?)" -Color Yellow, White, Red
            } else {
                Write-Color -Text "[e] " , "Failed to send email to $($EmailParameters.EmailParameters) because error: $($_.Exception.Message)" -Color Yellow, White, Red
            }
        }
    }
}
function Send-PasswordManagerNofifications {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $ManagerSection,
        [System.Collections.IDictionary] $Summary,
        [System.Collections.IDictionary] $CachedUsers,
        [ScriptBlock] $TemplateManager,
        [string] $TemplateManagerSubject,
        [ScriptBlock] $TemplateManagerNotCompliant,
        [string] $TemplateManagerNotCompliantSubject,
        [System.Collections.IDictionary] $EmailParameters,
        [System.Collections.IDictionary] $Logging,
        [System.Collections.IDictionary] $GlobalManagersCache
    )
    if ($ManagerSection.Enable) {
        Write-Color -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]) {

                $ManagerUser = $CachedUsers[$Manager]
            } elseif ($GlobalManagersCache[$Manager]) {

                $ManagerUser = $GlobalManagersCache[$Manager]
            } else {

                $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) {

                    $EmailSplat.Template = $TemplateManager
                } else {

                    $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) {

                    $EmailSplat.Template = $TemplateManagerNotCompliant
                } else {

                    $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.EmailParameters = $EmailParameters

            $EmailSplat.EmailDateFormat = $Logging.EmailDateFormat
            $EmailSplat.EmailDateFormatUTCConversion = $Logging.EmailDateFormatUTCConversion

            if ($ManagerSection.SendToDefaultEmail -ne $true) {
                $EmailSplat.EmailParameters.To = $ManagerUser.EmailAddress
            } else {
                $EmailSplat.EmailParameters.To = $ManagerSection.DefaultEmail
            }
            if ($Logging.NotifyOnManagerSend) {
                Write-Color -Text "[i] Sending notifications to managers ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow
            }
            $EmailResult = Send-PasswordEmail @EmailSplat
            if ($Logging.NotifyOnManagerSend) {
                if ($EmailResult.SentTo) {
                    Write-Color -Text "[r] Sending notifications to managers ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ") (status: ", $EmailResult.Status, " sent to: ", $EmailResult.SentTo, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow
                } else {
                    Write-Color -Text "[r] Sending notifications to managers ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ") (status: ", $EmailResult.Status -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow
                }
            }

            [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

            }
            if ($ManagerSection.SendCountMaximum -gt 0) {
                if ($ManagerSection.SendCountMaximum -le $CountManagers) {
                    Write-Color -Text "[i]", " Send count maximum reached. There may be more managers that match the rule." -Color Red, DarkRed
                    break
                }
            }
        }
        Write-Color -Text "[i] Sending notifications to managers (sent: ", $SummaryManagersEmails.Count, " out of ", $Summary['NotifyManager'].Values.Count, ")" -Color White, Yellow, White, Yellow, White, Yellow, White
        $SummaryManagersEmails
    } else {
        Write-Color -Text "[i] Sending notifications to managers is ", "disabled!" -Color White, Yellow, DarkRed
    }
}
function Send-PasswordSecurityNotifications {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $SecuritySection,
        [System.Collections.IDictionary] $Summary,
        [ScriptBlock]  $TemplateSecurity,
        [string] $TemplateSecuritySubject,
        [System.Collections.IDictionary] $Logging
    )

    if ($SecuritySection.Enable) {
        Write-Color -Text "[i] Sending notifications to security " -Color White, Yellow, White, Yellow, White, Yellow, White
        $CountSecurity = 0
        [Array] $SummaryEscalationEmails = foreach ($Manager in $Summary['NotifySecurity'].Keys) {
            $CountSecurity++

            $ManagerUser = $Summary['NotifySecurity'][$Manager]['Manager']

            [Array] $ManagedUsers = $Summary['NotifySecurity'][$Manager]['Security'].Values.Output

            $EmailSplat = [ordered] @{}

            if ($Summary['NotifySecurity'][$Manager].Security.Count -gt 0) {

                $EmailSplat.Template = $TemplateSecurity
                if ($TemplateSecuritySubject) {
                    $EmailSplat.Subject = $TemplateSecuritySubject
                } else {
                    $EmailSplat.Subject = "[Password Expiring] Dear Security - Accounts expired"
                }
            } else {
                continue
            }

            if ($SecuritySection.AttachCSV -and $ManagedUsers.Count -gt 0) {
                $ManagedUsers | Export-Csv -LiteralPath $Env:TEMP\ManagedUsersSecurity.csv -NoTypeInformation -Force -Encoding UTF8 -ErrorAction Stop
                $EmailSplat.Attachments = @(
                    if (Test-Path -LiteralPath "$Env:TEMP\ManagedUsersSecurity.csv") {
                        "$Env:TEMP\ManagedUsersSecurity.csv"
                    }
                )
            }
            $EmailSplat.User = $ManagerUser
            $EmailSplat.ManagedUsers = $ManagedUsers | Select-Object -Property 'Status', 'DisplayName', 'Enabled', 'SamAccountName', 'Domain', 'DateExpiry', 'DaysToExpire', 'PasswordLastSet', 'PasswordExpired'

            $EmailSplat.EmailParameters = $EmailParameters

            $EmailSplat.EmailDateFormat = $Logging.EmailDateFormat
            $EmailSplat.EmailDateFormatUTCConversion = $Logging.EmailDateFormatUTCConversion

            if ($SecuritySection.SendToDefaultEmail -ne $true) {
                $EmailSplat.EmailParameters.To = $ManagerUser.EmailAddress
            } else {
                $EmailSplat.EmailParameters.To = $SecuritySection.DefaultEmail
            }
            if ($Logging.NotifyOnSecuritySend) {
                Write-Color -Text "[i] Sending notifications to security ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow
            }
            $EmailResult = Send-PasswordEmail @EmailSplat
            if ($Logging.NotifyOnSecuritySend) {
                Write-Color -Text "[r] Sending notifications to security ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ") (status: ", $EmailResult.Status, " sent to: ", $EmailResult.SentTo, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow
            }
            [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'

            }
            if ($SecuritySection.SendCountMaximum -gt 0) {
                if ($SecuritySection.SendCountMaximum -le $CountSecurity) {
                    Write-Color -Text "[i]", " Send count maximum reached. There may be more managers that match the rule." -Color Red, DarkRed
                    break
                }
            }
        }
        Write-Color -Text "[i] Sending notifications to security (sent: ", $SummaryEscalationEmails.Count, " out of ", $Summary['NotifySecurity'].Values.Count, ")" -Color White, Yellow, White, Yellow, White, Yellow, White
        $SummaryEscalationEmails
    } else {
        Write-Color -Text "[i] Sending notifications to security is ", "disabled!" -Color White, Yellow, DarkRed
    }

}
function Send-PasswordUserNofifications {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $UserSection,
        [System.Collections.IDictionary] $Summary,
        [System.Collections.IDictionary] $Logging,
        [ScriptBlock] $TemplatePreExpiry,
        [string] $TemplatePreExpirySubject,
        [scriptBlock] $TemplatePostExpiry,
        [string] $TemplatePostExpirySubject,
        [System.Collections.IDictionary] $EmailParameters
    )
    if ($UserSection.Enable) {
        Write-Color -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++
            $User = $Notify.User
            $Rule = $Notify.Rule

            if ($Notify.ProcessManagersOnly -eq $true) {
                if ($Logging.NotifyOnSkipUserManagerOnly) {
                    Write-Color -Text "[i]", " Skipping User (Manager Only - $($Rule.Name)) ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire -Color Yellow, White, Magenta, White, Magenta, White, White, Blue
                }
                continue
            }

            $EmailSplat = [ordered] @{}

            if ($Notify.User.DaysToExpire -ge 0) {
                if ($Notify.Rule.TemplatePreExpiry) {

                    $EmailSplat.Template = $Notify.Rule.TemplatePreExpiry
                } elseif ($TemplatePreExpiry) {

                    $EmailSplat.Template = $TemplatePreExpiry
                } else {

                    $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 ($DaysToExpire 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 ($DaysToExpire days ago)'
                }
            }
            $EmailSplat.User = $Notify.User
            $EmailSplat.EmailParameters = $EmailParameters

            $EmailSplat.EmailDateFormat = $Logging.EmailDateFormat
            $EmailSplat.EmailDateFormatUTCConversion = $Logging.EmailDateFormatUTCConversion

            if ($UserSection.SendToDefaultEmail -ne $true) {
                $EmailSplat.EmailParameters.To = $Notify.User.EmailAddress
            } else {
                $EmailSplat.EmailParameters.To = $UserSection.DefaultEmail
            }
            if ($Notify.User.EmailAddress -like "*@*") {

                $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
                    EmailFrom            = $EmailSplat.User.EmailFrom
                }
            } else {

                $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
                    EmailFrom            = $EmailSplat.User.EmailFrom
                }
            }
            if ($Logging.NotifyOnUserSend) {
                if ($EmailResult.SentTo) {
                    Write-Color -Text "[i]", " Sending notifications to user ", $Notify.User.DisplayName, " (", $Notify.User.EmailAddress, ")", " status: ", $EmailResult.Status, " sent to: ", $EmailResult.SentTo, ", details: ", $EmailResult.Error -Color Yellow, White, Yellow, White, Yellow, White, White, Blue, White, Blue
                } else {
                    Write-Color -Text "[i]", " Skipping notifications to user ", $Notify.User.DisplayName, " (", $Notify.User.EmailAddress, ")", " status: ", $EmailResult.Status, " details: ", $EmailResult.Error -Color Yellow, White, Yellow, White, Yellow, White, White, Blue, White, Blue
                }
            }
            if ($UserSection.SendCountMaximum -gt 0) {
                if ($UserSection.SendCountMaximum -le $CountUsers) {
                    Write-Color -Text "[i]", " Send count maximum reached. There may be more accounts that match the rule." -Color Red, DarkRed
                    break
                }
            }
        }
        Write-Color -Text "[i] Sending notifications to users (sent: ", $SummaryUsersEmails.Count, " out of ", $Summary['Notify'].Values.Count, ")" -Color White, Yellow, White, Yellow, White, Yellow, White
        $SummaryUsersEmails
    } else {
        Write-Color -Text "[i] Sending notifications to users is ", "disabled!" -Color White, Yellow, DarkRed
    }

}
function Set-LoggingCapabilities {
    [CmdletBinding()]
    param(
        [string] $LogPath,
        [int] $LogMaximum,
        [switch] $ShowTime,
        [string] $TimeFormat
    )

    $Script:PSDefaultParameterValues = @{
        "Write-Color:LogFile"    = $LogPath
        "Write-Color:ShowTime"   = if ($PSBoundParameters.ContainsKey('ShowTime')) {
            $ShowTime.IsPresent } else {
            $null }
        "Write-Color:TimeFormat" = $TimeFormat
    }
    Remove-EmptyValue -Hashtable $Script:PSDefaultParameterValues

    if ($LogPath) {
        $FolderPath = [io.path]::GetDirectoryName($LogPath)
        if (-not (Test-Path -LiteralPath $FolderPath)) {
            $null = New-Item -Path $FolderPath -ItemType Directory -Force -WhatIf:$false
        }
        if ($LogMaximum -gt 0) {
            $CurrentLogs = Get-ChildItem -LiteralPath $FolderPath | Sort-Object -Property CreationTime -Descending | Select-Object -Skip $LogMaximum
            if ($CurrentLogs) {
                Write-Color -Text '[i] ', "Logs directory has more than ", $LogMaximum, " log files. Cleanup required..." -Color Yellow, DarkCyan, Red, DarkCyan
                foreach ($Log in $CurrentLogs) {
                    try {
                        Remove-Item -LiteralPath $Log.FullName -Confirm:$false -WhatIf:$false
                        Write-Color -Text '[+] ', "Deleted ", "$($Log.FullName)" -Color Yellow, White, Green
                    } catch {
                        Write-Color -Text '[-] ', "Couldn't delete log file $($Log.FullName). Error: ', "$($_.Exception.Message) -Color Yellow, White, Red
                    }
                }
            }
        } else {
            Write-Color -Text '[i] ', "LogMaximum is set to 0 (Unlimited). No log files will be deleted." -Color Yellow, DarkCyan
        }
    }
}
function Set-PasswordConfiguration {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Logging,
        [scriptblock] $ConfigurationDSL,
        [scriptblock] $TemplatePreExpiry,
        [string] $TemplatePreExpirySubject,
        [scriptblock] $TemplatePostExpiry,
        [string] $TemplatePostExpirySubject,
        [scriptblock] $TemplateManager,
        [string] $TemplateManagerSubject,
        [scriptblock] $TemplateSecurity,
        [string] $TemplateSecuritySubject,
        [scriptblock] $TemplateManagerNotCompliant,
        [string] $TemplateManagerNotCompliantSubject,
        [scriptblock] $TemplateAdmin,
        [string] $TemplateAdminSubject,
        [System.Collections.IDictionary] $EmailParameters,
        [System.Collections.IDictionary] $UserSection,
        [System.Collections.IDictionary] $ManagerSection,
        [System.Collections.IDictionary] $SecuritySection,
        [System.Collections.IDictionary] $AdminSection,
        [System.Collections.IDictionary] $UsersExternalSystem,
        [Array] $HTMLReports,
        [Array] $Rules,
        [string] $SearchPath,
        [string] $OverwriteEmailProperty,
        [string] $OverwriteManagerProperty,
        [string[]] $FilterOrganizationalUnit
    )

    if (-not $Rules) {
        $Rules = @() 
    }
    if (-not $HTMLReports) {
        $HTMLReports = @() 
    }

    if ($ConfigurationDSL) {
        try {
            $ConfigurationExecuted = & $ConfigurationDSL
            foreach ($Configuration in $ConfigurationExecuted) {
                if ($Configuration.Type -eq 'PasswordConfigurationOption') {
                    if ($Configuration.Settings.SearchPath) {
                        $SearchPath = $Configuration.Settings.SearchPath
                    }
                    if ($Configuration.Settings.OverwriteEmailProperty) {
                        $OverwriteEmailProperty = $Configuration.Settings.OverwriteEmailProperty
                    }
                    if ($Configuration.Settings.OverwriteManagerProperty) {
                        $OverwriteManagerProperty = $Configuration.Settings.OverwriteManagerProperty
                    }
                    if ($Configuration.Settings.FilterOrganizationalUnit) {
                        $FilterOrganizationalUnit = $Configuration.Settings.FilterOrganizationalUnit
                    }
                    foreach ($Setting in $Configuration.Settings.Keys) {
                        if ($Setting -notin 'SearchPath', 'OverwriteEmailProperty', 'OverwriteManagerProperty', 'FilterOrganizationalUnit') {
                            $Logging[$Setting] = $Configuration.Settings[$Setting]
                        }
                    }
                } elseif ($Configuration.Type -eq 'PasswordConfigurationEmail') {
                    $EmailParameters = $Configuration.Settings
                } elseif ($Configuration.Type -eq 'PasswordConfigurationTypeUser') {
                    $UserSection = $Configuration.Settings
                } elseif ($Configuration.Type -eq 'PasswordConfigurationTypeManager') {
                    $ManagerSection = $Configuration.Settings
                } elseif ($Configuration.Type -eq 'PasswordConfigurationTypeSecurity') {
                    $SecuritySection = $Configuration.Settings
                } elseif ($Configuration.Type -eq 'PasswordConfigurationTypeAdmin') {
                    $AdminSection = $Configuration.Settings
                } elseif ($Configuration.Type -eq 'PasswordConfigurationReport') {
                    $HTMLReports += $Configuration.Settings
                } elseif ($Configuration.Type -eq 'PasswordConfigurationRule') {
                    $Rules += $Configuration.Settings
                } elseif ($Configuration.Type -eq "PasswordConfigurationTemplatePreExpiry") {
                    $TemplatePreExpiry = $Configuration.Settings.Template
                    $TemplatePreExpirySubject = $Configuration.Settings.Subject
                } elseif ($Configuration.Type -eq "PasswordConfigurationTemplatePostExpiry") {
                    $TemplatePostExpiry = $Configuration.Settings.Template
                    $TemplatePostExpirySubject = $Configuration.Settings.Subject
                } elseif ($Configuration.Type -eq "PasswordConfigurationTemplateManager") {
                    $TemplateManager = $Configuration.Settings.Template
                    $TemplateManagerSubject = $Configuration.Settings.Subject
                } elseif ($Configuration.Type -eq "PasswordConfigurationTemplateSecurity") {
                    $TemplateSecurity = $Configuration.Settings.Template
                    $TemplateSecuritySubject = $Configuration.Settings.Subject
                } elseif ($Configuration.Type -eq "PasswordConfigurationTemplateManagerNotCompliant") {
                    $TemplateManagerNotCompliant = $Configuration.Settings.Template
                    $TemplateManagerNotCompliantSubject = $Configuration.Settings.Subject
                } elseif ($Configuration.Type -eq "PasswordConfigurationTemplateAdmin") {
                    $TemplateAdmin = $Configuration.Settings.Template
                    $TemplateAdminSubject = $Configuration.Settings.Subject
                } elseif ($Configuration.Type -eq 'ExternalUsers') {
                    $UsersExternalSystem = $Configuration
                }
            }
        } catch {
            Write-Color -Text "[e]", " Processing configuration failed because of error in line ", $_.InvocationInfo.ScriptLineNumber, " in ", $_.InvocationInfo.InvocationName, " with message: ", $_.Exception.Message -Color Yellow, White, Red
            return
        }
    }

    if (-not $TemplatePreExpiry) {
        Write-Color -Text "[i]", " TemplatePreExpiry not defined. Using default template (built-in)" -Color Yellow, Red
        $TemplatePreExpiry = {
            EmailText -LineBreak
            EmailText -Text "Dear ", "$DisplayName," -LineBreak
            EmailText -Text "Your password will expire in $DaysToExpire days and if you do not change it, you will not be able to connect to the Network and IT services. "

            EmailText -Text "Depending on your situation, please follow one of the methods below to change your password." -LineBreak

            EmailText -Text "If you are connected to the Internal Network (either directly or through VPN):"
            EmailList {
                EmailListItem -Text "Press CTRL+ALT+DEL"
                EmailListItem -Text "Choose Change password"
                EmailListItem -Text "Type in your old password and then type the new one according to the password policy (twice)"
                EmailListItem -Text "After the change is complete you will be prompted with information that the password has been changed"
            }

            EmailText -Text "If you are not connected to the Internal Network:"
            EmailList {
                EmailListItem -Text "Open [Password Change Link](https://account.activedirectory.windowsazure.com/ChangePassword.aspx) using your web browser"
                EmailListItem -Text "Login using your current credentials"
                EmailListItem -Text "On the change password form, type your old password and the new password that you want to set (twice)"
                EmailListItem -Text "Click Submit"
            }
            EmailText -Text "Please also remember to modify your password on the email configuration of your Smartphone or Tablet." -LineBreak
            EmailText -Text "Kind regards,"
            EmailText -Text "IT Service Desk"
        }
    }
    if (-not $TemplatePreExpirySubject) {
        Write-Color -Text "[i]", " TemplatePreExpirySubject not defined. Using default template (built-in)" -Color Yellow, Red
        $TemplatePreExpirySubject = '[Password Expiring] Your password will expire on $DateExpiry ($DaysToExpire days)'
    }

    if (-not $TemplatePostExpiry) {
        Write-Color -Text "[i]", " TemplatePostExpiry not defined. Using default template (built-in)" -Color Yellow, Red

        $TemplatePostExpiry = {
            EmailText -LineBreak
            EmailText -Text "Dear ", "$DisplayName," -LineBreak
            EmailText -Text "Your password already expired on $PasswordLastSet. If you do not change it, you will not be able to connect to the Network and IT services. "

            EmailText -Text "Depending on your situation, please follow one of the methods below to change your password." -LineBreak

            EmailText -Text "If you are connected to the Network (either directly or through VPN):"
            EmailList {
                EmailListItem -Text "Press CTRL+ALT+DEL"
                EmailListItem -Text "Choose Change password"
                EmailListItem -Text "Type in your old password and then type the new one according to the password policy (twice)"
                EmailListItem -Text "After the change is complete you will be prompted with information that the password has been changed"
            }

            EmailText -Text "If you are not connected to the Internal Network:"
            EmailList {
                EmailListItem -Text "Open [Password Change Link](https://account.activedirectory.windowsazure.com/ChangePassword.aspx) using your web browser"
                EmailListItem -Text "Login using your current credentials"
                EmailListItem -Text "On the change password form, type your old password and the new password that you want to set (twice)"
                EmailListItem -Text "Click Submit"
            }
            EmailText -Text "Please also remember to modify your password on the email configuration of your Smartphone or Tablet." -LineBreak
            EmailText -Text "Kind regards,"
            EmailText -Text "IT Service Desk"
        }
    }
    if (-not $TemplatePostExpirySubject) {
        Write-Color -Text "[i]", " TemplatePostExpirySubject not defined. Using default template (built-in)" -Color Yellow, Red
        $TemplatePostExpirySubject = '[Password Expired] Your password expired on $DateExpiry ($DaysToExpire days ago)'
    }

    if (-not $TemplateSecurity) {
        Write-Color -Text "[i]", " TemplateSecurity not defined. Using default template (built-in)" -Color Yellow, Red
        $TemplateSecurity = {
            EmailText -LineBreak
            EmailText -Text "Hello ", "$ManagerDisplayName", "," -LineBreak -FontWeight normal, bold, normal

            EmailText -Text @(
                "Below is a summary of ", "all service accounts",
                " where the passwords have exceeded the time limit stipulated in the password policy. These accounts are all in violation of the policy and immediate action/escalation should take place."
            ) -LineBreak -FontWeight normal, bold, normal

            EmailText -Text "It has been agreed that the ", "password never expires", " flag has been set to ", "true", " to avoid business disruption/loss of service. As a result we require your escalation to the managers of the account to take immediate action to change the password ASAP." -LineBreak -FontWeight normal, bold, normal, bold, normal
            EmailText -Text "Numerous automated reminders have been sent to the Manager, but no response/action has been taken yet." -LineBreak

            EmailText -Text "Please reach out directly to the manager/site to ensure that these passwords are changed immediately." -LineBreak

            EmailText -Text "If there is still lack of responses/action taken, it will be in your (IT Security) discretion to disable the account(s) question and take any appropriate action." -LineBreak -FontWeight bold

            EmailTable -DataTable $ManagerUsersTable -HideFooter

            EmailText -LineBreak
            EmailText -Text "Many thanks in advance." -LineBreak
            EmailText -Text "Kind regards,"
            EmailText -Text "IT Service Desk"
        }
    }
    if (-not $TemplateSecuritySubject) {
        Write-Color -Text "[i]", " TemplateSecuritySubject not defined. Using default template (built-in)" -Color Yellow, Red
        $TemplateSecuritySubject = "[Passsword Expired] Following accounts are expired!"
    }
    if (-not $TemplateManager) {
        Write-Color -Text "[i]", " TemplateManager not defined. Using default template (built-in)" -Color Yellow, Red

        $TemplateManager = {
            EmailText -LineBreak
            EmailText -Text "Hello $ManagerDisplayName," -LineBreak

            EmailText -Text "Below is a summary of accounts where the password is due to expire soon. These accounts are either:"
            EmailList {
                EmailListItem -Text 'Managed by you'
                EmailListItem -Text 'You are the manager of the owner of these accounts.'
            }
            EmailText -Text "Where you are the owner, please action the password change on each account outlined below, according to the rules specified by Password Policy." -LineBreak

            EmailTable -DataTable $ManagerUsersTable -HideFooter

            EmailText -LineBreak
            EmailText -Text @(
                "Please note that for Service Accounts, even though the ",
                "'password never expires' "
                "flag remains set to "
                "'true' "
                ", the password MUST be changed before the expiry date specified in the above table. "
                "It is the responsibility of the manager of the account to ensure that this takes place. "
            ) -FontWeight normal, bold, normal, bold, normal, normal -LineBreak

            EmailText -Text @(
                "Please make an effort "
                "to change password yourself using known methods rather than asking the Service Desk to change the password for you. "
                "If password is changed by Service Desk agent, there are at least 2 people knowing the password - Service Desk Agent and You! "
                "Do you really want the Service Desk agent to know the password to critical system you manage/own? "
                "Be responsible!"
            ) -FontWeight bold, normal, normal, normal, bold -LineBreak -Color None, None, None, None, Red

            EmailText -Text "One of the ways to change the password is: " -FontWeight bold
            EmailList {
                EmailListItem -Text "Press CTRL+ALT+DEL"
                EmailListItem -Text "Choose Change password"
                EmailListItem -Text "In the account name - change it to the account you want to change password for." -FontWeight bold
                EmailListItem -Text "Type in current password for the account and then type the new one according to the rules specified in the password policy."
                EmailListItem -Text "After the change is complete you will be provided with information that the password has been changed"
            }
            EmailText -Text "Failure to take action could result in loss of service/escalation to the IT Security team." -LineBreak -FontWeight bold
            EmailText -Text "Kind regards,"
            EmailText -Text "IT Service Desk"
        }
    }
    if (-not $TemplateManagerSubject) {
        Write-Color -Text "[i]", " TemplateManagerSubject not defined. Using default template (built-in)" -Color Yellow, Red
        $TemplateManagerSubject = "[Passsword Expiring] Accounts you manage/own are expiring or already expired"
    }

    if (-not $TemplateManagerNotCompliant) {
        $TemplateManagerNotCompliant = {
            EmailText -LineBreak
            EmailText -Text "Hello $ManagerDisplayName," -LineBreak

            EmailText -Text "Below is a summary of accounts where there is missing 'critical' information. These accounts are either:"

            EmailList {
                EmailListItem -Text "Missing a Manager in the AD - please add an active manager"
                EmailListItem -Text "The Manager in AD is Disabled - please add an active manager"
                EmailListItem -Text "Manager Last logon >90 days - please confirm if the manager is still an employee/change the manager to an active manager"
                EmailListItem -Text "Manager is missing email - add manager email"
            }
            EmailText -Text "Please contact the respective local IT Service Desk (outlined in the below table) to update this Manager's attributes in the AD directly. The suggested action to take can be found in the below table." -LineBreak

            EmailTable -DataTable $ManagerUsersTableManagerNotCompliant -HideFooter

            EmailText -LineBreak

            EmailText -Text "Kind regards," -LineBreak
            EmailText -Text "IT Service Desk" -LineBreak
        }
    }
    if (-not $TemplateManagerNotCompliantSubject) {
        $TemplateManagerNotCompliantSubject = "[Password Escalation] Accounts are expiring with non-compliant manager"
    }

    if (-not $TemplateAdmin) {

        $TemplateAdmin = {
            EmailText -LineBreak
            EmailText -Text "Hello $ManagerDisplayName," -LineBreak

            EmailText -Text "Here's the summary of password notifications:"

            EmailList {
                EmailListItem -Text "Found users matching rule to send emails: ", $SummaryUsersEmails.Count
                EmailListItem -Text "Sent emails to users: ", ($SummaryUsersEmails | Where-Object { $_.Status -eq $true }).Count
                EmailListItem -Text "Couldn't send emails because of no email: ", ($SummaryUsersEmails | Where-Object { $_.Status -eq $false -and $_.StatusError -eq 'No email address for user' }).Count
                EmailListItem -Text "Couldn't send emails because other reasons: ", ($SummaryUsersEmails | Where-Object { $_.Status -eq $false -and $_.StatusError -ne 'No email address for user' }).Count
                EmailListItem -Text "Sent emails to managers: ", $SummaryManagersEmails.Count
                EmailListItem -Text "Sent emails to security: ", $SummaryEscalationEmails.Count
            }

            EmailText -Text "It took ", $TimeToProcess , " seconds to process the template." -LineBreak

            EmailText -Text "Hope everything works correctly! " -LineBreak

            EmailText -Text "Kind regards," -LineBreak
            EmailText -Text "IT Service Desk" -LineBreak
        }
        if (-not $TemplateAdminSubject) {
            $TemplateAdminSubject = '[Password Summary] Passwords summary'
        }
    }

    $OutputInformation = [ordered] @{
        EmailParameters                    = $EmailParameters
        UserSection                        = $UserSection
        ManagerSection                     = $ManagerSection
        SecuritySection                    = $SecuritySection
        AdminSection                       = $AdminSection
        HTMLReports                        = $HTMLReports
        Rules                              = $Rules
        SearchPath                         = $SearchPath
        OverwriteEmailProperty             = $OverwriteEmailProperty
        OverwriteManagerProperty           = $OverwriteManagerProperty
        Logging                            = $Logging
        TemplatePreExpiry                  = $TemplatePreExpiry
        TemplatePreExpirySubject           = $TemplatePreExpirySubject
        TemplatePostExpiry                 = $TemplatePostExpiry
        TemplatePostExpirySubject          = $TemplatePostExpirySubject
        TemplateManager                    = $TemplateManager
        TemplateManagerSubject             = $TemplateManagerSubject
        TemplateSecurity                   = $TemplateSecurity
        TemplateSecuritySubject            = $TemplateSecuritySubject
        TemplateManagerNotCompliant        = $TemplateManagerNotCompliant
        TemplateManagerNotCompliantSubject = $TemplateManagerNotCompliantSubject
        TemplateAdmin                      = $TemplateAdmin
        TemplateAdminSubject               = $TemplateAdminSubject
        UsersExternalSystem                = $UsersExternalSystem
        FilterOrganizationalUnit           = $FilterOrganizationalUnit
    }
    $OutputInformation
}
function Find-Password {
    <#
    .SYNOPSIS
    Scan Active Directory forest for all users and their password expiration date
 
    .DESCRIPTION
    Scan Active Directory forest for all users and their password expiration date
 
    .PARAMETER Forest
    Target different Forest, by default current forest is used
 
    .PARAMETER ExcludeDomains
    Exclude domain from search, by default whole forest is scanned
 
    .PARAMETER IncludeDomains
    Include only specific domains, by default whole forest is scanned
 
    .PARAMETER ExtendedForestInformation
    Ability to provide Forest Information from another command to speed up processing
 
    .PARAMETER OverwriteEmailProperty
    Overwrite EmailAddress property with different property name
 
    .PARAMETER OverwriteManagerProperty
    Overwrite Manager property with different property name.
    Can use DistinguishedName or SamAccountName
 
    .PARAMETER RulesProperties
    Add additional properties to be returned from rules
 
    .EXAMPLE
    Find-Password | ft
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [System.Collections.IDictionary] $ExtendedForestInformation,
        [string] $OverwriteEmailProperty,
        [Parameter(DontShow)][switch] $AsHashTable,
        [Parameter(DontShow)][string] $HashtableField = 'DistinguishedName',
        [ValidateSet('Users', 'Contacts')][string[]] $ReturnObjectsType = @('Users', 'Contacts'),
        [Parameter(DontShow)][switch] $AsHashTableObject,
        [Parameter(DontShow)][string[]] $AddEmptyProperties = @(),
        [Parameter(DontShow)][string[]] $RulesProperties,
        [string] $OverwriteManagerProperty,
        [Parameter(DontShow)][System.Collections.IDictionary] $UsersExternalSystem,
        [Parameter(DontShow)][System.Collections.IDictionary] $ExternalSystemReplacements = [ordered] @{
            Managers = [System.Collections.Generic.List[PSCustomObject]]::new()
            Users    = [System.Collections.Generic.List[PSCustomObject]]::new()
        },
        [string[]] $FilterOrganizationalUnit,
        [System.Collections.IDictionary] $Cache = [ordered] @{},
        [System.Collections.IDictionary] $CacheManager = [ordered] @{}
    )

    $ExternalSystemManagers = [ordered]@{}
    if ($UsersExternalSystem.Name) {
        Write-Color -Text '[i] ', "Using external system ", $UsersExternalSystem.Name, " for EMAIL replacement functionality" -Color Yellow, White, Yellow, White
        Write-Color -Text '[i] ', "There are ", $UsersExternalSystem.Users.Count, " users in the external system" -Color Yellow, White, Yellow, White
    }
    if (-not $ExternalSystemReplacements.Users) {
        $ExternalSystemReplacements.Users = [System.Collections.Generic.List[PSCustomObject]]::new()
    }
    if (-not $ExternalSystemReplacements.Managers) {
        $ExternalSystemReplacements.Managers = [System.Collections.Generic.List[PSCustomObject]]::new()
    }

    $Today = Get-Date

    $GuidForExchange = Convert-ADSchemaToGuid -SchemaName 'msExchMailboxGuid'
    if ($GuidForExchange) {
        $ExchangeProperty = 'msExchMailboxGuid'
    }

    $Properties = @(
        'Manager', 'DisplayName', 'GivenName', 'Surname', 'SamAccountName', 'EmailAddress',
        'msDS-UserPasswordExpiryTimeComputed', 'PasswordExpired', 'PasswordLastSet', 'PasswordNotRequired',
        'Enabled', 'PasswordNeverExpires', 'Mail', 'MemberOf', 'LastLogonDate', 'Name'
        'userAccountControl'
        'pwdLastSet', 'ObjectClass'
        'LastLogonDate'
        'Country'
        if ($UsersExternalSystem -and $UsersExternalSystem.Type -eq 'ExternalUsers') {
            $UsersExternalSystem.ActiveDirectoryProperty
        }
        if ($ExchangeProperty) {
            $ExchangeProperty
        }
        if ($OverwriteEmailProperty) {
            $OverwriteEmailProperty
        }
        if ($OverwriteManagerProperty) {
            $OverwriteManagerProperty
        }
        foreach ($Rule in $RulesProperties) {
            $Rule
        }
    )
    $Properties = $Properties | Sort-Object -Unique

    [Array] $ExtendedProperties = foreach ($Rule in $RulesProperties) {
        $Rule
    }
    [Array] $ExtendedProperties = $ExtendedProperties | Sort-Object -Unique

    $PropertiesContacts = @(
        'SamAccountName', 'CanonicalName', 'WhenChanged', 'WhenChanged', 'DisplayName', 'DistinguishedName', 'Name', 'Mail', 'TargetAddress', 'ObjectClass'
    )

    if (-not $Cache) {
        $Cache = [ordered] @{ }
    }

    if (-not $CachedUsers) {
        $CachedUsers = [ordered] @{ }
    }
    Write-Color -Text '[i] ', "Discovering forest information" -Color Yellow, White
    $ForestInformation = Get-WinADForestDetails -PreferWritable -Extended -Forest $Forest -ExcludeDomains $ExcludeDomains -IncludeDomains $IncludeDomains -ExtendedForestInformation $ExtendedForestInformation

    $DNSNetBios = @{ }
    foreach ($NETBIOS in $ForestInformation.DomainsExtendedNetBIOS.Keys) {
        $DNSNetBios[$ForestInformation.DomainsExtendedNetBIOS[$NETBIOS].DnsRoot] = $NETBIOS
    }

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

        Write-Color -Text "[i] ", "Getting users from ", "$($Domain)", " using ", $Server -Color Yellow, White, Yellow, White
        try {
            Get-ADUser -Server $Server -Filter '*' -Properties $Properties -ErrorAction Stop
        } catch {
            $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " "
            Write-Color '[e] Error: ', $ErrorMessage -Color White, Red
        }
    }
    Write-Color -Text "[i] ", "Caching users for easy access" -Color Yellow, White
    foreach ($User in $Users) {
        $Cache[$User.DistinguishedName] = $User

        $Cache[$User.SamAccountName] = $User
    }

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

            Write-Color -Text "[i] ", "Getting contacts from ", "$($Domain)", " using ", $Server -Color Yellow, White, Yellow, White
            try {
                Get-ADObject -LDAPFilter "objectClass=Contact" -Server $Server -Properties $PropertiesContacts -ErrorAction Stop
            } catch {
                $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " "
                Write-Color '[e] Error: ', $ErrorMessage -Color White, Red
            }
        }
        foreach ($Contact in $Contacts) {
            $Cache[$Contact.DistinguishedName] = $Contact
        }
    }

    Write-Color -Text "[i] ", "Preparing all users for password expirations in forest ", $Forest.Name -Color Yellow, White, Yellow, White
    $CountUsers = 0
    foreach ($User in $Users) {
        $CountUsers++
        Write-Verbose -Message "Processing $($User.DisplayName) - $($CountUsers)/$($Users.Count)"
        $SkipUser = $false
        $DateExpiry = $null
        $DaysToExpire = $null
        $PasswordDays = $null
        $PasswordNeverExpires = $null
        $PasswordAtNextLogon = $null
        $HasMailbox = $null

        $OUPath = ConvertFrom-DistinguishedName -DistinguishedName $User.DistinguishedName -ToOrganizationalUnit

        foreach ($OU in $FilterOrganizationalUnit) {
            if ($null -eq $OUPath) {
                $SkipUser = $true
                break
            } elseif ($OUPath -notlike "$OU") {
                $SkipUser = $true
                break
            }
        }
        if ($SkipUser) {
            continue
        }

        if ($OverwriteManagerProperty) {

            $ManagerTemp = $User.$OverwriteManagerProperty
            if ($ManagerTemp) {
                $ManagerSpecial = $Cache[$ManagerTemp]
            } else {
                $ManagerSpecial = $null
            }
        } else {
            $ManagerSpecial = $null
        }

        if ($ManagerSpecial) {

            $ManagerDN = $ManagerSpecial.DistinguishedName
            $Manager = $ManagerSpecial.DisplayName
            $ManagerSamAccountName = $ManagerSpecial.SamAccountName
            $ManagerDisplayName = $ManagerSpecial.DisplayName
            $ManagerEmail = $ManagerSpecial.Mail

            if ($ManagerSamAccountName -and $UsersExternalSystem -and $UsersExternalSystem.Global -eq $true) {
                $ADProperty = $UsersExternalSystem.ActiveDirectoryProperty
                if ($ADProperty -eq 'SamAccountName') {

                    $EmailProperty = $UsersExternalSystem.EmailProperty
                    $ExternalUser = $UsersExternalSystem['Users'][$ManagerSamAccountName]
                    if ($ExternalUser -and $ExternalUser.$EmailProperty -like '*@*' -and $ExternalUser.$EmailProperty -ne $ManagerEmail) {
                        $ReplacedManagerEmail = $ManagerEmail
                        $ManagerEmail = $ExternalUser.$EmailProperty

                        if (-not $ExternalSystemManagers[$ManagerSamAccountName]) {
                            $ExternalSystemManagers[$ManagerSamAccountName] = $ManagerSamAccountName
                            $ExternalSystemReplacements.Managers.Add(
                                [PSCustomObject]@{
                                    ManagerSamAccountName = $ManagerSamAccountName
                                    ExternalEmail         = $ManagerEmail
                                    ADEmailAddress        = $ReplacedManagerEmail
                                    ExternalSystem        = $UsersExternalSystem.Name
                                }
                            )
                        }

                    }
                }
            }

            $ManagerEnabled = $ManagerSpecial.Enabled
            $ManagerLastLogon = $ManagerSpecial.LastLogonDate
            if ($ManagerLastLogon) {
                $ManagerLastLogonDays = $( - $($ManagerLastLogon - $Today).Days)
            } else {
                $ManagerLastLogonDays = $null
            }
            $ManagerType = $ManagerSpecial.ObjectClass
        } elseif ($User.Manager) {
            $ManagerDN = $Cache[$User.Manager].DistinguishedName
            $Manager = $Cache[$User.Manager].DisplayName
            $ManagerSamAccountName = $Cache[$User.Manager].SamAccountName
            $ManagerDisplayName = $Cache[$User.Manager].DisplayName
            $ManagerEmail = $Cache[$User.Manager].Mail

            if ($ManagerSamAccountName -and $UsersExternalSystem -and $UsersExternalSystem.Global -eq $true) {
                $ADProperty = $UsersExternalSystem.ActiveDirectoryProperty
                if ($ADProperty -eq 'SamAccountName') {

                    $EmailProperty = $UsersExternalSystem.EmailProperty
                    $ExternalUser = $UsersExternalSystem['Users'][$ManagerSamAccountName]
                    if ($ExternalUser -and $ExternalUser.$EmailProperty -like '*@*' -and $ExternalUser.$EmailProperty -ne $ManagerEmail) {
                        $ReplacedManagerEmail = $ManagerEmail
                        $ManagerEmail = $ExternalUser.$EmailProperty
                        if (-not $ExternalSystemManagers[$ManagerSamAccountName]) {
                            $ExternalSystemManagers[$ManagerSamAccountName] = $ManagerSamAccountName
                            $ExternalSystemReplacements.Managers.Add(
                                [PSCustomObject]@{
                                    ManagerSamAccountName = $ManagerSamAccountName
                                    ExternalEmail         = $ManagerEmail
                                    ADEmailAddress        = $ReplacedManagerEmail
                                    ExternalSystem        = $UsersExternalSystem.Name
                                }
                            )
                        }

                    }
                }
            }
            $ManagerEnabled = $Cache[$User.Manager].Enabled
            $ManagerLastLogon = $Cache[$User.Manager].LastLogonDate
            if ($ManagerLastLogon) {
                $ManagerLastLogonDays = $( - $($ManagerLastLogon - $Today).Days)
            } else {
                $ManagerLastLogonDays = $null
            }
            $ManagerType = $Cache[$User.Manager].ObjectClass
        } else {
            if ($User.ObjectClass -eq 'user') {
                $ManagerStatus = 'Missing'
            } else {
                $ManagerStatus = 'Not available'
            }
            $ManagerDN = $null
            $Manager = $null
            $ManagerSamAccountName = $null
            $ManagerDisplayName = $null
            $ManagerEmail = $null
            $ManagerEnabled = $null
            $ManagerLastLogon = $null
            $ManagerLastLogonDays = $null
            $ManagerType = $null
        }

        if ($ManagerDN -and -not $CacheManager[$ManagerDN]) {
            $CacheManager[$ManagerDN] = [PSCustomObject] @{
                DistinguishedName = $ManagerDN
                Domain            = ConvertFrom-DistinguishedName -DistinguishedName $ManagerDN -ToDomainCN
                DisplayName       = $ManagerDisplayName
                SamAccountName    = $ManagerSamAccountName
                EmailAddress      = $ManagerEmail
                Enabled           = $ManagerEnabled
                LastLogonDate     = $ManagerLastLogon
                LastLogonDays     = $ManagerLastLogonDays
                Type              = $ManagerType
            }
        }

        if ($OverwriteEmailProperty) {

            $EmailTemp = $User.$OverwriteEmailProperty
            if ($EmailTemp -like '*@*') {
                $EmailAddress = $EmailTemp
            } else {
                $EmailAddress = $User.EmailAddress
            }

            if ($Cache["$($User.Manager)"]) {
                if ($Cache["$($User.Manager)"].$OverwriteEmailProperty -like '*@*') {

                    $ManagerEmail = $Cache["$($User.Manager)"].$OverwriteEmailProperty
                }
            }
        } else {
            $EmailAddress = $User.EmailAddress
        }

        if ($UsersExternalSystem -and $UsersExternalSystem.Global -eq $true) {
            if ($UsersExternalSystem.Type -eq 'ExternalUsers') {
                $ADProperty = $UsersExternalSystem.ActiveDirectoryProperty
                $EmailProperty = $UsersExternalSystem.EmailProperty
                $ExternalUser = $UsersExternalSystem['Users'][$User.$ADProperty]

                $EmailFrom = 'AD'
                if ($ExternalUser -and $ExternalUser.$EmailProperty -like '*@*' -and $EmailAddress -ne $ExternalUser.$EmailProperty) {
                    $EmailFrom = 'ILM'
                    $EmailAddress = $ExternalUser.$EmailProperty
                    $ExternalSystemReplacements.Users.Add(
                        [PSCustomObject]@{
                            UserSamAccountName = $User.SamAccountName
                            ExternalEmail      = $EmailAddress
                            ADEmailAddress     = $User.EmailAddress
                            ExternalSystem     = $UsersExternalSystem.Name
                        }
                    )
                }
            } else {
                Write-Color -Text '[-] ', "External system type not supported. Please use only type as provided using 'New-PasswordConfigurationExternalUsers'." -Color Yellow, White, Red
                return
            }
        } else {
            $EmailFrom = 'AD'
        }

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

        if ($User.Manager) {
            if ($ManagerEnabled -and $ManagerEmail) {
                if ((Test-EmailAddress -EmailAddress $ManagerEmail).IsValid -eq $true) {
                    $ManagerStatus = 'Enabled'
                } else {
                    $ManagerStatus = 'Enabled, bad email'
                }
            } elseif ($ManagerEnabled) {
                $ManagerStatus = 'No email'
            } elseif ($Cache[$User.Manager].ObjectClass -eq 'Contact') {
                $ManagerStatus = 'Enabled' 
            } else {
                $ManagerStatus = 'Disabled'
            }
        }

        if ($User."msDS-UserPasswordExpiryTimeComputed" -ne 9223372036854775807) {

            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 {

            $PasswordNeverExpires = $true
        }

        if ($User.pwdLastSet -eq 0 -and $DateExpiry.Year -eq 1601) {
            $PasswordAtNextLogon = $true
        } else {
            $PasswordAtNextLogon = $false
        }

        if ($PasswordNeverExpires -or $null -eq $User.PasswordLastSet) {

            $DateExpiry = $null
            $DaysToExpire = $null
        }

        $UserAccountControl = Convert-UserAccountControl -UserAccountControl $User.UserAccountControl
        if ($UserAccountControl -contains 'INTERDOMAIN_TRUST_ACCOUNT') {
            continue
        }
        if ($ExchangeProperty) {
            if ($User.'msExchMailboxGuid') {
                $HasMailbox = 'Yes'
            } else {
                $HasMailbox = 'No'
            }
        } else {
            $HasMailbox = 'Unknown'
        }
        if ($User.LastLogonDate) {
            $LastLogonDays = $( - $($User.LastLogonDate - $Today).Days)
        } else {
            $LastLogonDays = $null
        }

        if ($User.Country) {
            $Country = Convert-CountryCodeToCountry -CountryCode $User.Country
            $CountryCode = $User.Country
        } else {
            $Country = 'Unknown'
            $CountryCode = 'Unknown'
        }

        if ($AddEmptyProperties.Count -gt 0) {
            $StartUser = [ordered] @{
                UserPrincipalName    = $User.UserPrincipalName
                SamAccountName       = $User.SamAccountName
                Domain               = ConvertFrom-DistinguishedName -DistinguishedName $User.DistinguishedName -ToDomainCN
                RuleName             = ''
                RuleOptions          = [System.Collections.Generic.List[string]]::new()
                Enabled              = $User.Enabled
                HasMailbox           = $HasMailbox
                EmailAddress         = $EmailAddress
                SystemEmailAddress   = $User.EmailAddress
                DateExpiry           = $DateExpiry
                DaysToExpire         = $DaysToExpire
                PasswordExpired      = $User.PasswordExpired
                PasswordDays         = $PasswordDays
                PasswordAtNextLogon  = $PasswordAtNextLogon
                PasswordLastSet      = $User.PasswordLastSet
                PasswordNotRequired  = $User.PasswordNotRequired
                PasswordNeverExpires = $PasswordNeverExpires
                LastLogonDate        = $User.LastLogonDate
                LastLogonDays        = $LastLogonDays
            }
            foreach ($Property in $AddEmptyProperties) {
                $StartUser.$Property = $null
            }
            $EndUser = [ordered] @{
                Manager               = $Manager
                ManagerDisplayName    = $ManagerDisplayName
                ManagerSamAccountName = $ManagerSamAccountName
                ManagerEmail          = $ManagerEmail
                ManagerStatus         = $ManagerStatus
                ManagerLastLogonDays  = $ManagerLastLogonDays
                ManagerType           = $ManagerType
                DisplayName           = $User.DisplayName
                Name                  = $User.Name
                GivenName             = $User.GivenName
                Surname               = $User.Surname
                OrganizationalUnit    = $OUPath
                MemberOf              = $User.MemberOf
                DistinguishedName     = $User.DistinguishedName
                ManagerDN             = $User.Manager
                Country               = $Country
                CountryCode           = $CountryCode
                Type                  = 'User'
                EmailFrom             = $EmailFrom
            }
            $MyUser = $StartUser + $EndUser
        } else {
            $MyUser = [ordered] @{
                UserPrincipalName     = $User.UserPrincipalName
                SamAccountName        = $User.SamAccountName
                Domain                = ConvertFrom-DistinguishedName -DistinguishedName $User.DistinguishedName -ToDomainCN
                RuleName              = ''
                RuleOptions           = [System.Collections.Generic.List[string]]::new()
                Enabled               = $User.Enabled
                HasMailbox            = $HasMailbox
                EmailAddress          = $EmailAddress
                SystemEmailAddress    = $User.EmailAddress
                DateExpiry            = $DateExpiry
                DaysToExpire          = $DaysToExpire
                PasswordExpired       = $User.PasswordExpired
                PasswordDays          = $PasswordDays
                PasswordAtNextLogon   = $PasswordAtNextLogon
                PasswordLastSet       = $User.PasswordLastSet
                PasswordNotRequired   = $User.PasswordNotRequired
                PasswordNeverExpires  = $PasswordNeverExpires
                LastLogonDate         = $User.LastLogonDate
                LastLogonDays         = $LastLogonDays
                Manager               = $Manager
                ManagerDisplayName    = $ManagerDisplayName
                ManagerSamAccountName = $ManagerSamAccountName
                ManagerEmail          = $ManagerEmail
                ManagerStatus         = $ManagerStatus
                ManagerLastLogonDays  = $ManagerLastLogonDays
                ManagerType           = $ManagerType
                DisplayName           = $User.DisplayName
                Name                  = $User.Name
                GivenName             = $User.GivenName
                Surname               = $User.Surname
                OrganizationalUnit    = ConvertFrom-DistinguishedName -DistinguishedName $User.DistinguishedName -ToOrganizationalUnit
                MemberOf              = $User.MemberOf
                DistinguishedName     = $User.DistinguishedName
                ManagerDN             = $User.Manager
                Country               = $Country
                CountryCode           = $CountryCode
                Type                  = 'User'
                EmailFrom             = $EmailFrom
            }
        }
        foreach ($Property in $ConditionProperties) {
            $MyUser["$Property"] = $User.$Property
        }
        foreach ($E in $ExtendedProperties) {
            $MyUser[$E] = $User.$E
        }
        if ($HashtableField -eq 'NetBiosSamAccountName') {
            $HashField = $DNSNetBios[$MyUser.Domain] + '\' + $MyUser.SamAccountName
            if ($AsHashTableObject) {
                $CachedUsers["$HashField"] = $MyUser
            } else {
                $CachedUsers["$HashField"] = [PSCustomObject] $MyUser
            }
        } else {
            if ($AsHashTableObject) {
                $CachedUsers["$($User.$HashtableField)"] = $MyUser
            } else {
                $CachedUsers["$($User.$HashtableField)"] = [PSCustomObject] $MyUser
            }
        }
    }
    if ($ReturnObjectsType -contains 'Contacts') {
        $CountContacts = 0
        foreach ($Contact in $Contacts) {
            $CountContacts++

            $OUPath = ConvertFrom-DistinguishedName -DistinguishedName $Contact.DistinguishedName -ToOrganizationalUnit

            foreach ($OU in $FilterOrganizationalUnit) {
                if ($null -eq $OUPath) {
                    $SkipUser = $true
                    break
                } elseif ($OUPath -notlike "$OU") {
                    $SkipUser = $true
                    break
                }
            }
            if ($SkipUser) {
                continue
            }

            Write-Verbose -Message "Processing $($Contact.DisplayName) - $($CountContacts)/$($Contacts.Count)"

            $MyUser = [ordered] @{
                UserPrincipalName     = $null
                SamAccountName        = $null
                Domain                = ConvertFrom-DistinguishedName -DistinguishedName $Contact.DistinguishedName -ToDomainCN
                RuleName              = ''
                RuleOptions           = [System.Collections.Generic.List[string]]::new()
                Enabled               = $true
                HasMailbox            = $null
                EmailAddress          = $Contact.Mail
                SystemEmailAddress    = $Contact.Mail
                DateExpiry            = $null
                DaysToExpire          = $null
                PasswordExpired       = $null
                PasswordDays          = $null
                PasswordAtNextLogon   = $null
                PasswordLastSet       = $null
                PasswordNotRequired   = $null
                PasswordNeverExpires  = $null
                LastLogonDate         = $null
                LastLogonDays         = $null
                Manager               = $null
                ManagerDisplayName    = $null
                ManagerSamAccountName = $null
                ManagerEmail          = $null
                ManagerStatus         = $null
                ManagerLastLogonDays  = $null
                ManagerType           = $null
                DisplayName           = $Contact.DisplayName
                Name                  = $Contact.Name
                GivenName             = $null
                Surname               = $null
                OrganizationalUnit    = $OUPath
                MemberOf              = $Contact.MemberOf
                DistinguishedName     = $Contact.DistinguishedName
                ManagerDN             = $null
                Country               = $null
                CountryCode           = $null
                Type                  = 'Contact'
                EmailFrom             = $EmailFrom
            }

            foreach ($E in $ExtendedProperties) {
                $MyUser[$E] = $User.$E
            }
            if ($HashtableField -eq 'NetBiosSamAccountName') {

                continue
            } else {
                if ($AsHashTableObject) {
                    $CachedUsers["$($Contact.$HashtableField)"] = $MyUser
                } else {
                    $CachedUsers["$($Contact.$HashtableField)"] = [PSCustomObject] $MyUser
                }
            }
        }
    }
    if ($AsHashTable) {
        $CachedUsers
    } else {
        $CachedUsers.Values
    }
}
function Find-PasswordNotification {
    <#
    .SYNOPSIS
    Searches thru XML logs created by Password Solution
 
    .DESCRIPTION
    Searches thru XML logs created by Password Solution
 
    .PARAMETER SearchPath
    Path to file where the XML log is located
 
    .PARAMETER Manager
    Search thru manager escalations
 
    .EXAMPLE
    Find-PasswordNotification -SearchPath $PSScriptRoot\Search\SearchLog.xml | Format-Table
 
    .EXAMPLE
    Find-PasswordNotification -SearchPath "$PSScriptRoot\Search\SearchLog_2021-06.xml" -Manager | Format-Table
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $SearchPath,
        [switch] $Manager
    )
    if ($SearchPath) {
        if (Test-Path -LiteralPath $SearchPath) {
            try {
                $SummarySearch = Import-Clixml -LiteralPath $SearchPath -ErrorAction Stop

            } catch {
                Write-Color -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 Find-PasswordQuality {
    <#
    .SYNOPSIS
    Scan Active Directory forest for asses password quality of users
 
    .DESCRIPTION
    Scan Active Directory forest for asses password quality of users including weak passwords, duplicate groups and more.
 
    .PARAMETER WeakPasswords
    List of weak passwords to check against
 
    .PARAMETER WeakPasswordsFilePath
    Path to a file that contains weak passwords, one password per line.
 
    .PARAMETER WeakPasswordsHashesFile
    Path to a file that contains NT hashes of weak passwords, one hash in HEX format per line. For performance reasons, the -WeakPasswordHashesSortedFile parameter should be used instead.
 
    .PARAMETER WeakPasswordsHashesSortedFile
    Path to a file that contains NT hashes of weak passwords, one hash in HEX format per line. The hashes must be sorted alphabetically, because a binary search is performed. This parameter is typically used with a list of leaked password hashes from HaveIBeenPwned.
 
    .PARAMETER IncludeStatistics
    Include statistics in output
 
    .PARAMETER Forest
    Target different Forest, by default current forest is used
 
    .PARAMETER ExcludeDomains
    Exclude domain from search, by default whole forest is scanned
 
    .PARAMETER IncludeDomains
    Include only specific domains, by default whole forest is scanned
 
    .PARAMETER ExtendedForestInformation
    Ability to provide Forest Information from another command to speed up processing
 
    .EXAMPLE
    Find-PasswordQuality -WeakPasswords "Test1", "Test2", "Test3"
 
    .EXAMPLE
    Find-PasswordQuality -WeakPasswords "Test1", "Test2", "Test3" -IncludeStatistics
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [string[]] $WeakPasswords,
        [string] $WeakPasswordsFilePath,
        [string] $WeakPasswordsHashesFile,
        [string] $WeakPasswordsHashesSortedFile,
        [switch] $IncludeStatistics,

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

    $PropertiesToAdd = @(
        'ClearTextPassword'
        'LMHash'
        'EmptyPassword'
        'WeakPassword'

        'AESKeysMissing'
        'PreAuthNotRequired'
        'DESEncryptionOnly'
        'Kerberoastable'
        'DelegatableAdmins'
        'SmartCardUsersWithPassword'
        'DuplicatePasswordGroups'
    )
    if ($WeakPasswordsHashesFile) {
        if (Test-Path -LiteralPath $WeakPasswordsHashesFile) {
            Write-Color -Text "[i] ", "Weak password hashes available to read from ", $WeakPasswordsHashesFile -Color Yellow, Gray, White, Yellow, White, Yellow, White
            $WeakPasswordHashesStats = Get-FileInformation -File $WeakPasswordsHashesFile
        } else {
            Write-Color -Text "[e] ", "Weak password hashes file not found at ", $WeakPasswordsHashesFile -Color Red, Yellow, White, Yellow, Red
            return
        }
    }
    if ($WeakPasswordsHashesSortedFile) {
        if (Test-Path -LiteralPath $WeakPasswordsHashesSortedFile) {
            Write-Color -Text "[i] ", "Weak passwords hashes (sorted) available to read from ", $WeakPasswordsHashesSortedFile -Color Yellow, Gray, White, Yellow, White, Yellow, White
            $WeakPasswordHashesSortedStats = Get-FileInformation -File $WeakPasswordsHashesSortedFile
        } else {
            Write-Color -Text "[e] ", "Weak passwords hashes (sorted) file not found at ", $WeakPasswordsHashesSortedFile -Color Red, Yellow, White, Yellow, Red
            return
        }
    }
    if ($WeakPasswordsFilePath) {
        if (Test-Path -LiteralPath $WeakPasswordsFilePath) {
            Write-Color -Text "[i] ", "Weak passwords available to read from ", $WeakPasswordsFilePath -Color Yellow, Gray, White, Yellow, White, Yellow, White
            $WeakPasswordsStats = Get-FileInformation -File $WeakPasswordsFilePath
        } else {
            Write-Color -Text "[e] ", "Weak passwords file not found at ", $WeakPasswordsFilePath -Color Red, Yellow, White, Yellow, Red
            return
        }
    }
    $ModuleExists = Get-Command -Module DSInternals -ErrorAction SilentlyContinue
    if (-not $ModuleExists) {
        Write-Color -Text "[e] ", "DSInternals module is not installed. Please install it using Install-Module DSInternals -Verbose" -Color Yellow, Red
        return
    }
    $AllUsers = Find-Password -AsHashTable -HashtableField NetBiosSamAccountName -ReturnObjectsType Users -AsHashTableObject -AddEmptyProperties $PropertiesToAdd -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains

    Write-Color -Text "[i] ", "Discovering forest information" -Color Yellow, Gray, White, Yellow, White, Yellow, White
    $ForestInformation = Get-WinADForestDetails -PreferWritable -Forest $Forest -ExcludeDomains $ExcludeDomains -IncludeDomains $IncludeDomains -ExtendedForestInformation $ExtendedForestInformation

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

        Write-Color -Text "[i] ", "Getting replication data from ", "$($Domain)", " using ", $Server -Color Yellow, Gray, White, Yellow, White, Yellow, White

        $testPasswordQualitySplat = @{
            WeakPasswords                = $WeakPasswords
            WeakPasswordsFile            = $WeakPasswordsFilePath
            WeakPasswordHashesFile       = $WeakPasswordsHashesFile
            WeakPasswordHashesSortedFile = $WeakPasswordsHashesSortedFile
            IncludeDisabledAccounts      = $true
        }
        Remove-EmptyValue -Hashtable $testPasswordQualitySplat

        try {
            Get-ADReplAccount -All -Server $Server -ErrorAction Stop
        } catch {
            Write-Color -Text "[e] ", "Unable to get replication data from ", "$($Domain)", " using ", $Server, ". Error: ", $_.Exception.Message -Color Red, Yellow, White, Yellow, Red, Red
        }
    }
    Write-Color -Text "[i] Testing password quality" -Color Yellow, Gray, White, Yellow, White, Yellow, White
    $Quality = $PasswordQuality | Test-PasswordQuality @testPasswordQualitySplat

    Write-Color -Text "[i] Processing results, merging data from DSInternals" -Color Yellow, Gray, White, Yellow, White, Yellow, White
    foreach ($Property in $Quality.PSObject.Properties.Name) {
        $PasswordsInHash[$Property] = $Quality.$Property
    }

    $PasswordGroupsUsers = [ordered] @{}
    $Count = 0
    foreach ($Group in $PasswordsInHash.DuplicatePasswordGroups) {
        $Count++
        foreach ($User in $Group) {
            $PasswordGroupsUsers[$User] = "Group $Count"
        }
    }

    $QualityStatistics = [ordered] @{
        AESKeysMissing                         = $PasswordsInHash.AESKeysMissing.Count
        AESKeysMissingEnabledOnly              = 0
        AESKeysMissingDisabledOnly             = 0
        DESEncryptionOnly                      = $PasswordsInHash.DESEncryptionOnly.Count
        DESEncryptionOnlyEnabledOnly           = 0
        DESEncryptionOnlyDisabledOnly          = 0
        DelegatableAdmins                      = $PasswordsInHash.DelegatableAdmins.Count
        DelegatableAdminsEnabledOnly           = 0
        DelegatableAdminsDisabledOnly          = 0
        DuplicatePasswordGroups                = $PasswordsInHash.DuplicatePasswordGroups.Count
        DuplicatePasswordUsers                 = $PasswordGroupsUsers.Keys.Count
        DuplicatePasswordUsersEnabledOnly      = 0
        DuplicatePasswordUsersDisabledOnly     = 0
        ClearTextPassword                      = $PasswordsInHash.ClearTextPassword.Count
        ClearTextPasswordEnabledOnly           = 0
        ClearTextPasswordDisabledOnly          = 0
        LMHash                                 = $PasswordsInHash.LMHash.Count
        LMHashEnabledOnly                      = 0
        LMHashDisabledOnly                     = 0
        EmptyPassword                          = $PasswordsInHash.EmptyPassword.Count
        EmptyPasswordEnabledOnly               = 0
        EmptyPasswordDisabledOnly              = 0
        WeakPassword                           = $PasswordsInHash.WeakPassword.Count
        WeakPasswordEnabledOnly                = 0
        WeakPasswordDisabledOnly               = 0

        PasswordNotRequired                    = 0 
        PasswordNotRequiredEnabledOnly         = 0
        PasswordNotRequiredDisabledOnly        = 0
        PasswordNeverExpires                   = 0 
        PasswordNeverExpiresEnabledOnly        = 0
        PasswordNeverExpiresDisabledOnly       = 0
        PreAuthNotRequired                     = $PasswordsInHash.PreAuthNotRequired.Count
        PreAuthNotRequiredEnabledOnly          = 0
        PreAuthNotRequiredDisabledOnly         = 0
        Kerberoastable                         = $PasswordsInHash.Kerberoastable.Count
        KerberoastableEnabledOnly              = 0
        KerberoastableDisabledOnly             = 0
        SmartCardUsersWithPassword             = $PasswordsInHash.SmartCardUsersWithPassword.Count
        SmartCardUsersWithPasswordEnabledOnly  = 0
        SmartCardUsersWithPasswordDisabledOnly = 0
    }
    $CountryStatistics = [ordered] @{
        DuplicatePasswordUsers = [ordered] @{}
        WeakPassword           = [ordered] @{}
    }
    $ContinentStatistics = [ordered] @{
        DuplicatePasswordUsers = [ordered] @{}
        WeakPassword           = [ordered] @{}
    }
    $CountryCodeStatistics = [ordered] @{
        DuplicatePasswordUsers = [ordered] @{}
        WeakPassword           = [ordered] @{}
    }
    $CountryToContinent = Convert-CountryToContinent

    $OutputUsers = foreach ($User in $AllUsers.Keys) {
        if ($AllUsers[$User].Country) {
            $Continent = $CountryToContinent[$AllUsers[$User].Country]
            if (-not $Continent) {
                $Continent = 'Unknown'
            }
        } else {
            $Continent = 'Unknown'
        }
        if ($AllUsers[$User].PasswordNotRequired) {
            $QualityStatistics.PasswordNotRequired++
            if ($AllUsers[$User].Enabled -eq $true) {
                $QualityStatistics.PasswordNotRequiredEnabledOnly++
            } else {
                $QualityStatistics.PasswordNotRequiredDisabledOnly++
            }
        }
        if ($AllUsers[$User].PasswordNeverExpires) {
            $QualityStatistics.PasswordNeverExpires++
            if ($AllUsers[$User].Enabled -eq $true) {
                $QualityStatistics.PasswordNeverExpiresEnabledOnly++
            } else {
                $QualityStatistics.PasswordNeverExpiresDisabledOnly++
            }
        }
        foreach ($Property in $PasswordsInHash.Keys) {
            if ($Property -eq 'DuplicatePasswordGroups') {
                if ($PasswordGroupsUsers[$User]) {
                    $AllUsers[$User][$Property] = $PasswordGroupsUsers[$User]
                    if ($AllUsers[$User].Enabled -eq $true) {
                        $QualityStatistics["$($Property)EnabledOnly"]++
                        $QualityStatistics.DuplicatePasswordUsersEnabledOnly++
                    } else {
                        $QualityStatistics["$($Property)DisabledOnly"]++
                        $QualityStatistics.DuplicatePasswordUsersDisabledOnly++
                    }

                    $CountryStatistics['DuplicatePasswordUsers'][$AllUsers[$User].Country]++
                    $ContinentStatistics['DuplicatePasswordUsers'][$Continent]++
                    $CountryCodeStatistics['DuplicatePasswordUsers'][$AllUsers[$User].CountryCode]++

                } else {
                    $AllUsers[$User][$Property] = ''
                }
            } elseif ($Property -in $PropertiesToAdd) {
                if ($PasswordsInHash[$Property] -contains $User) {
                    $AllUsers[$User][$Property] = $true
                    if ($AllUsers[$User].Enabled -eq $true) {
                        $QualityStatistics["$($Property)EnabledOnly"]++
                    } else {
                        $QualityStatistics["$($Property)DisabledOnly"]++
                    }

                    if ($Property -eq 'WeakPassword') {
                        $CountryStatistics[$Property][$AllUsers[$User].Country]++
                        $ContinentStatistics[$Property][$Continent]++
                        $CountryCodeStatistics[$Property][$AllUsers[$User].CountryCode]++
                    }
                } else {
                    $AllUsers[$User][$Property] = $false
                }
            }
        }
        [PSCustomObject] $AllUsers[$User]
    }
    if ($IncludeStatistics) {
        [ordered] @{
            Forest                       = $ForestInformation.Forest
            Domains                      = $ForestInformation.Domains
            Statistics                   = $QualityStatistics
            StatisticsCountry            = $CountryStatistics
            StatisticsCountryCode        = $CountryCodeStatistics
            StatisticsContinents         = $ContinentStatistics
            Users                        = $OutputUsers
            WeakPasswordsFileInformation = [ordered] @{
                WeakPasswordHashesStats       = $WeakPasswordHashesStats
                WeakPasswordHashesSortedStats = $WeakPasswordHashesSortedStats
                WeakPasswordsStats            = $WeakPasswordsStats
            }
        }
    } else {
        $OutputUsers
    }
}
function New-PasswordConfigurationEmail {
    [cmdletBinding(DefaultParameterSetName = 'Compatibility', SupportsShouldProcess)]
    param(
        [Parameter(ParameterSetName = 'SecureString')]

        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [alias('SmtpServer')][string] $Server,

        [Parameter(ParameterSetName = 'SecureString')]

        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [int] $Port,

        [Parameter(Mandatory, ParameterSetName = 'SecureString')]
        [Parameter(Mandatory, ParameterSetName = 'oAuth')]
        [Parameter(Mandatory, ParameterSetName = 'Graph')]
        [Parameter(Mandatory, ParameterSetName = 'MgGraphRequest')]
        [Parameter(Mandatory, ParameterSetName = 'Compatibility')]
        [Parameter(Mandatory, ParameterSetName = 'SendGrid')]
        [object] $From,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [Parameter(ParameterSetName = 'SendGrid')]
        [string] $ReplyTo,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [Parameter(ParameterSetName = 'SendGrid')]
        [alias('Importance')][ValidateSet('Low', 'Normal', 'High')][string] $Priority,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [ValidateSet('None', 'OnSuccess', 'OnFailure', 'Delay', 'Never')][string[]] $DeliveryNotificationOption,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [MailKit.Net.Smtp.DeliveryStatusNotificationType] $DeliveryStatusNotificationType,

        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(Mandatory, ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [Parameter(Mandatory, ParameterSetName = 'SendGrid')]
        [pscredential] $Credential,

        [Parameter(ParameterSetName = 'SecureString')]
        [string] $Username,

        [Parameter(ParameterSetName = 'SecureString')]
        [string] $Password,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [MailKit.Security.SecureSocketOptions] $SecureSocketOptions,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [switch] $UseSsl,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [switch] $SkipCertificateRevocation,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [alias('SkipCertificateValidatation')][switch] $SkipCertificateValidation,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [int] $Timeout,

        [Parameter(ParameterSetName = 'oAuth')]
        [alias('oAuth')][switch] $oAuth2,

        [Parameter(ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [switch] $RequestReadReceipt,

        [Parameter(ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [switch] $RequestDeliveryReceipt,

        [Parameter(ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [switch] $Graph,

        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [switch] $MgGraphRequest,

        [Parameter(ParameterSetName = 'SecureString')]
        [switch] $AsSecureString,

        [Parameter(ParameterSetName = 'SendGrid')]
        [switch] $SendGrid,

        [Parameter(ParameterSetName = 'SendGrid')]
        [switch] $SeparateTo,

        [Parameter(ParameterSetName = 'Graph')]
        [Parameter(ParameterSetName = 'MgGraphRequest')]
        [switch] $DoNotSaveToSentItems,

        [Parameter(ParameterSetName = 'SecureString')]
        [Parameter(ParameterSetName = 'oAuth')]
        [Parameter(ParameterSetName = 'Compatibility')]
        [string] $LocalDomain
    )

    $Output = [ordered] @{
        Type     = 'PasswordConfigurationEmail'
        Settings = [ordered] @{
            Server                         = if ($PSBoundParameters.ContainsKey('Server')) {
                $Server } else {
                $null }
            Port                           = if ($PSBoundParameters.ContainsKey('Port')) {
                $Port } else {
                $null }
            From                           = if ($PSBoundParameters.ContainsKey('From')) {
                $From } else {
                $null }
            ReplyTo                        = if ($PSBoundParameters.ContainsKey('ReplyTo')) {
                $ReplyTo } else {
                $null }
            Priority                       = if ($PSBoundParameters.ContainsKey('Priority')) {
                $Priority } else {
                $null }
            DeliveryNotificationOption     = if ($PSBoundParameters.ContainsKey('DeliveryNotificationOption')) {
                $DeliveryNotificationOption } else {
                $null }
            DeliveryStatusNotificationType = if ($PSBoundParameters.ContainsKey('DeliveryStatusNotificationType')) {
                $DeliveryStatusNotificationType } else {
                $null }
            Credential                     = if ($PSBoundParameters.ContainsKey('Credential')) {
                $Credential } else {
                $null }
            Username                       = if ($PSBoundParameters.ContainsKey('Username')) {
                $Username } else {
                $null }
            Password                       = if ($PSBoundParameters.ContainsKey('Password')) {
                $Password } else {
                $null }
            SecureSocketOptions            = if ($PSBoundParameters.ContainsKey('SecureSocketOptions')) {
                $SecureSocketOptions } else {
                $null }
            UseSsl                         = if ($PSBoundParameters.ContainsKey('UseSsl')) {
                $UseSsl } else {
                $null }
            SkipCertificateRevocation      = if ($PSBoundParameters.ContainsKey('SkipCertificateRevocation')) {
                $SkipCertificateRevocation } else {
                $null }
            SkipCertificateValidation      = if ($PSBoundParameters.ContainsKey('SkipCertificateValidatation')) {
                $SkipCertificateValidation } else {
                $null }
            Timeout                        = if ($PSBoundParameters.ContainsKey('Timeout')) {
                $Timeout } else {
                $null }
            oAuth2                         = if ($PSBoundParameters.ContainsKey('oAuth2')) {
                $oAuth2 } else {
                $null }
            RequestReadReceipt             = if ($PSBoundParameters.ContainsKey('RequestReadReceipt')) {
                $RequestReadReceipt } else {
                $null }
            RequestDeliveryReceipt         = if ($PSBoundParameters.ContainsKey('RequestDeliveryReceipt')) {
                $RequestDeliveryReceipt } else {
                $null }
            Graph                          = if ($PSBoundParameters.ContainsKey('Graph')) {
                $Graph } else {
                $null }
            MgGraphRequest                 = if ($PSBoundParameters.ContainsKey('MgGraphRequest')) {
                $MgGraphRequest } else {
                $null }
            AsSecureString                 = if ($PSBoundParameters.ContainsKey('AsSecureString')) {
                $AsSecureString } else {
                $null }
            SendGrid                       = if ($PSBoundParameters.ContainsKey('SendGrid')) {
                $SendGrid } else {
                $null }
            SeparateTo                     = if ($PSBoundParameters.ContainsKey('SeparateTo')) {
                $SeparateTo } else {
                $null }
            DoNotSaveToSentItems           = if ($PSBoundParameters.ContainsKey('DoNotSaveToSentItems')) {
                $DoNotSaveToSentItems } else {
                $null }
            WhatIf                         = $WhatIfPreference
        }
    }
    Remove-EmptyValue -Hashtable $Output.Settings
    $Output
}
function New-PasswordConfigurationExternalUsers {
    <#
    .SYNOPSIS
    This function caches users from external systems to be used in the password configuration.
 
    .DESCRIPTION
    This function caches users from external systems to be used in the password configuration.
    It provides ability to find user by some property and get another property of the user.
 
    .PARAMETER Users
    Parameter description
 
    .PARAMETER ActiveDirectoryProperty
    Property in Active Directory to search for when comparing against SearchProperty.
 
    .PARAMETER SearchProperty
    Property to cache on the user object.
 
    .PARAMETER EmailProperty
    How the email property is called in the user object.
 
    .PARAMETER Global
    Tells the solution to globally overwrite email addresses for all users.
 
    .PARAMETER Name
    Name of the configuration. Visible in HTML reports.
 
    .EXAMPLE
    New-PasswordConfigurationExternalUsers -Users $ExportDataFromHrSystem -SearchProperty '<property in the HR system>' -EmailProperty '<email property in HR system>' -ActiveDirectoryProperty 'SamAccountName'
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [parameter(Mandatory)][string] $Name,
        [parameter(Mandatory)][Array] $Users,
        [parameter(Mandatory)][string] $ActiveDirectoryProperty,
        [parameter(Mandatory)][string] $SearchProperty,
        [parameter(Mandatory)][string] $EmailProperty,
        [switch] $Global
    )

    $CachedUsers = [ordered] @{}

    if ($Users.Count -gt 0 -and $Users[0].$SearchProperty -and $Users[0].$EmailProperty) {
        Write-Color -Text '[+] ', "Caching users for '$Name'" -Color Green, White
    } else {
        Write-Color -Text '[-] ', "Couldn't cache users as either users not provided or email/search property are invalid. Please fix 'New-PasswordConfigurationExternalUsers'" -Color Yellow, White
        return
    }
    try {
        foreach ($User in $Users) {
            if ($User.$SearchProperty) {
                $CachedUsers[$User.$SearchProperty] = $User | Select-Object -Property $EmailProperty
            }
        }
    } catch {
        Write-Color -Text '[-] ', "Couldn't cache users. Please fix 'New-PasswordConfigurationExternalUsers'. Error: ", "$($_.Exception.Message)" -Color Yellow, White, Red
        return
    }
    [ordered] @{
        Type                    = 'ExternalUsers'
        ActiveDirectoryProperty = $ActiveDirectoryProperty
        SearchProperty          = $SearchProperty
        EmailProperty           = $EmailProperty
        Users                   = $CachedUsers
        Global                  = $Global.IsPresent
        Name                    = $Name
    }
}
function New-PasswordConfigurationOption {
    <#
    .SYNOPSIS
    Provides a way to create a PasswordConfigurationOption object.
 
    .DESCRIPTION
    This function provides a way to create a PasswordConfigurationOption object.
    The object is used to store configuration options for the Password Solution module.
 
    .PARAMETER ShowTime
    Show time in the console output. If not provided, time will not be shown.
    Time in the log file is always shown.
 
    .PARAMETER LogFile
    File path to the log file. If not provided, there will be no logging to file
 
    .PARAMETER TimeFormat
    Time format used in the logging functionality.
 
    .PARAMETER LogMaximum
    Maximum number of log files to keep. Default is 0 (unlimited).
    Once the number of log files exceeds the limit, the oldest log files will be deleted.
 
    .PARAMETER NotifyOnSkipUserManagerOnly
    Provides a way to control output to screen for SkipUserManagerOnly.
 
    .PARAMETER NotifyOnSecuritySend
    Provides a way to control output to screen for SecuritySend.
 
    .PARAMETER NotifyOnManagerSend
    Provides a way to control output to screen for ManagerSend.
 
    .PARAMETER NotifyOnUserSend
    Provides a way to control output to screen for UserSend.
 
    .PARAMETER NotifyOnUserMatchingRule
    Provides a way to control output to screen for UserMatchingRule.
 
    .PARAMETER NotifyOnUserDaysToExpireNull
    Provides a way to control output to screen for UserDaysToExpireNull.
 
    .PARAMETER SearchPath
    Path to XML file that will be used for storing search results.
 
    .PARAMETER EmailDateFormat
    Parameter description
 
    .PARAMETER EmailDateFormatUTCConversion
    Parameter description
 
    .PARAMETER OverwriteEmailProperty
    Parameter description
 
    .PARAMETER OverwriteManagerProperty
    Parameter description
 
    .PARAMETER FilterOrganizationalUnit
    Provides a way to filter users by Organizational Unit limiting the scope of the search.
    The search is performed using 'like' operator, so you can use wildcards if needed.
 
    .EXAMPLE
    $Options = @{
        # Logging to file and to screen
        ShowTime = $true
        LogFile = "$PSScriptRoot\Logs\PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).log"
        TimeFormat = "yyyy-MM-dd HH:mm:ss"
        LogMaximum = 365
        NotifyOnSkipUserManagerOnly = $false
        NotifyOnSecuritySend = $true
        NotifyOnManagerSend = $true
        NotifyOnUserSend = $true
        NotifyOnUserMatchingRule = $false
        NotifyOnUserDaysToExpireNull = $false
        SearchPath = "$PSScriptRoot\Search\SearchLog_$((Get-Date).ToString('yyyy-MM')).xml"
        EmailDateFormat = "yyyy-MM-dd"
        EmailDateFormatUTCConversion = $true
        FilterOrganizationalUnit = @(
            "*OU=Accounts,OU=Administration,DC=ad,DC=evotec,DC=xyz"
            "*OU=Administration,DC=ad,DC=evotec,DC=xyz"
        )
    }
    New-PasswordConfigurationOption @Options
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [switch] $ShowTime                     , #= $true
        [string] $LogFile                      , #= "$PSScriptRoot\Logs\PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).log"
        [string] $TimeFormat                   , #= "yyyy-MM-dd HH:mm:ss"
        [int] $LogMaximum                   , #= 365
        [switch] $NotifyOnSkipUserManagerOnly  , #= $false
        [switch] $NotifyOnSecuritySend         , #= $true
        [switch] $NotifyOnManagerSend          , #= $true
        [switch] $NotifyOnUserSend             , #= $true
        [switch] $NotifyOnUserMatchingRule     , #= $true
        [switch] $NotifyOnUserDaysToExpireNull , #= $true
        [string] $SearchPath,
        [string] $EmailDateFormat,
        [switch] $EmailDateFormatUTCConversion,
        [string] $OverwriteEmailProperty,
        [string] $OverwriteManagerProperty,
        [string[]] $FilterOrganizationalUnit
    )

    $Output = [ordered] @{
        Type     = "PasswordConfigurationOption"
        Settings = [ordered] @{
            ShowTime                     = $ShowTime.IsPresent
            LogFile                      = $LogFile
            TimeFormat                   = $TimeFormat
            LogMaximum                   = $LogMaximum
            NotifyOnSkipUserManagerOnly  = $NotifyOnSkipUserManagerOnly.IsPresent
            NotifyOnSecuritySend         = $NotifyOnSecuritySend.IsPresent
            NotifyOnManagerSend          = $NotifyOnManagerSend.IsPresent
            NotifyOnUserSend             = $NotifyOnUserSend.IsPresent
            NotifyOnUserMatchingRule     = $NotifyOnUserMatchingRule.IsPresent
            NotifyOnUserDaysToExpireNull = $NotifyOnUserDaysToExpireNull.IsPresent
            SearchPath                   = $SearchPath

            EmailDateFormat              = $EmailDateFormat
            EmailDateFormatUTCConversion = $EmailDateFormatUTCConversion.IsPresent

            OverwriteEmailProperty       = $OverwriteEmailProperty

            OverwriteManagerProperty     = $OverwriteManagerProperty

            FilterOrganizationalUnit     = $FilterOrganizationalUnit
        }
    }
    Remove-EmptyValue -Hashtable $Output.Settings
    $Output
}
function New-PasswordConfigurationReport {
    <#
    .SYNOPSIS
    Provides HTML report configuration for Password Notifications in Password Solution.
 
    .DESCRIPTION
    Provides HTML report configuration for Password Notifications in Password Solution.
    The New-PasswordConfigurationReport function generates configuration for HTML report.
 
    .PARAMETER Enable
    Specifies whether to enable the report generation. The default value is $false.
 
    .PARAMETER ShowHTML
    Specifies whether to display the report in HTML format right after it's generated in default browser. The default value is $false.
 
    .PARAMETER Title
    Specifies the title of the report. The default value is "Password Solution Summary".
 
    .PARAMETER Online
    Specifies whether to generate the report using CDN for CSS and JS scripts, or use it locally.
    It doesn't require internet connectivity during generation.
    Makes the final output 3MB smaller. The default value is $false.
 
    .PARAMETER DisableWarnings
    Specifies whether to disable warning messages during report generation. The default value is $false.
 
    .PARAMETER ShowConfiguration
    Specifies whether to display the current Password Solution configuration settings. The default value is $false.
 
    .PARAMETER ShowAllUsers
    Specifies whether to display information about all user accounts. The default value is $false.
 
    .PARAMETER ShowRules
    Specifies whether to display information from the rules. The default value is $false.
 
    .PARAMETER ShowUsersSent
    Specifies whether to display information about users who have received (or not) password expiry notifications. The default value is $false.
 
    .PARAMETER ShowManagersSent
    Specifies whether to display information about managers who have received password expiry notifications. The default value is $false.
 
    .PARAMETER ShowEscalationSent
    Specifies whether to display information about escalation contacts who have received password expiry notifications. The default value is $false.
 
    .PARAMETER ShowSkippedUsers
    Specifies whether to display information about users who were during password expiry notifications because of inability to asses their expiration date. The default value is $false.
 
    .PARAMETER ShowSkippedLocations
    Specifies whether to display information about locations where skipped users are located. The default value is $false.
 
    .PARAMETER ShowSearchUsers
    Specifies whether to display information for searching who got password expiry notifications. The default value is $false.
 
    .PARAMETER ShowSearchManagers
    Specifies whether to display information for searching who got password expiry notifications and for which accounts from managers. The default value is $false.
 
    .PARAMETER ShowSearchEscalations
    Specifies whether to display information for searching who got password escalation notifications and what's the status of that message. The default value is $false.
 
    .PARAMETER ShowExternalSystemReplacementsUsers
    Specifies whether to display information about users who's email address was replaced by an external system. The default value is $false.
 
    .PARAMETER ShowExternalSystemReplacementsManagers
    Specifies whether to display information about managers who's email address was replaced by an external system. The default value is $false.
 
    .PARAMETER FilePath
    Specifies the file path for the report
 
    .PARAMETER AttachToEmail
    Specifies whether to attach the report to an administrative email. The default value is $false.
 
    .PARAMETER NestedRules
    Specifies whether to display nested password rules.
    Each rule has it's own tab with output.
    Having many rules and all other settings enabled can result in a very long list of tabs that's hard to navigate.
    This setting forces separate tab for all rules.
    The default value is $false.
 
    .OUTPUTS
    The function returns an ordered dictionary that contains the report settings.
 
    .EXAMPLE
    New-PasswordConfigurationReport -ShowHTML -Title "Password Configuration Report" -FilePath "C:\Reports\PasswordReport.html"
 
    .EXAMPLE
    $Date = Get-Date
    $Report = [ordered] @{
        Enable = $true
        ShowHTML = $true
        Title = "Password Solution Summary"
        Online = $true
        DisableWarnings = $true
        ShowConfiguration = $true
        ShowAllUsers = $true
        ShowRules = $true
        ShowUsersSent = $true
        ShowManagersSent = $true
        ShowEscalationSent = $true
        ShowSkippedUsers = $true
        ShowSkippedLocations = $true
        ShowSearchUsers = $true
        ShowSearchManagers = $true
        ShowSearchEscalations = $true
        NestedRules = $false
        FilePath = "$PSScriptRoot\Reporting\PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).html"
        AttachToEmail = $true
    }
    New-PasswordConfigurationReport @Report
 
    #>

    [CmdletBinding()]
    param(
        [switch] $Enable,
        [switch] $ShowHTML,
        [string] $Title,
        [switch] $Online,
        [switch] $DisableWarnings,
        [switch] $ShowConfiguration,
        [switch] $ShowAllUsers,
        [switch] $ShowRules,
        [switch] $ShowUsersSent,
        [switch] $ShowManagersSent,
        [switch] $ShowEscalationSent,
        [switch] $ShowSkippedUsers,
        [switch] $ShowSkippedLocations,
        [switch] $ShowSearchUsers,
        [switch] $ShowSearchManagers,
        [switch] $ShowSearchEscalations ,
        [string] $FilePath,
        [switch] $AttachToEmail,
        [switch] $NestedRules,
        [switch] $ShowExternalSystemReplacementsUsers,
        [switch] $ShowExternalSystemReplacementsManagers
    )

    $Output = [ordered] @{
        Type     = "PasswordConfigurationReport"
        Settings = [ordered] @{
            Enable                                 = $Enable.IsPresent
            ShowHTML                               = $ShowHTML.IsPresent
            Title                                  = $Title
            Online                                 = $Online.IsPresent
            DisableWarnings                        = $DisableWarnings.IsPresent
            ShowConfiguration                      = $ShowConfiguration.IsPresent
            ShowAllUsers                           = $ShowAllUsers.IsPresent
            ShowRules                              = $ShowRules.IsPresent
            ShowUsersSent                          = $ShowUsersSent.IsPresent
            ShowManagersSent                       = $ShowManagersSent.IsPresent
            ShowEscalationSent                     = $ShowEscalationSent.IsPresent
            ShowSkippedUsers                       = $ShowSkippedUsers.IsPresent
            ShowSkippedLocations                   = $ShowSkippedLocations.IsPresent
            ShowSearchUsers                        = $ShowSearchUsers.IsPresent
            ShowSearchManagers                     = $ShowSearchManagers.IsPresent
            ShowSearchEscalations                  = $ShowSearchEscalations.IsPresent
            FilePath                               = $FilePath
            AttachToEmail                          = $AttachToEmail.IsPresent
            NestedRules                            = $NestedRules.IsPresent
            ShowExternalSystemReplacementsUsers    = $ShowExternalSystemReplacementsUsers.IsPresent
            ShowExternalSystemReplacementsManagers = $ShowExternalSystemReplacementsManagers.IsPresent
        }
    }
    $Output
}
function New-PasswordConfigurationRule {
    <#
    .SYNOPSIS
    Short description
 
    .DESCRIPTION
    Long description
 
    .PARAMETER ReminderConfiguration
    Parameter description
 
    .PARAMETER Name
    Parameter description
 
    .PARAMETER Enable
    Parameter description
 
    .PARAMETER IncludeExpiring
    Parameter description
 
    .PARAMETER IncludePasswordNeverExpires
    Parameter description
 
    .PARAMETER PasswordNeverExpiresDays
    Parameter description
 
    .PARAMETER IncludeName
    Include user in rule if any of the properties match the value of Name in the properties defined in IncludeNameProperties
 
    .PARAMETER IncludeNameProperties
    Include user in rule if any of the properties match the value as defined in IncludeName
 
    .PARAMETER ExcludeName
    Exclude user from rule if any of the properties match the value of Name in the properties defined in ExcludeNameProperties
 
    .PARAMETER ExcludeNameProperties
    Exclude user from rule if any of the properties match the value as defined in ExcludeName
 
    .PARAMETER IncludeOU
    Parameter description
 
    .PARAMETER ExcludeOU
    Parameter description
 
    .PARAMETER IncludeGroup
    Parameter description
 
    .PARAMETER ExcludeGroup
    Parameter description
 
    .PARAMETER ReminderDays
    Parameter description
 
    .PARAMETER ManagerReminder
    Parameter description
 
    .PARAMETER ManagerNotCompliant
    Parameter description
 
    .PARAMETER ManagerNotCompliantDisplayName
    Parameter description
 
    .PARAMETER ManagerNotCompliantEmailAddress
    Parameter description
 
    .PARAMETER ManagerNotCompliantDisabled
    Parameter description
 
    .PARAMETER ManagerNotCompliantMissing
    Parameter description
 
    .PARAMETER ManagerNotCompliantMissingEmail
    Parameter description
 
    .PARAMETER ManagerNotCompliantLastLogonDays
    Parameter description
 
    .PARAMETER SecurityEscalation
    Parameter description
 
    .PARAMETER SecurityEscalationDisplayName
    Parameter description
 
    .PARAMETER SecurityEscalationEmailAddress
    Parameter description
 
    .PARAMETER OverwriteEmailProperty
    Parameter description
 
    .PARAMETER OverwriteManagerProperty
    Parameter description
 
    .PARAMETER OverwriteEmailFromExternalUsers
    Allow to overwrite email from external users for specific rule
 
    .PARAMETER ProcessManagersOnly
    This parameters is used to process users, but only managers will be notified.
    Sending emails to users within the rule will be skipped completly.
    This is useful if users would have email addresses, that would normally trigger an email to them.
 
    .EXAMPLE
    An example
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [scriptblock] $ReminderConfiguration,
        [parameter(Mandatory)][string] $Name,
        [switch] $Enable,
        [switch] $IncludeExpiring,
        [switch] $IncludePasswordNeverExpires,
        [nullable[int]]$PasswordNeverExpiresDays,
        [string[]] $IncludeNameProperties,
        [string[]] $IncludeName,

        [string[]] $ExcludeNameProperties,
        [string[]] $ExcludeName,

        [string[]] $IncludeOU,
        [string[]] $ExcludeOU,
        [string[]] $IncludeGroup,
        [string[]] $ExcludeGroup,

        [parameter(Mandatory)][alias('ExpirationDays', 'Days')][Array] $ReminderDays,

        [switch] $ManagerReminder,

        [switch] $ManagerNotCompliant,
        [string] $ManagerNotCompliantDisplayName,
        [string] $ManagerNotCompliantEmailAddress,

        [switch] $ManagerNotCompliantDisabled,
        [switch] $ManagerNotCompliantMissing,
        [switch]$ManagerNotCompliantMissingEmail,
        [nullable[int]] $ManagerNotCompliantLastLogonDays,

        [switch] $SecurityEscalation,
        [string] $SecurityEscalationDisplayName,
        [string] $SecurityEscalationEmailAddress,

        [string] $OverwriteEmailProperty,
        [string] $OverwriteManagerProperty,

        [switch] $ProcessManagersOnly,

        [switch] $OverwriteEmailFromExternalUsers

    )

    $Output = [ordered] @{
        Name                            = $Name
        Enable                          = $Enable.IsPresent
        IncludeExpiring                 = $IncludeExpiring.IsPresent
        IncludePasswordNeverExpires     = $IncludePasswordNeverExpires.IsPresent
        Reminders                       = $ReminderDays
        PasswordNeverExpiresDays        = $PasswordNeverExpiresDays
        IncludeNameProperties           = $IncludeNameProperties
        IncludeName                     = $IncludeName
        IncludeOU                       = $IncludeOU
        ExcludeOU                       = $ExcludeOU
        SendToManager                   = [ordered] @{}

        ProcessManagersOnly             = $ProcessManagersOnly.IsPresent

        OverwriteEmailProperty          = $OverwriteEmailProperty

        OverwriteManagerProperty        = $OverwriteManagerProperty

        OverwriteEmailFromExternalUsers = $OverwriteEmailFromExternalUsers.IsPresent
    }
    $Output.SendToManager['Manager'] = [ordered] @{
        Enable    = $false
        Reminders = [ordered] @{}
    }
    $Output.SendToManager['ManagerNotCompliant'] = [ordered] @{
        Enable        = $false
        Manager       = [ordered] @{
            DisplayName  = $ManagerNotCompliantDisplayName
            EmailAddress = $ManagerNotCompliantEmailAddress
        }
        Disabled      = $ManagerNotCompliantDisabled
        Missing       = $ManagerNotCompliantMissing
        MissingEmail  = $ManagerNotCompliantMissingEmail
        LastLogon     = if ($PSBoundParameters.ContainsKey('ManagerNotCompliantLastLogonDays')) {
            $true } else {
            $false }
        LastLogonDays = $ManagerNotCompliantLastLogonDays
        Reminders     = [ordered] @{ }
    }
    $Output.SendToManager['SecurityEscalation'] = [ordered] @{
        Enable    = $false
        Manager   = [ordered] @{
            DisplayName  = $SecurityEscalationDisplayName
            EmailAddress = $SecurityEscalationEmailAddress
        }
        Reminders = [ordered] @{}
    }
    if ($ManagerReminder) {
        $Output.SendToManager['Manager'].Enable = $true
    }
    if ($ManagerNotCompliant) {
        $Output.SendToManager['ManagerNotCompliant'].Enable = $true
    }
    if ($SecurityEscalation) {
        $Output.SendToManager['SecurityEscalation'].Enable = $true
    }
    if ($ReminderConfiguration) {
        try {
            $RemindersExecution = & $ReminderConfiguration
        } catch {
            Write-Color -Text "[e]", " Processing rule ", $Output.Name, " failed because of error: ", $_.Exception.Message -Color Yellow, White, Red
            return
        }
        foreach ($Reminder in $RemindersExecution) {
            if ($Reminder.Type -eq 'Manager') {
                foreach ($ReminderReminders in $Reminder.Reminders) {
                    $Output.SendToManager['Manager'].Reminders += $ReminderReminders
                }
            } elseif ($Reminder.Type -eq 'ManagerNotCompliant') {
                foreach ($ReminderReminders in $Reminder.Reminders) {
                    $Output.SendToManager['ManagerNotCompliant'].Reminders += $ReminderReminders
                }
            } elseif ($Reminder.Type -eq 'Security') {
                foreach ($ReminderReminders in $Reminder.Reminders) {
                    $Output.SendToManager['SecurityEscalation'].Reminders += $ReminderReminders
                }
            } else {

                throw "Invalid reminder type: $($Reminder.Type)"
            }
        }
    }

    Remove-EmptyValue -Hashtable $Output -Recursive -Rerun 2
    $Configuration = [ordered] @{
        Type     = 'PasswordConfigurationRule'
        Settings = $Output
    }
    $Configuration
}
function New-PasswordConfigurationRuleReminder {
    [CmdletBinding(DefaultParameterSetName = 'Daily')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'Daily')]
        [Parameter(Mandatory, ParameterSetName = 'DayOfWeek')]
        [Parameter(Mandatory, ParameterSetName = 'DayOfMonth')]
        [ValidateSet('Manager', 'ManagerNotCompliant', 'Security')][string] $Type,

        [Parameter(Mandatory, ParameterSetName = 'Daily')]
        [Parameter(Mandatory, ParameterSetName = 'DayOfWeek')]
        [Parameter(Mandatory, ParameterSetName = 'DayOfMonth')]
        [alias('ConditionDays', 'Days')][Array] $ExpirationDays,

        [Parameter(Mandatory, ParameterSetName = 'DayOfWeek')]
        [ValidateSet(
            'Monday',
            'Tuesday',
            'Wednesday',
            'Thursday',
            'Friday',
            'Saturday',
            'Sunday'
        )][Array] $DayOfWeek,

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

        [Parameter(ParameterSetName = 'Daily')]
        [Parameter(ParameterSetName = 'DayOfWeek')]
        [Parameter(ParameterSetName = 'DayOfMonth')]
        [ValidateSet('lt', 'gt', 'eq', 'in')][string] $ComparisonType
    )

    if (-not $ComparisonType) {
        $ComparisonType = 'eq'
    }

    if ($PSCmdlet.ParameterSetName -eq 'Daily') {
        $Reminders = [ordered] @{
            Type      = $Type
            Reminders = @{
                Default = [ordered] @{
                    Enable = $true
                }
            }
        }
    } elseif ($PSCmdlet.ParameterSetName -eq 'DayOfWeek') {
        $Reminders = [ordered] @{
            Type      = $Type
            Reminders = @{
                OnDayOfWeek = [ordered] @{
                    Enable         = $true
                    Reminder       = $ExpirationDays
                    ComparisonType = $ComparisonType
                    Days           = $DayOfWeek
                }
            }
        }
    } elseif ($PSCmdlet.ParameterSetName -eq 'DayOfMonth') {
        $Reminders = [ordered] @{
            Type      = $Type
            Reminders = @{
                OnDayOfMonth = [ordered] @{
                    Enable         = $true
                    Reminder       = $ExpirationDays
                    ComparisonType = $ComparisonType
                    Days           = $DayOfMonth
                }
            }
        }
    }
    $Reminders
}
function New-PasswordConfigurationTemplate {
    [CmdletBinding()]
    param(
        [parameter(Mandatory)][ScriptBlock] $Template,
        [parameter(Mandatory)][string] $Subject,
        [parameter(Mandatory)][ValidateSet('PreExpiry', 'PostExpiry', 'Manager', 'ManagerNotCompliant', 'Security', 'Admin')] $Type
    )

    $Output = [ordered] @{
        Type     = "PasswordConfigurationTemplate$Type"
        Settings = [ordered] @{
            Template = $Template
            Subject  = $Subject
        }
    }
    $Output
}
function New-PasswordConfigurationType {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][ValidateSet('User', 'Manager', 'Security', 'Admin')][string] $Type,
        [switch] $Enable,
        [int] $SendCountMaximum,
        [string] $DefaultEmail,
        [switch] $AttachCSV
    )

    $Output = [ordered] @{
        Type     = "PasswordConfigurationType$Type"
        Settings = @{
            Enable                 = $Enable.IsPresent
            SendCountMaximum       = $SendCountMaximum
            SendToDefaultEmail     = if ($DefaultEmail) {
                $true } else {
                $false }
            DefaultEmail           = $DefaultEmail
            OverwriteEmailProperty = $OverwriteEmailProperty
            AttachCSV              = $AttachCSV.IsPresent
        }
    }
    $Output
}
function Show-PasswordQuality {
    <#
    .SYNOPSIS
    Creates an HTML report showing password quality for all user objects in Active Directory.
 
    .DESCRIPTION
    Creates an HTML report showing password quality for all user objects in Active Directory.
    This comman utilizes DSInternals PowerShell module to get the data.
    Then it uses PSWriteHTML to create nice looking report.
 
    .PARAMETER FilePath
    Path to the file where report will be saved.
 
    .PARAMETER DontShow
    If specified, report will not be opened in a browser.
 
    .PARAMETER Online
    If specified report will use CDN for JS and CSS files.
    If not specified, it will merge all CSS and JS files into one HTML file.
    This makes the file at least 3MB bigger, even if there is very small amount of data.
    Keep in mind that this report can be created without internet access,
    just that opening it in a browser with -Online switch will require internet access.
 
    .PARAMETER WeakPasswords
    List of weak passwords that should be checked for.
    Provide a list of common passwords that you want to check for, and that your users may have used.
 
    .PARAMETER WeakPasswordsFilePath
    Path to a file that contains weak passwords, one password per line.
 
    .PARAMETER WeakPasswordsHashesFile
    Path to a file that contains NT hashes of weak passwords, one hash in HEX format per line. For performance reasons, the -WeakPasswordHashesSortedFile parameter should be used instead.
 
    .PARAMETER WeakPasswordsHashesSortedFile
    Path to a file that contains NT hashes of weak passwords, one hash in HEX format per line. The hashes must be sorted alphabetically, because a binary search is performed. This parameter is typically used with a list of leaked password hashes from HaveIBeenPwned.
 
    .PARAMETER SeparateDuplicateGroups
    If specified, report will show duplicate groups separately, one group per tab.
 
    .EXAMPLE
    Show-PasswordQuality -FilePath $PSScriptRoot\Reporting\PasswordQuality.html -Online -WeakPasswords "Test1", "Test2", "Test3" -Verbose
 
    .EXAMPLE
    Show-PasswordQuality -FilePath "C:\Support\GitHub\TheDashboard\Ignore\Reports\CustomReports\PasswordQuality_$(Get-Date -f yyyy-MM-dd_HHmmss).html" -WeakPasswords "Test1", "Test2", "Test3" #-Verbose
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [System.Collections.IDictionary] $ExtendedForestInformation,
        [string] $FilePath,
        [switch] $DontShow,
        [switch] $Online,
        [alias('KnownPasswords')][string[]] $WeakPasswords,
        [alias('KnownPasswordsFilePath')][string] $WeakPasswordsFilePath,
        [alias('KnownPasswordsHashesFile')][string] $WeakPasswordsHashesFile,
        [alias('KnownPasswordsHashesSortedFile')][string] $WeakPasswordsHashesSortedFile,
        [switch] $SeparateDuplicateGroups,
        [switch] $PassThru,
        [switch] $AddWorldMap,
        [alias('LogFile')][string] $LogPath,
        [int] $LogMaximum,
        [switch] $LogShowTime,
        [string] $LogTimeFormat = "yyyy-MM-dd HH:mm:ss"
    )
    $TimeStart = Start-TimeLog
    $Script:Reporting = [ordered] @{}
    $Script:Reporting['Version'] = Get-GitHubVersion -Cmdlet 'Show-PasswordQuality' -RepositoryOwner 'evotecit' -RepositoryName 'PasswordSolution'

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

    Set-LoggingCapabilities -LogPath $LogPath -LogMaximum $LogMaximum -ShowTime:$LogShowTime.IsPresent -TimeFormat $TimeFormat

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

    Write-Color '[i]', ' Gathering passwords data' -Color Yellow, DarkGray, Yellow, DarkGray, Magenta
    Write-Color '[i]', ' Using provided ', $WeakPasswords.Count, " weak passwords to verify against." -Color Yellow, DarkGray, Yellow, DarkGray, Magenta
    $TimeStartPasswords = Start-TimeLog
    $findPasswordQualitySplat = @{
        IncludeStatistics             = $true
        WeakPasswords                 = $WeakPasswords
        WeakPasswordsFilePath         = $WeakPasswordsFilePath
        WeakPasswordsHashesFile       = $WeakPasswordsHashesFile
        WeakPasswordsHashesSortedFile = $WeakPasswordsHashesSortedFile
        Forest                        = $Forest
        ExcludeDomains                = $ExcludeDomains
        IncludeDomains                = $IncludeDomains
        ExtendedForestInformation     = $ExtendedForestInformation
    }
    $PasswordQuality = Find-PasswordQuality @findPasswordQualitySplat
    if (-not $PasswordQuality) {

        return
    }
    $Users = $PasswordQuality.Users
    $Statistics = $PasswordQuality.Statistics
    $Countries = $PasswordQuality.StatisticsCountry
    $CountriesCodes = $PasswordQuality.StatisticsCountryCode
    $Continents = $PasswordQuality.StatisticsContinents

    $EndLogPasswords = Stop-TimeLog -Time $TimeStartPasswords -Option OneLiner

    Write-Color '[i]', ' Time to gather passwords data ', $EndLogPasswords -Color Yellow, DarkGray, Yellow, DarkGray, Magenta

    $TimeStartHTML = Start-TimeLog
    Write-Color -Text '[i] ', 'Generating HTML report...' -Color Yellow, DarkGray
    New-HTML {
        New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey
        New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow
        New-HTMLPanelStyle -BorderRadius 0px
        New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ', ' -ArrayJoin

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

        Write-Color -Text '[i] ', 'Generating summary statistics' -Color Yellow, DarkGray

        New-HTMLSection {
            New-HTMLSection -Invisible {
                New-HTMLPanel -Invisible {
                    New-HTMLText -Text @(
                        "This report shows current status of an Active Directory forest $($PasswordQuality.Forest)."
                        "It focuses on the password quality of users in the following domains: "
                    ) -FontSize 12px
                    New-HTMLList {
                        foreach ($Domain in $PasswordQuality.Domains) {
                            New-HTMLListItem -Text $Domain -Color Blue
                        }
                    } -FontSize 12px

                    $WeakPasswordsFileInformation = $PasswordQuality.WeakPasswordsFileInformation
                    if ($WeakPasswords.Count -gt 0 -or $WeakPasswordsFileInformation.WeakPasswordsStats -or $WeakPasswordsFileInformation.WeakPasswordHashesStats -or $WeakPasswordsFileInformation.WeakPasswordHashesSortedStats) {
                        New-HTMLText -Text @(
                            "The report uses following weak password features: "
                        ) -FontSize 12px
                        New-HTMLList {
                            if ($WeakPasswords.Count -gt 0) {
                                New-HTMLListItem -Text @(
                                    "This report uses ", $WeakPasswords.Count, " weak passwords to check for, as provided during runtime."
                                ) -FontSize 12px -Color None, Red, None -FontWeight normal, bold, normal
                            }
                            if ($WeakPasswordsFileInformation.WeakPasswordsStats) {
                                New-HTMLListItem -Text @(
                                    "This report uses weak passwords from ", $WeakPasswordsFileInformation.WeakPasswordsStats.FullName, " to check for, as provided during runtime, size ", $WeakPasswordsFileInformation.WeakPasswordsStats.Size, ", last write time ", $WeakPasswordsFileInformation.WeakPasswordsStats.LastWriteTime, "."
                                ) -FontSize 12px -Color None, Red, None, Blue, None, Blue, None -FontWeight normal, bold, normal, bold, normal, bold, normal
                            }
                            if ($WeakPasswordsFileInformation.WeakPasswordHashesStats) {
                                New-HTMLListItem -Text @(
                                    "This report uses weak passwords hashes from ", $WeakPasswordsFileInformation.WeakPasswordHashesStats.FullName, " to check for, as provided during runtime, size ", $WeakPasswordsFileInformation.WeakPasswordHashesStats.Size, ", last write time ", $WeakPasswordsFileInformation.WeakPasswordHashesStats.LastWriteTime, "."
                                ) -FontSize 12px -Color None, Red, None, Blue, None, Blue, None -FontWeight  normal, bold, normal, bold, normal, bold, normal
                            }
                            if ($WeakPasswordsFileInformation.WeakPasswordHashesSortedStats) {
                                New-HTMLListItem -Text @(
                                    "This report uses weak passwords hashes from ", $WeakPasswordsFileInformation.WeakPasswordHashesSortedStats.FullName, " to check for, as provided during runtime, size ", $WeakPasswordsFileInformation.WeakPasswordHashesSortedStats.Size, ", last write time ", $WeakPasswordsFileInformation.WeakPasswordHashesSortedStats.LastWriteTime, "."
                                ) -FontSize 12px -Color None, Red, None, Blue, None, Blue, None -FontWeight normal, bold, normal, bold, normal, bold, normal
                            }
                        }
                    }

                    New-HTMLText -Text "Here's a short overview of what this report shows:" -Color None -FontSize 12px

                    New-HTMLList {
                        foreach ($Statistic in $Statistics.Keys | Where-Object { $_ -notlike '*EnabledOnly' -and $_ -notlike '*DisabledOnly' } ) {
                            $ValueTotal = $Statistics[$Statistic]
                            if ($Statistic -eq "DuplicatePasswordGroups") {
                                $ValueEnabled = $Statistics['DuplicatePasswordUsersEnabledOnly']
                                $ValueDisabled = $Statistics['DuplicatePasswordUsersDisabledOnly']
                                New-HTMLListItem -Text @(
                                    "$($Statistic)",
                                    " property shows there are "
                                    "$ValueTotal"
                                    " groups of people with duplicate passwords."
                                ) -Color Blue, None, Salmon, None, LightSkyBlue, None -FontWeight bold, normal, bold, normal, bold, normal
                            } elseif ($Statistic -eq 'DuplicatePasswordUsers') {

                                $ValueEnabled = $Statistics['DuplicatePasswordUsersEnabledOnly']
                                $ValueDisabled = $Statistics['DuplicatePasswordUsersDisabledOnly']

                                New-HTMLListItem -Text @(
                                    "$($Statistic)",
                                    " property shows there are "
                                    "$ValueEnabled"
                                    "enabled accounts, and "
                                    $ValueDisabled
                                    " disabled accounts having duplicate passwords with other accounts."
                                ) -Color Blue, None, Salmon, None, LightSkyBlue, None -FontWeight bold, normal, bold, normal, bold, normal
                            } else {
                                $ValueEnabled = $Statistics[$Statistic + 'EnabledOnly']
                                $ValueDisabled = $Statistics[$Statistic + 'DisabledOnly']

                                New-HTMLListItem -Text @(
                                    "$($Statistic)",
                                    " property shows there are "
                                    "$ValueEnabled "
                                    "enabled accounts, and "
                                    "$ValueDisabled "
                                    "that are disabled."
                                ) -Color Blue, None, Salmon, None, LightSkyBlue, None -FontWeight bold, normal, bold, normal, bold, normal
                            }
                        }
                    } -Type Unordered -FontSize 12px

                    New-HTMLText -Text "Please review the report and make sure that you're happy with findings!" -Color Blue -FontSize 12px
                }
            }
            New-HTMLSection -Invisible {
                New-HTMLChart {
                    New-ChartBarOptions -Type barStacked
                    New-ChartAxisY -LabelMaxWidth 250 -Show -LabelAlign left
                    New-ChartLegend -LegendPosition bottom -HorizontalAlign center -Color Alizarin, LightSkyBlue -Names 'Enabled', 'Disabled'
                    foreach ($Statistic in $Statistics.Keys | Where-Object { $_ -notlike '*EnabledOnly' -and $_ -notlike '*DisabledOnly' } ) {
                        if ($Statistic -eq "DuplicatePasswordGroups") {
                            $ValueTotal = $Statistics[$Statistic]
                            New-ChartBar -Name $Statistic -Value @($ValueTotal, 0)
                        } else {
                            $ValueEnabled = $Statistics[$Statistic + 'EnabledOnly']
                            $ValueDisabled = $Statistics[$Statistic + 'DisabledOnly']
                            New-ChartBar -Name $Statistic -Value @($ValueEnabled, $ValueDisabled)
                        }
                    }

                }
            }
        }

        $PropertiesHighlight = @(
            'ClearTextPassword'
            'LMHash'
            'EmptyPassword'
            'WeakPassword'

            'AESKeysMissing'
            'PreAuthNotRequired'
            'DESEncryptionOnly'
            'Kerberoastable'
            'DelegatableAdmins'
            'SmartCardUsersWithPassword'

        )

        Write-Color -Text '[i] ', 'Generating users table with all information' -Color Yellow, DarkGray

        New-HTMLSection -HeaderText "Password Quality" {
            New-HTMLTable -DataTable $Users -Filtering {
                New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $true -BackgroundColor LimeGreen -FailBackgroundColor BlizzardBlue
                New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator lt -Value 30 -BackgroundColor LimeGreen -HighlightHeaders LastLogonDays, LastLogonDate
                New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator gt -Value 30 -BackgroundColor Orange -HighlightHeaders LastLogonDays, LastLogonDate
                New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator gt -Value 60 -BackgroundColor Alizarin -HighlightHeaders LastLogonDays, LastLogonDate
                New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType string -Operator eq -Value '' -BackgroundColor None -HighlightHeaders LastLogonDays, LastLogonDate
                New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -Operator ge -Value 0 -BackgroundColor LimeGreen -HighlightHeaders PasswordLastSet, PasswordLastChangedDays
                New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -Operator gt -Value 300 -BackgroundColor Orange -HighlightHeaders PasswordLastSet, PasswordLastChangedDays
                New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -Operator gt -Value 360 -BackgroundColor Alizarin -HighlightHeaders PasswordLastSet, PasswordLastChangedDays
                New-HTMLTableCondition -Name 'PasswordNotRequired' -ComparisonType string -Operator eq -Value $false -BackgroundColor LimeGreen -FailBackgroundColor Alizarin
                New-HTMLTableCondition -Name 'PasswordExpired' -ComparisonType string -Operator eq -Value $false -BackgroundColor LimeGreen -FailBackgroundColor Alizarin -HighlightHeaders PasswordExpired, DaysToExpire, DateExpiry

                foreach ($Property in $PropertiesHighlight) {
                    New-HTMLTableCondition -Name $Property -ComparisonType string -Operator eq -Value $true -BackgroundColor Salmon -FailBackgroundColor LightGreen
                }
                New-HTMLTableCondition -Name 'DuplicatePasswordGroups' -ComparisonType string -Operator ne -Value "" -BackgroundColor Orange -FailBackgroundColor LightGreen
            } -ScrollX -ExcludeProperty 'RuleName', 'RuleOptions', 'CountryCode', 'Type', 'ManagerDN', 'DistinguishedName', 'MemberOf'

        }
        if ($SeparateDuplicateGroups) {
            Write-Color -Text '[i] ', 'Generating duplicate password groups section' -Color Yellow, DarkGray
            New-HTMLSection -HeaderText "Duplicate Password Groups" {
                $TotalDuplicateGroups = 0
                $EnabledUsersInDuplicateGroups = 0
                $DisabledUsersInDuplicateGroups = 0
                $DuplicateGroups = [ordered] @{}
                foreach ($User in $Users) {
                    if ($User.DuplicatePasswordGroups) {
                        if ($User.Enabled) {
                            $EnabledUsersInDuplicateGroups++
                        } else {
                            $DisabledUsersInDuplicateGroups++
                        }
                        if (-not $DuplicateGroups[$User.DuplicatePasswordGroups]) {
                            $DuplicateGroups[$User.DuplicatePasswordGroups] = [PSCustomObject] @{
                                GroupName             = $User.DuplicatePasswordGroups
                                UsersTotal            = 0
                                UsersEnabled          = 0
                                UsersDisabled         = 0
                                WeakPassword          = $false
                                Users                 = [System.Collections.Generic.List[string]]::new()
                                Country               = [System.Collections.Generic.List[string]]::new()
                                UsersBySamAccountName = [System.Collections.Generic.List[string]]::new()
                                UsersByUPN            = [System.Collections.Generic.List[string]]::new()
                                UsersByEmail          = [System.Collections.Generic.List[string]]::new()
                            }
                        }
                        if ($User.WeakPassword) {
                            $DuplicateGroups[$User.DuplicatePasswordGroups].WeakPassword = $true
                        }
                        $DuplicateGroups[$User.DuplicatePasswordGroups].Users.Add($User.Name)
                        if ($User.Enabled) {
                            $DuplicateGroups[$User.DuplicatePasswordGroups].UsersEnabled++
                        } else {
                            $DuplicateGroups[$User.DuplicatePasswordGroups].UsersDisabled++
                        }
                        $DuplicateGroups[$User.DuplicatePasswordGroups].UsersTotal++

                        if ($User.EmailAddress) {
                            $DuplicateGroups[$User.DuplicatePasswordGroups].UsersByEmail.Add($User.EmailAddress)
                        }
                        if ($User.UserPrincipalName) {
                            $DuplicateGroups[$User.DuplicatePasswordGroups].UsersByUPN.Add($User.UserPrincipalName)
                        }
                        if ($User.SamAccountName) {
                            $DuplicateGroups[$User.DuplicatePasswordGroups].UsersBySamAccountName.Add($User.SamAccountName)
                        }
                        $DuplicateGroups[$User.DuplicatePasswordGroups].Country.Add($User.Country)
                    }
                }

                $TotalDuplicateGroups = $DuplicateGroups.Keys.Count

                foreach ($Group in $DuplicateGroups.Values) {

                    $Group.Country = $Group.Country | Select-Object -Unique
                }

                New-HTMLContainer {
                    New-HTMLSection {
                        New-HTMLPanel {
                            New-HTMLToast -TextHeader 'Total Duplicate Groups' -Text "Groups of users to review: $TotalDuplicateGroups" -BarColorLeft MayaBlue -IconSolid info-circle -IconColor MayaBlue
                        } -Invisible
                        New-HTMLPanel {
                            New-HTMLToast -TextHeader 'Enabled Users' -Text "Users with duplicate password that are enabled: $EnabledUsersInDuplicateGroups" -BarColorLeft OrangeRed -IconSolid info-circle -IconColor OrangeRed
                        } -Invisible
                        New-HTMLPanel {
                            New-HTMLToast -TextHeader 'Disabled Users' -Text "Users with duplicate password that are disabled: $DisabledUsersInDuplicateGroups" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel
                        } -Invisible
                    } -Invisible

                    New-HTMLText -Text @(
                        'The following table shows the users that have the same password as other users in the same group. '
                        'The table is sortable and filterable. '
                        'The table also shows the country of the user. '
                        'The table also shows the email address, UPN and SamAccountName of the user.'
                    ) -FontSize 12px

                    New-HTMLSection -Invisible {
                        New-HTMLTable -DataTable $DuplicateGroups.Values -Filtering -Title "Duplicate Password Group: $DuplicateGroup" {
                            New-HTMLTableCondition -Name 'WeakPassword' -ComparisonType string -Operator eq -Value $true -BackgroundColor Salmon -FailBackgroundColor LightBlue
                        }-ScrollX -ExcludeProperty 'RuleName', 'RuleOptions', 'Type', 'CountryCode'
                    }

                    New-HTMLText -Text @(
                        "Please NOTE: "
                        "number of "
                        "users"
                        " , may not be the same as the number of users in "
                        "UsersBySamAccountName"
                        ", "
                        "UsersByUpn"
                        " or "
                        "UsersByEmail"
                        " columns. We only show users with email address, UPN or SamAccountName if it exists. "
                        "If the account doesn't have email, UPN or SamAccountName, we don't show it in the table."
                    ) -FontSize 12px -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold, normal, normal
                }
            }
        }
        if ($AddWorldMap) {
            Write-Color -Text '[i] ', 'Generating duplicate passwords map' -Color Yellow, DarkGray
            New-HTMLSection -HeaderText 'Duplicate Passwords Per Country' {
                New-HTMLTabPanel {
                    New-HTMLTab -Name 'Map showing duplicate passwords per country' {
                        New-HTMLSection -Invisible {
                            New-HTMLPanel {
                                New-HTMLMap -Map world_countries {

                                    foreach ($Country in $CountriesCodes['DuplicatePasswordUsers'].Keys) {
                                        if ($Country -eq 'Unknown') {
                                            New-MapArea -Area 'GL' -Value $CountriesCodes['DuplicatePasswordUsers'][$Country] -Tooltip {
                                                New-HTMLText -Text @(
                                                    'Unknown / Unavailable'
                                                    '<br>'
                                                    "Users with duplicate passwords $($CountriesCodes['DuplicatePasswordUsers'][$Country])"
                                                ) -Color Black, Black, Blue -FontWeight bold, normal, normal -SkipParagraph -FontSize 15px, 14px, 14px
                                            }
                                        } else {
                                            New-MapArea -Area $Country -Value $CountriesCodes['DuplicatePasswordUsers'][$Country] -Tooltip {
                                                New-HTMLText -Text @(
                                                    Convert-CountryCodeToCountry -CountryCode $Country
                                                    '<br>'
                                                    "Users with duplicate passwords $($CountriesCodes['DuplicatePasswordUsers'][$Country])"
                                                ) -Color Black, Black, Blue -FontWeight bold, normal, normal -SkipParagraph -FontSize 15px, 14px, 14px
                                            }
                                        }
                                    }

                                    New-MapLegendOption -Type 'Area' -Mode horizontal
                                    New-MapLegendOption -Type 'Plot' -Mode horizontal

                                    New-MapLegendSlice -Type 'Area' -Label 'Duplicate passwords up to 5' -Min 0 -Max 5 -SliceColor 'Bisque' -StrokeWidth 0
                                    New-MapLegendSlice -Type 'Area' -Label 'Duplicate between 5 and 15' -Min 6 -Max 15 -SliceColor 'Amber' -StrokeWidth 0
                                    New-MapLegendSlice -Type 'Area' -Label 'Duplicate between 16 and 30' -Min 16 -Max 30 -SliceColor 'CarnationPink' -StrokeWidth 0
                                    New-MapLegendSlice -Type 'Area' -Label 'Duplicate between 31 and 50' -Min 31 -Max 50 -SliceColor 'BrinkPink' -StrokeWidth 0
                                    New-MapLegendSlice -Type 'Area' -Label 'Duplicate over 50' -Min 51 -SliceColor 'Red' -StrokeWidth 0
                                } -ShowAreaLegend 
                                New-HTMLText -Text @(
                                    "The map shows the number of users with duplicate passwords per country. The legend shows the number of users with duplicate passwords per color."
                                ) -FontSize 12px
                            }
                        }
                    }
                    New-HTMLTab -Name 'Duplicate Passwords Per Country' {
                        New-HTMLTable -DataTable $Countries['DuplicatePasswordUsers'] -Filtering
                    }
                    New-HTMLTab -Name 'Duplicate Passwords Per Continent' {
                        New-HTMLTable -DataTable $Continents['DuplicatePasswordUsers'] -Filtering
                    }
                }
            }
            Write-Color -Text '[i] ', 'Generating weak password map' -Color Yellow, DarkGray
            New-HTMLSection -HeaderText 'Weak Password Per Country' {
                New-HTMLTabPanel {
                    New-HTMLTab -Name 'Map showing weak password per country' {
                        New-HTMLSection -Invisible {
                            New-HTMLPanel {
                                New-HTMLMap -Map world_countries {

                                    foreach ($Country in $CountriesCodes['WeakPassword'].Keys) {
                                        if ($Country -eq 'Unknown') {
                                            New-MapArea -Area 'GL' -Value $CountriesCodes['WeakPassword'][$Country] -Tooltip {
                                                New-HTMLText -Text @(
                                                    'Unknown / Unavailable'
                                                    '<br>'
                                                    "Users with weak passwords $($CountriesCodes['WeakPassword'][$Country])"
                                                ) -Color Black, Black, Blue -FontWeight bold, normal, normal -SkipParagraph -FontSize 15px, 14px, 14px
                                            }
                                        } else {
                                            New-MapArea -Area $Country -Value $CountriesCodes['WeakPassword'][$Country] -Tooltip {
                                                New-HTMLText -Text @(
                                                    Convert-CountryCodeToCountry -CountryCode $Country
                                                    '<br>'
                                                    "Users with weak passwords $($CountriesCodes['WeakPassword'][$Country])"
                                                ) -Color Black, Black, Blue -FontWeight bold, normal, normal -SkipParagraph -FontSize 15px, 14px, 14px
                                            }
                                        }
                                    }

                                    New-MapLegendOption -Type 'Area' -Mode horizontal
                                    New-MapLegendOption -Type 'Plot' -Mode horizontal

                                    New-MapLegendSlice -Type 'Area' -Label 'Weak passwords up to 5' -Min 0 -Max 5 -SliceColor 'Bisque' -StrokeWidth 0
                                    New-MapLegendSlice -Type 'Area' -Label 'Weak between 5 and 15' -Min 6 -Max 15 -SliceColor 'Amber' -StrokeWidth 0
                                    New-MapLegendSlice -Type 'Area' -Label 'Weak between 16 and 30' -Min 16 -Max 30 -SliceColor 'CarnationPink' -StrokeWidth 0
                                    New-MapLegendSlice -Type 'Area' -Label 'Weak between 31 and 50' -Min 31 -Max 50 -SliceColor 'BrinkPink' -StrokeWidth 0
                                    New-MapLegendSlice -Type 'Area' -Label 'Weak over 50' -Min 51 -SliceColor 'Red' -StrokeWidth 0
                                } -ShowAreaLegend 
                            }
                        }
                    }
                    New-HTMLTab -Name 'Weak Password Per Country' {
                        New-HTMLTable -DataTable $Countries['WeakPassword'] -Filtering
                    }
                    New-HTMLTab -Name 'Weak Password Per Continent' {
                        New-HTMLTable -DataTable $Continents['WeakPassword'] -Filtering
                    }
                }
            }
            if ($LogPath -and (Test-Path -LiteralPath $LogPath)) {
                $LogContent = Get-Content -Raw -LiteralPath $LogPath
                New-HTMLSection -Name 'Log' {
                    New-HTMLCodeBlock -Code $LogContent -Style generic
                }
            }
        }
    } -ShowHTML:(-not $DontShow.IsPresent) -Online:$Online.IsPresent -TitleText "Password Solution - Quality Password Check" -Author "Password Solution" -FilePath $FilePath

    $EndLogHTML = Stop-TimeLog -Time $TimeStartHTML -Option OneLiner
    $EndLog = Stop-TimeLog -Time $TimeStart -Option OneLiner
    Write-Color '[i]', ' Time to generate HTML ', $EndLogHTML -Color Yellow, DarkGray, Yellow, DarkGray, Magenta
    Write-Color '[i]', ' Time to generate ', $EndLog -Color Yellow, DarkGray, Yellow, DarkGray, Magenta
    Write-Color '[i]', "[PasswordSolution] ", 'Version', ' [Informative] ', $Script:Reporting['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta

    if ($PassThru) {
        $PasswordQuality
    }
}
function Start-PasswordSolution {
    <#
    .SYNOPSIS
    Starts Password Expiry Notifications for the whole forest
 
    .DESCRIPTION
    Starts Password Expiry Notifications for the whole forest
 
    .PARAMETER ConfigurationDSL
    Parameter description
 
    .PARAMETER EmailParameters
    Parameters for Email. Uses Mailozaurr splatting behind the scenes, so it supports all options that Mailozaurr does.
 
    .PARAMETER OverwriteEmailProperty
    Property responsible for overwriting the default email field in Active Directory. Useful when the password notification has to go somewhere else than users email address.
 
    .PARAMETER UserSection
    Parameter description
 
    .PARAMETER ManagerSection
    Parameter description
 
    .PARAMETER SecuritySection
    Parameter description
 
    .PARAMETER AdminSection
    Parameter description
 
    .PARAMETER UsersExternalSystem
    Property responsible for overwriting the default email field in Active Directory.
    Useful when the password notification has to go somewhere else than users email address.
    It comes in a specific format as generated by `New-PasswordConfigurationExternalUsers`
 
    .PARAMETER Rules
    Parameter description
 
    .PARAMETER TemplatePreExpiry
    Parameter description
 
    .PARAMETER TemplatePreExpirySubject
    Parameter description
 
    .PARAMETER TemplatePostExpiry
    Parameter description
 
    .PARAMETER TemplatePostExpirySubject
    Parameter description
 
    .PARAMETER TemplateManager
    Parameter description
 
    .PARAMETER TemplateManagerSubject
    Parameter description
 
    .PARAMETER TemplateSecurity
    Parameter description
 
    .PARAMETER TemplateSecuritySubject
    Parameter description
 
    .PARAMETER TemplateManagerNotCompliant
    Parameter description
 
    .PARAMETER TemplateManagerNotCompliantSubject
    Parameter description
 
    .PARAMETER TemplateAdmin
    Parameter description
 
    .PARAMETER TemplateAdminSubject
    Parameter description
 
    .PARAMETER Logging
    Parameter description
 
    .PARAMETER HTMLReports
    Parameter description
 
    .PARAMETER SearchPath
    Parameter description
 
    .EXAMPLE
    An example
 
    .NOTES
    General notes
    #>

    [CmdletBinding(DefaultParameterSetName = 'DSL')]
    param(
        [Parameter(ParameterSetName = 'Legacy', Position = 0)]
        [Parameter(ParameterSetName = 'DSL', Position = 0)][scriptblock] $ConfigurationDSL,
        [Parameter(Mandatory, ParameterSetName = 'Legacy')][System.Collections.IDictionary] $EmailParameters,
        [Parameter(ParameterSetName = 'Legacy')][string] $OverwriteEmailProperty,
        [Parameter(ParameterSetName = 'Legacy')][string] $OverwriteManagerProperty,
        [Parameter(Mandatory, ParameterSetName = 'Legacy')][System.Collections.IDictionary] $UserSection,
        [Parameter(Mandatory, ParameterSetName = 'Legacy')][System.Collections.IDictionary] $ManagerSection,
        [Parameter(Mandatory, ParameterSetName = 'Legacy')][System.Collections.IDictionary] $SecuritySection,
        [Parameter(Mandatory, ParameterSetName = 'Legacy')][System.Collections.IDictionary] $AdminSection,
        [Parameter(ParameterSetName = 'Legacy')][System.Collections.IDictionary] $UsersExternalSystem,
        [Parameter(Mandatory, ParameterSetName = 'Legacy')][Array] $Rules,
        [Parameter(ParameterSetName = 'Legacy')][scriptblock] $TemplatePreExpiry,
        [Parameter(ParameterSetName = 'Legacy')][string] $TemplatePreExpirySubject,
        [Parameter(ParameterSetName = 'Legacy')][scriptblock] $TemplatePostExpiry,
        [Parameter(ParameterSetName = 'Legacy')][string] $TemplatePostExpirySubject,
        [Parameter(Mandatory, ParameterSetName = 'Legacy')][scriptblock] $TemplateManager,
        [Parameter(Mandatory, ParameterSetName = 'Legacy')][string] $TemplateManagerSubject,
        [Parameter(Mandatory, ParameterSetName = 'Legacy')][scriptblock] $TemplateSecurity,
        [Parameter(Mandatory, ParameterSetName = 'Legacy')][string] $TemplateSecuritySubject,
        [Parameter(Mandatory, ParameterSetName = 'Legacy')][scriptblock] $TemplateManagerNotCompliant,
        [Parameter(Mandatory, ParameterSetName = 'Legacy')][string] $TemplateManagerNotCompliantSubject,
        [Parameter(Mandatory, ParameterSetName = 'Legacy')][scriptblock] $TemplateAdmin,
        [Parameter(Mandatory, ParameterSetName = 'Legacy')][string] $TemplateAdminSubject,
        [Parameter(ParameterSetName = 'Legacy')][System.Collections.IDictionary] $Logging = @{},
        [Parameter(ParameterSetName = 'Legacy')][Array] $HTMLReports,
        [Parameter(ParameterSetName = 'Legacy')][string] $SearchPath,
        [Parameter(ParameterSetName = 'Legacy')][string[]] $FilterOrganizationalUnit
    )
    $TimeStart = Start-TimeLog
    $Script:Reporting = [ordered] @{}
    $Script:Reporting['Version'] = Get-GitHubVersion -Cmdlet 'Start-PasswordSolution' -RepositoryOwner 'evotecit' -RepositoryName 'PasswordSolution'

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

    $TodayDate = Get-Date
    $Today = Get-Date -Format "yyyy-MM-dd HH:mm:ss"

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

    $AllSkipped = [ordered] @{}
    $Locations = [ordered] @{}

    $SplatPasswordConfiguration = [ordered] @{
        ConfigurationDSL                   = $ConfigurationDSL
        EmailParameters                    = $EmailParameters
        OverwriteEmailProperty             = $OverwriteEmailProperty
        OverwriteManagerProperty           = $OverwriteManagerProperty
        UserSection                        = $UserSection
        ManagerSection                     = $ManagerSection
        SecuritySection                    = $SecuritySection
        AdminSection                       = $AdminSection
        Rules                              = $Rules
        TemplatePreExpiry                  = $TemplatePreExpiry
        TemplatePreExpirySubject           = $TemplatePreExpirySubject
        TemplatePostExpiry                 = $TemplatePostExpiry
        TemplatePostExpirySubject          = $TemplatePostExpirySubject
        TemplateManager                    = $TemplateManager
        TemplateManagerSubject             = $TemplateManagerSubject
        TemplateSecurity                   = $TemplateSecurity
        TemplateSecuritySubject            = $TemplateSecuritySubject
        TemplateManagerNotCompliant        = $TemplateManagerNotCompliant
        TemplateManagerNotCompliantSubject = $TemplateManagerNotCompliantSubject
        TemplateAdmin                      = $TemplateAdmin
        TemplateAdminSubject               = $TemplateAdminSubject
        Logging                            = $Logging
        HTMLReports                        = $HTMLReports
        SearchPath                         = $SearchPath
        UsersExternalSystem                = $UsersExternalSystem
        FilterOrganizationalUnit           = $FilterOrganizationalUnit
    }
    $InitialVariables = Set-PasswordConfiguration @SplatPasswordConfiguration
    if (-not $InitialVariables) {
        return
    }

    $EmailParameters = $InitialVariables.EmailParameters
    $OverwriteEmailProperty = $InitialVariables.OverwriteEmailProperty
    $OverwriteManagerProperty = $InitialVariables.OverwriteManagerProperty
    $UserSection = $InitialVariables.UserSection
    $ManagerSection = $InitialVariables.ManagerSection
    $SecuritySection = $InitialVariables.SecuritySection
    $AdminSection = $InitialVariables.AdminSection
    $Rules = $InitialVariables.Rules
    $TemplatePreExpiry = $InitialVariables.TemplatePreExpiry
    $TemplatePreExpirySubject = $InitialVariables.TemplatePreExpirySubject
    $TemplatePostExpiry = $InitialVariables.TemplatePostExpiry
    $TemplatePostExpirySubject = $InitialVariables.TemplatePostExpirySubject
    $TemplateManager = $InitialVariables.TemplateManager
    $TemplateManagerSubject = $InitialVariables.TemplateManagerSubject
    $TemplateSecurity = $InitialVariables.TemplateSecurity
    $TemplateSecuritySubject = $InitialVariables.TemplateSecuritySubject
    $TemplateManagerNotCompliant = $InitialVariables.TemplateManagerNotCompliant
    $TemplateManagerNotCompliantSubject = $InitialVariables.TemplateManagerNotCompliantSubject
    $TemplateAdmin = $InitialVariables.TemplateAdmin
    $TemplateAdminSubject = $InitialVariables.TemplateAdminSubject
    $Logging = $InitialVariables.Logging
    $HTMLReports = $InitialVariables.HTMLReports
    $SearchPath = $InitialVariables.SearchPath
    $UsersExternalSystem = $InitialVariables.UsersExternalSystem
    $FilterOrganizationalUnit = $InitialVariables.FilterOrganizationalUnit

    Set-LoggingCapabilities -LogPath $Logging.LogFile -LogMaximum $Logging.LogMaximum -ShowTime:$Logging.ShowTime -TimeFormat $Logging.TimeFormat

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

    [Array] $ExtendedProperties = foreach ($Rule in $Rules ) {
        if ($Rule.OverwriteEmailProperty) {
            $Rule.OverwriteEmailProperty
        }
        if ($Rule.OverwriteManagerProperty) {
            $Rule.OverwriteManagerProperty
        }
    }

    $SummarySearch = Import-SearchInformation -SearchPath $SearchPath

    Write-Color -Text "[i]", " Starting process to find expiring users" -Color Yellow, White, Green, White, Green, White, Green, White
    $ExternalSystemReplacements = [ordered] @{}

    $GlobalManagerCache = [ordered] @{}
    $CachedUsers = Find-Password -AsHashTable -OverwriteEmailProperty $OverwriteEmailProperty -RulesProperties $ExtendedProperties -OverwriteManagerProperty $OverwriteManagerProperty -UsersExternalSystem $UsersExternalSystem -ExternalSystemReplacements $ExternalSystemReplacements -FilterOrganizationalUnit $FilterOrganizationalUnit -CacheManager $GlobalManagerCache

    if ($Rules.Count -eq 0) {
        Write-Color -Text "[e]", " No rules found. Please add some rules to configuration" -Color Yellow, White, Red
        return
    }

    foreach ($Rule in $Rules) {
        $SplatProcessingRule = [ordered] @{
            Rule                = $Rule
            Summary             = $Summary
            CachedUsers         = $CachedUsers
            AllSkipped          = $AllSkipped
            Locations           = $Locations
            Loggin              = $Logging
            TodayDate           = $TodayDate
            UsersExternalSystem = $UsersExternalSystem
        }
        Invoke-PasswordRuleProcessing @SplatProcessingRule
    }

    $SplatUserNotifications = [ordered] @{
        UserSection               = $UserSection
        Summary                   = $Summary
        Logging                   = $Logging
        TemplatePreExpiry         = $TemplatePreExpiry
        TemplatePreExpirySubject  = $TemplatePreExpirySubject
        TemplatePostExpiry        = $TemplatePostExpiry
        TemplatePostExpirySubject = $TemplatePostExpirySubject
        EmailParameter            = $EmailParameters
    }

    [Array] $SummaryUsersEmails = Send-PasswordUserNofifications @SplatUserNotifications

    $SplatManagerNotifications = [ordered] @{
        ManagerSection                     = $ManagerSection
        Summary                            = $Summary
        CachedUsers                        = $CachedUsers
        TemplateManager                    = $TemplateManager
        TemplateManagerSubject             = $TemplateManagerSubject
        TemplateManagerNotCompliant        = $TemplateManagerNotCompliant
        TemplateManagerNotCompliantSubject = $TemplateManagerNotCompliantSubject
        EmailParameters                    = $EmailParameters
        Loggin                             = $Logging
        GlobalManagersCache                = $GlobalManagerCache
    }
    [Array] $SummaryManagersEmails = Send-PasswordManagerNofifications @SplatManagerNotifications

    $SplatSecurityNotifications = [ordered] @{
        SecuritySection         = $SecuritySection
        Summary                 = $Summary
        TemplateSecurity        = $TemplateSecurity
        TemplateSecuritySubject = $TemplateSecuritySubject
        Logging                 = $Logging
    }
    [Array] $SummaryEscalationEmails = Send-PasswordSecurityNotifications @SplatSecurityNotifications

    $TimeEnd = Stop-TimeLog -Time $TimeStart -Option OneLiner

    Export-SearchInformation -SearchPath $SearchPath -SummarySearch $SummarySearch -Today $Today -SummaryUsersEmails $SummaryUsersEmails -SummaryManagersEmails $SummaryManagersEmails -SummaryEscalationEmails $SummaryEscalationEmails

    $HtmlAttachments = [System.Collections.Generic.List[string]]::new()

    foreach ($Report in $HTMLReports) {
        if ($Report.Enable) {
            $ReportSettings = @{
                Report                     = $Report
                EmailParameters            = $EmailParameters
                Logging                    = $Logging

                SearchPath                 = $SearchPath
                Rules                      = $Rules
                UserSection                = $UserSection
                ManagerSection             = $ManagerSection
                SecuritySection            = $SecuritySection
                AdminSection               = $AdminSection
                CachedUsers                = $CachedUsers
                Summary                    = $Summary
                SummaryUsersEmails         = $SummaryUsersEmails
                SummaryManagersEmails      = $SummaryManagersEmails
                SummaryEscalationEmails    = $SummaryEscalationEmails
                SummarySearch              = $SummarySearch
                Locations                  = $Locations
                AllSkipped                 = $AllSkipped
                ExternalSystemReplacements = $ExternalSystemReplacements
            }
            New-HTMLReport @ReportSettings

            if ($Report.AttachToEmail) {
                if (Test-Path -LiteralPath $Report.FilePath) {
                    $HtmlAttachments.Add($Report.FilePath)
                } else {
                    Write-Color -Text "[w] HTML report ", $Report.FilePath, " does not exist! Probably a temporary path was used. " -Color DarkYellow, Red, DarkYellow
                }
            }
        }
    }

    $AdminSplat = [ordered] @{
        AdminSection         = $AdminSection
        TemplateAdmin        = $TemplateAdmin
        TemplateAdminSubject = $TemplateAdminSubject
        TimeEnd              = $TimeEnd
        EmailParameters      = $EmailParameters
        HtmlAttachment       = $HtmlAttachments
    }
    Send-PasswordAdminNotifications @AdminSplat
}

Export-ModuleMember -Function @('Find-Password', 'Find-PasswordNotification', 'Find-PasswordQuality', 'New-PasswordConfigurationEmail', 'New-PasswordConfigurationExternalUsers', 'New-PasswordConfigurationOption', 'New-PasswordConfigurationReport', 'New-PasswordConfigurationRule', 'New-PasswordConfigurationRuleReminder', 'New-PasswordConfigurationTemplate', 'New-PasswordConfigurationType', 'Show-PasswordQuality', 'Start-PasswordSolution') -Alias @()
# SIG # Begin signature block
# MIItsQYJKoZIhvcNAQcCoIItojCCLZ4CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDYd5DaEvfURaXi
# Elzd14bRIab+PVVMI7Yordo1nj4UuaCCJrQwggWNMIIEdaADAgECAhAOmxiO+dAt
# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa
# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E
# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy
# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF
# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1
# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB
# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR
# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6
# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB
# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S
# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x
# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB
# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP
# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC
# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc
# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov
# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy
# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW
# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF
# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z
# twGpn1eqXijiuZQwggWQMIIDeKADAgECAhAFmxtXno4hMuI5B72nd3VcMA0GCSqG
# SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvkXUo8MCIw
# aTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdtHauyefLK
# EdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu34LzB4Tm
# dDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0QF+xembu
# d8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2kZhAkHnD
# eMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM1LyuGwN1
# XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmIdph2PVld
# QnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZK37AlLTS
# YW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72gLD76GSm
# M9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqsX40/ybzT
# QRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyhHsXAj6Kx
# fgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD
# VR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzANBgkq
# hkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNkaA9Wz3eucPn9mkqZucl4
# XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjSPMFDQK4dUPVS/JA7u5iZ
# aWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK7VB6fWIhCoDIc2bRoAVg
# X+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eBcg3AFDLvMFkuruBx8lbk
# apdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp5aPNoiBB19GcZNnqJqGL
# FNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msgdDDS4Dk0EIUhFQEI6FUy
# 3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vriRbgjU2wGb2dVf0a1TD9u
# KFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ79ARj6e/CVABRoIoqyc54
# zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5nLGbsQAe79APT0JsyQq8
# 7kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3i0objwG2J5VT6LaJbVu8
# aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0HEEcRrYc9B9F1vM/zZn4w
# ggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqG
# SIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbS
# g9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9
# /UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXn
# HwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0
# VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4f
# sbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40Nj
# gHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0
# QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvv
# mz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T
# /jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk
# 42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5r
# mQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E
# FgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5n
# P+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcG
# CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu
# Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln
# aUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8v
# Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNV
# HSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIB
# AH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxp
# wc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIl
# zpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQ
# cAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfe
# Kuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+j
# Sbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJsh
# IUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6
# OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDw
# N7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR
# 81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2
# VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIGsDCCBJigAwIBAgIQ
# CK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQGEwJVUzEV
# MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t
# MSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjEwNDI5MDAw
# MDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT
# aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zrPYGXcMW7xIUmMJ+k
# jmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHMgQM+TXAkZLON4gh9
# NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8IrgnQnAZaf6mIBJNYc9
# URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyCEUhSaN4QvRRXXegY
# E2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0p6MDDnSlrzm2q2AS
# 4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQakhCBj7A7CdfHmzJa
# wv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0XLyTRSiDNipmKF+w
# c86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960IHnWmZcy740hQ83eR
# Gv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2FKZbS110YU0/EpF2
# 3r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBHX8mBUHOFECMhWWCK
# ZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q27IwyCQLMbDwMVhEC
# AwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFGg34Ou2
# O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9P
# MA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzB3BggrBgEFBQcB
# AQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggr
# BgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1
# c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwHAYDVR0gBBUwEzAH
# BgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIBADojRD2NCHbuj7w6
# mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6jfCbVN7w6XUhtldU/
# SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmImoqKwba9oUgYftzY
# gBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtfJqGVWEjVGv7XJz/9
# kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrxoj7bQ7gzyE84FJKZ
# 9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3LIU/Gs4m6Ri+kAew
# Q3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx4b6cpwoG1iZnt5Lm
# Tl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9Oj9FpsToFpFSi0HA
# SIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+ICw2/O/TOHnuO77Xr
# y7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug0wcCampAMEhLNKhR
# ILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5Vzu0nAPthkX0tGFu
# v2jiJmCG6sivqf6UHedjGzqGVnhOMIIGwjCCBKqgAwIBAgIQBUSv85SdCDmmv9s/
# X+VhFjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5
# NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIzMDcxNDAwMDAwMFoXDTM0MTAx
# MzIzNTk1OVowSDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu
# MSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMzCCAiIwDQYJKoZIhvcN
# AQEBBQADggIPADCCAgoCggIBAKNTRYcdg45brD5UsyPgz5/X5dLnXaEOCdwvSKOX
# ejsqnGfcYhVYwamTEafNqrJq3RApih5iY2nTWJw1cb86l+uUUI8cIOrHmjsvlmbj
# aedp/lvD1isgHMGXlLSlUIHyz8sHpjBoyoNC2vx/CSSUpIIa2mq62DvKXd4ZGIX7
# ReoNYWyd/nFexAaaPPDFLnkPG2ZS48jWPl/aQ9OE9dDH9kgtXkV1lnX+3RChG4PB
# uOZSlbVH13gpOWvgeFmX40QrStWVzu8IF+qCZE3/I+PKhu60pCFkcOvV5aDaY7Mu
# 6QXuqvYk9R28mxyyt1/f8O52fTGZZUdVnUokL6wrl76f5P17cz4y7lI0+9S769Sg
# LDSb495uZBkHNwGRDxy1Uc2qTGaDiGhiu7xBG3gZbeTZD+BYQfvYsSzhUa+0rRUG
# FOpiCBPTaR58ZE2dD9/O0V6MqqtQFcmzyrzXxDtoRKOlO0L9c33u3Qr/eTQQfqZc
# ClhMAD6FaXXHg2TWdc2PEnZWpST618RrIbroHzSYLzrqawGw9/sqhux7UjipmAmh
# cbJsca8+uG+W1eEQE/5hRwqM/vC2x9XH3mwk8L9CgsqgcT2ckpMEtGlwJw1Pt7U2
# 0clfCKRwo+wK8REuZODLIivK8SgTIUlRfgZm0zu++uuRONhRB8qUt+JQofM604qD
# y0B7AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAW
# BgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglg
# hkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0O
# BBYEFKW27xPn783QZKHVVqllMaPe1eNJMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6
# Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEy
# NTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUF
# BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6
# Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZT
# SEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAIEa1t6g
# qbWYF7xwjU+KPGic2CX/yyzkzepdIpLsjCICqbjPgKjZ5+PF7SaCinEvGN1Ott5s
# 1+FgnCvt7T1IjrhrunxdvcJhN2hJd6PrkKoS1yeF844ektrCQDifXcigLiV4JZ0q
# BXqEKZi2V3mP2yZWK7Dzp703DNiYdk9WuVLCtp04qYHnbUFcjGnRuSvExnvPnPp4
# 4pMadqJpddNQ5EQSviANnqlE0PjlSXcIWiHFtM+YlRpUurm8wWkZus8W8oM3NG6w
# QSbd3lqXTzON1I13fXVFoaVYJmoDRd7ZULVQjK9WvUzF4UbFKNOt50MAcN7MmJ4Z
# iQPq1JE3701S88lgIcRWR+3aEUuMMsOI5ljitts++V+wQtaP4xeR0arAVeOGv6wn
# LEHQmjNKqDbUuXKWfpd5OEhfysLcPTLfddY2Z1qJ+Panx+VPNTwAvb6cKmx5Adza
# ROY63jg7B145WPR8czFVoIARyxQMfq68/qTreWWqaNYiyjvrmoI1VygWy2nyMpqy
# 0tg6uLFGhmu6F/3Ed2wVbK6rr3M66ElGt9V/zLY4wNjsHPW2obhDLN9OTH0eaHDA
# dwrUAuBcYLso/zjlUlrWrBciI0707NMX+1Br/wd3H3GXREHJuEbTbDJ8WC9nR2Xl
# G3O2mflrLAZG70Ee8PBf4NvZrZCARK+AEEGKMIIHXzCCBUegAwIBAgIQB8JSdCgU
# otar/iTqF+XdLjANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UE
# ChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQg
# Q29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMB4XDTIzMDQxNjAw
# MDAwMFoXDTI2MDcwNjIzNTk1OVowZzELMAkGA1UEBhMCUEwxEjAQBgNVBAcMCU1p
# a2/FgsOzdzEhMB8GA1UECgwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMSEwHwYD
# VQQDDBhQcnplbXlzxYJhdyBLxYJ5cyBFVk9URUMwggIiMA0GCSqGSIb3DQEBAQUA
# A4ICDwAwggIKAoICAQCUmgeXMQtIaKaSkKvbAt8GFZJ1ywOH8SwxlTus4McyrWmV
# OrRBVRQA8ApF9FaeobwmkZxvkxQTFLHKm+8knwomEUslca8CqSOI0YwELv5EwTVE
# h0C/Daehvxo6tkmNPF9/SP1KC3c0l1vO+M7vdNVGKQIQrhxq7EG0iezBZOAiukNd
# GVXRYOLn47V3qL5PwG/ou2alJ/vifIDad81qFb+QkUh02Jo24SMjWdKDytdrMXi0
# 235CN4RrW+8gjfRJ+fKKjgMImbuceCsi9Iv1a66bUc9anAemObT4mF5U/yQBgAuA
# o3+jVB8wiUd87kUQO0zJCF8vq2YrVOz8OJmMX8ggIsEEUZ3CZKD0hVc3dm7cWSAw
# 8/FNzGNPlAaIxzXX9qeD0EgaCLRkItA3t3eQW+IAXyS/9ZnnpFUoDvQGbK+Q4/bP
# 0ib98XLfQpxVGRu0cCV0Ng77DIkRF+IyR1PcwVAq+OzVU3vKeo25v/rntiXCmCxi
# W4oHYO28eSQ/eIAcnii+3uKDNZrI15P7VxDrkUIc6FtiSvOhwc3AzY+vEfivUkFK
# RqwvSSr4fCrrkk7z2Qe72Zwlw2EDRVHyy0fUVGO9QMuh6E3RwnJL96ip0alcmhKA
# BGoIqSW05nXdCUbkXmhPCTT5naQDuZ1UkAXbZPShKjbPwzdXP2b8I9nQ89VSgQID
# AQABo4ICAzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYD
# VR0OBBYEFHrxaiVZuDJxxEk15bLoMuFI5233MA4GA1UdDwEB/wQEAwIHgDATBgNV
# HSUEDDAKBggrBgEFBQcDAzCBtQYDVR0fBIGtMIGqMFOgUaBPhk1odHRwOi8vY3Js
# My5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQw
# OTZTSEEzODQyMDIxQ0ExLmNybDBToFGgT4ZNaHR0cDovL2NybDQuZGlnaWNlcnQu
# Y29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAy
# MUNBMS5jcmwwPgYDVR0gBDcwNTAzBgZngQwBBAEwKTAnBggrBgEFBQcCARYbaHR0
# cDovL3d3dy5kaWdpY2VydC5jb20vQ1BTMIGUBggrBgEFBQcBAQSBhzCBhDAkBggr
# BgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBo
# dHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2Rl
# U2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqG
# SIb3DQEBCwUAA4ICAQC3EeHXUPhpe31K2DL43Hfh6qkvBHyR1RlD9lVIklcRCR50
# ZHzoWs6EBlTFyohvkpclVCuRdQW33tS6vtKPOucpDDv4wsA+6zkJYI8fHouW6Tqa
# 1W47YSrc5AOShIcJ9+NpNbKNGih3doSlcio2mUKCX5I/ZrzJBkQpJ0kYha/pUST2
# CbE3JroJf2vQWGUiI+J3LdiPNHmhO1l+zaQkSxv0cVDETMfQGZKKRVESZ6Fg61b0
# djvQSx510MdbxtKMjvS3ZtAytqnQHk1ipP+Rg+M5lFHrSkUlnpGa+f3nuQhxDb7N
# 9E8hUVevxALTrFifg8zhslVRH5/Df/CxlMKXC7op30/AyQsOQxHW1uNx3tG1DMgi
# zpwBasrxh6wa7iaA+Lp07q1I92eLhrYbtw3xC2vNIGdMdN7nd76yMIjdYnAn7r38
# wwtaJ3KYD0QTl77EB8u/5cCs3ShZdDdyg4K7NoJl8iEHrbqtooAHOMLiJpiL2i9Y
# n8kQMB6/Q6RMO3IUPLuycB9o6DNiwQHf6Jt5oW7P09k5NxxBEmksxwNbmZvNQ65Z
# n3exUAKqG+x31Egz5IZ4U/jPzRalElEIpS0rgrVg8R8pEOhd95mEzp5WERKFyXhe
# 6nB6bSYHv8clLAV0iMku308rpfjMiQkqS3LLzfUJ5OHqtKKQNMLxz9z185UCszGC
# BlMwggZPAgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ
# bmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBS
# U0E0MDk2IFNIQTM4NCAyMDIxIENBMQIQB8JSdCgUotar/iTqF+XdLjANBglghkgB
# ZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJ
# AzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8G
# CSqGSIb3DQEJBDEiBCCXMlM14Q2EPxflYVY14JDyK//I58IloVeRZifJh9/AjjAN
# BgkqhkiG9w0BAQEFAASCAgCEJjwduIiEZoAk67gzf2u7v9nXTDNDNCETR2hFnImH
# UPvcJuyXmPq6zf3ZaTQ5e8vP/nE3/dyVOagzPMzeLB7KzaRTaczJl6amc13WJ73G
# jXHLvRHyedWTCf1XML2DKUxUIbuuAp6uiYMuEOggRNYgvtITbFQrPYnBo4zD9mU9
# w0cQuml8vzUsN3Jxcawjo6rjAbp7ZaYh6hPd/Bhp2o6Un16CDjLckHZXX5NZFOD8
# peYGog1SWlbLr8P6v3zhJTTTKclrC5FiiK+quOjK7IJO+mCmcHtlI1CpYlWfehPp
# BbOoFgTqxREvsAa2cJc9eeodB7KHX9OWSuQgmjkjqyDc/iyeG4nM4lW9UrYVwJ/q
# P1K7nijP31rFnaKSv3tyLOunlUad7GnLoKZXznWbcQ6kJetLp72aJYYpbQP50Em7
# 5zlLa4mQ+v9hH78sGeYJoRQ+VeCZvuRELcwzKVegFy6OR+JyN4hwYbM0Ss6fUCiz
# UF4Ed4KIAFLLeOeoR/x4UZySMcjGoBbPwcSlIcg3bzwYjgzd7Jzkx+IdX5ca6vDs
# GwuiN9KX8ctStLhSV19aXweIvzUqgj/lq4M0/c8kJZ6kQIhB0ctpqPgIfbh0LlI+
# O6uX/2A4QFFnNkoIaKbXq5tJffG1XFHpoMVyw68mB44N1dBBWKQzMtyF+wLjeOzG
# OaGCAyAwggMcBgkqhkiG9w0BCQYxggMNMIIDCQIBATB3MGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0ECEAVEr/OUnQg5
# pr/bP1/lYRYwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcN
# AQcBMBwGCSqGSIb3DQEJBTEPFw0yNDA4MjIxNDU0MzNaMC8GCSqGSIb3DQEJBDEi
# BCC0pw2A75kTyX3ljBjC+rUq0oXEEjgtTSkWJ4TWngu2JzANBgkqhkiG9w0BAQEF
# AASCAgBhw18CBrALFXTWl03CufmlwbrCM8UxpSfUAy8F6h7mwmM3YalyrhiSRXpv
# ovcHkbVGyMzgttZBLoJvfapcFWLFpvUeeK2LcVjzTsitLFJUkpI0aJRJc5RGofjT
# Dlb6b9DZYQv5Wq2DxzvRNJZnBTpHkBmN5jalRB72a/Jgfqum7ZCNE2MBMFu20nj7
# 5HVN1hhpc2YU7LWbCzAv0KOXmM/V81ieeTfi1ncyEdG87OH2M8yxDBf2YtH6Y+E2
# BGSgjEOnjOtdoHGVqaCeKSEne1/TAoXYhRf/JnzbVkWBz54kRwwXo67qCnQHyA+7
# Y7rIno3Kgk/XDig64UUmprwow6Z/CfSIDwyiYQXD7kRAvb2dS/0nOOrktDkcOteS
# 3TtSnTzhTRIOHEwcWJFgaEfuE7IU8DPXnElATfwLmrup/ZHL+rnHcihpK0gTJJf6
# 9kV2/pK8GDvtfMrI1PiUPw+3xCGgx+XKg5vUdel+Jd6w/KKUr5NgiKNPzKdobu//
# idONld+EowE4GiXjYuBrkf9zA+jvNwBfln6Gj+6gxVOz64lWkHo/Ymb74Em9riKk
# 17I10Rrhuhf02ZSE0ttGB93kApA57Zo2abFkK4oIsgGHw541S84Pw4awZsgAuh5c
# zSd/rRGAOj+kh8JQaawljRzD3f3RPwO2y9BrIwOY+34CrBXRlA==
# SIG # End signature block