PasswordSolution.psm1
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 ($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 ($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 .PARAMETER ReturnHashTable Return hashtable of countries and their corresponding continent .EXAMPLE Convert-CountryToContinent -Country "Poland" .EXAMPLE Convert-CountryToContinent -ReturnHashTable .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 ($ReturnHashTable) { $CountryToContinent } else { if ($CountryToContinent[$Country]) { $CountryToContinent[$Country] } else { "Unknown" } } } function ConvertFrom-DistinguishedName { <# .SYNOPSIS Converts a Distinguished Name to CN, OU, Multiple OUs or DC .DESCRIPTION Converts a Distinguished Name to CN, OU, Multiple OUs or DC .PARAMETER DistinguishedName Distinguished Name to convert .PARAMETER ToOrganizationalUnit Converts DistinguishedName to Organizational Unit .PARAMETER ToDC Converts DistinguishedName to DC .PARAMETER ToDomainCN Converts DistinguishedName to Domain Canonical Name (CN) .PARAMETER ToCanonicalName Converts DistinguishedName to Canonical Name .EXAMPLE $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName -ToOrganizationalUnit Output: OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz .EXAMPLE $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName Output: Przemyslaw Klys .EXAMPLE ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToMultipleOrganizationalUnit -IncludeParent Output: OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz OU=Production,DC=ad,DC=evotec,DC=xyz .EXAMPLE ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToMultipleOrganizationalUnit Output: OU=Production,DC=ad,DC=evotec,DC=xyz .EXAMPLE $Con = @( 'CN=Windows Authorization Access Group,CN=Builtin,DC=ad,DC=evotec,DC=xyz' 'CN=Mmm,DC=elo,CN=nee,DC=RootDNSServers,CN=MicrosoftDNS,CN=System,DC=ad,DC=evotec,DC=xyz' 'CN=e6d5fd00-385d-4e65-b02d-9da3493ed850,CN=Operations,CN=DomainUpdates,CN=System,DC=ad,DC=evotec,DC=xyz' 'OU=Domain Controllers,DC=ad,DC=evotec,DC=pl' 'OU=Microsoft Exchange Security Groups,DC=ad,DC=evotec,DC=xyz' ) ConvertFrom-DistinguishedName -DistinguishedName $Con -ToLastName Output: Windows Authorization Access Group Mmm e6d5fd00-385d-4e65-b02d-9da3493ed850 Domain Controllers Microsoft Exchange Security Groups .EXAMPLEE ConvertFrom-DistinguishedName -DistinguishedName 'DC=ad,DC=evotec,DC=xyz' -ToCanonicalName ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToCanonicalName ConvertFrom-DistinguishedName -DistinguishedName 'CN=test,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToCanonicalName Output: ad.evotec.xyz ad.evotec.xyz\Production\Users ad.evotec.xyz\Production\Users\test .NOTES General notes #> [CmdletBinding(DefaultParameterSetName = 'Default')] param( [Parameter(ParameterSetName = 'ToOrganizationalUnit')] [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')] [Parameter(ParameterSetName = 'ToDC')] [Parameter(ParameterSetName = 'ToDomainCN')] [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'ToLastName')] [Parameter(ParameterSetName = 'ToCanonicalName')] [alias('Identity', 'DN')][Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)][string[]] $DistinguishedName, [Parameter(ParameterSetName = 'ToOrganizationalUnit')][switch] $ToOrganizationalUnit, [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')][alias('ToMultipleOU')][switch] $ToMultipleOrganizationalUnit, [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')][switch] $IncludeParent, [Parameter(ParameterSetName = 'ToDC')][switch] $ToDC, [Parameter(ParameterSetName = 'ToDomainCN')][switch] $ToDomainCN, [Parameter(ParameterSetName = 'ToLastName')][switch] $ToLastName, [Parameter(ParameterSetName = 'ToCanonicalName')][switch] $ToCanonicalName ) Process { foreach ($Distinguished in $DistinguishedName) { if ($ToDomainCN) { $DN = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1' $CN = $DN -replace ',DC=', '.' -replace "DC=" if ($CN) { $CN } } elseif ($ToOrganizationalUnit) { $Value = [Regex]::Match($Distinguished, '(?=OU=)(.*\n?)(?<=.)').Value if ($Value) { $Value } } elseif ($ToMultipleOrganizationalUnit) { if ($IncludeParent) { $Distinguished } while ($true) { #$dn = $dn -replace '^.+?,(?=CN|OU|DC)' $Distinguished = $Distinguished -replace '^.+?,(?=..=)' if ($Distinguished -match '^DC=') { break } $Distinguished } } elseif ($ToDC) { #return [Regex]::Match($DistinguishedName, '(?=DC=)(.*\n?)(?<=.)').Value # return [Regex]::Match($DistinguishedName, '.*?(DC=.*)').Value $Value = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1' if ($Value) { $Value } #return [Regex]::Match($DistinguishedName, 'CN=.*?(DC=.*)').Groups[1].Value } elseif ($ToLastName) { # Would be best if it worked, but there is too many edge cases so hand splits seems to be the best solution # Feel free to change it back to regex if you know how ;) <# https://stackoverflow.com/questions/51761894/regex-extract-ou-from-distinguished-name $Regex = "^(?:(?<cn>CN=(?<name>.*?)),)?(?<parent>(?:(?<path>(?:CN|OU).*?),)?(?<domain>(?:DC=.*)+))$" $Found = $Distinguished -match $Regex if ($Found) { $Matches.name } #> $NewDN = $Distinguished -split ",DC=" if ($NewDN[0].Contains(",OU=")) { [Array] $ChangedDN = $NewDN[0] -split ",OU=" } elseif ($NewDN[0].Contains(",CN=")) { [Array] $ChangedDN = $NewDN[0] -split ",CN=" } else { [Array] $ChangedDN = $NewDN[0] } if ($ChangedDN[0].StartsWith('CN=')) { $ChangedDN[0] -replace 'CN=', '' } else { $ChangedDN[0] -replace 'OU=', '' } } elseif ($ToCanonicalName) { $Domain = $null $Rest = $null foreach ($O in $Distinguished -split '(?<!\\),') { if ($O -match '^DC=') { $Domain += $O.Substring(3) + '.' } else { $Rest = $O.Substring(3) + '\' + $Rest } } if ($Domain -and $Rest) { $Domain.Trim('.') + '\' + ($Rest.TrimEnd('\') -replace '\\,', ',') } elseif ($Domain) { $Domain.Trim('.') } elseif ($Rest) { $Rest.TrimEnd('\') -replace '\\,', ',' } } else { $Regex = '^CN=(?<cn>.+?)(?<!\\),(?<ou>(?:(?:OU|CN).+?(?<!\\),)+(?<dc>DC.+?))$' #$Output = foreach ($_ in $Distinguished) { $Found = $Distinguished -match $Regex if ($Found) { $Matches.cn } #} #$Output.cn } } } } function Convert-UserAccountControl { [cmdletBinding()] param( [alias('UAC')][int] $UserAccountControl, [string] $Separator ) $UserAccount = [ordered] @{ "SCRIPT" = 1 "ACCOUNTDISABLE" = 2 "HOMEDIR_REQUIRED" = 8 "LOCKOUT" = 16 "PASSWD_NOTREQD" = 32 "ENCRYPTED_TEXT_PWD_ALLOWED" = 128 "TEMP_DUPLICATE_ACCOUNT" = 256 "NORMAL_ACCOUNT" = 512 "INTERDOMAIN_TRUST_ACCOUNT" = 2048 "WORKSTATION_TRUST_ACCOUNT" = 4096 "SERVER_TRUST_ACCOUNT" = 8192 "DONT_EXPIRE_PASSWORD" = 65536 "MNS_LOGON_ACCOUNT" = 131072 "SMARTCARD_REQUIRED" = 262144 "TRUSTED_FOR_DELEGATION" = 524288 "NOT_DELEGATED" = 1048576 "USE_DES_KEY_ONLY" = 2097152 "DONT_REQ_PREAUTH" = 4194304 "PASSWORD_EXPIRED" = 8388608 "TRUSTED_TO_AUTH_FOR_DELEGATION" = 16777216 "PARTIAL_SECRETS_ACCOUNT" = 67108864 } $Output = foreach ($_ in $UserAccount.Keys) { $binaryAnd = $UserAccount[$_] -band $UserAccountControl if ($binaryAnd -ne "0") { $_ } } if ($Separator) { $Output -join $Separator } else { $Output } } function Get-GitHubVersion { <# .SYNOPSIS Get the latest version of a GitHub repository and compare with local version .DESCRIPTION Get the latest version of a GitHub repository and compare with local version .PARAMETER Cmdlet Cmdlet to find module for .PARAMETER RepositoryOwner Repository owner .PARAMETER RepositoryName Repository name .EXAMPLE Get-GitHubVersion -Cmdlet 'Start-DelegationModel' -RepositoryOwner 'evotecit' -RepositoryName 'DelegationModel' .NOTES General notes #> [cmdletBinding()] param( [Parameter(Mandatory)][string] $Cmdlet, [Parameter(Mandatory)][string] $RepositoryOwner, [Parameter(Mandatory)][string] $RepositoryName ) $App = Get-Command -Name $Cmdlet -ErrorAction SilentlyContinue if ($App) { [Array] $GitHubReleases = (Get-GitHubLatestRelease -Url "https://api.github.com/repos/$RepositoryOwner/$RepositoryName/releases" -Verbose:$false) $LatestVersion = $GitHubReleases[0] if (-not $LatestVersion.Errors) { if ($App.Version -eq $LatestVersion.Version) { "Current/Latest: $($LatestVersion.Version) at $($LatestVersion.PublishDate)" } elseif ($App.Version -lt $LatestVersion.Version) { "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Update?" } elseif ($App.Version -gt $LatestVersion.Version) { "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Lucky you!" } } else { "Current: $($App.Version)" } } } function Get-WinADForestDetails { [CmdletBinding()] param( [alias('ForestName')][string] $Forest, [string[]] $ExcludeDomains, [string[]] $ExcludeDomainControllers, [alias('Domain', 'Domains')][string[]] $IncludeDomains, [alias('DomainControllers', 'ComputerName')][string[]] $IncludeDomainControllers, [switch] $SkipRODC, [string] $Filter = '*', [switch] $TestAvailability, [ValidateSet('All', 'Ping', 'WinRM', 'PortOpen', 'Ping+WinRM', 'Ping+PortOpen', 'WinRM+PortOpen')] $Test = 'All', [int[]] $Ports = 135, [int] $PortsTimeout = 100, [int] $PingCount = 1, [switch] $PreferWritable, [switch] $Extended, [System.Collections.IDictionary] $ExtendedForestInformation ) if ($Global:ProgressPreference -ne 'SilentlyContinue') { $TemporaryProgress = $Global:ProgressPreference $Global:ProgressPreference = 'SilentlyContinue' } if (-not $ExtendedForestInformation) { # standard situation, building data from AD $Findings = [ordered] @{ } try { if ($Forest) { $ForestInformation = Get-ADForest -ErrorAction Stop -Identity $Forest } else { $ForestInformation = Get-ADForest -ErrorAction Stop } <# $ForestInformation = [ordered] @{ ApplicationPartitions = $ForestInf.ApplicationPartitions | ForEach-Object -Process { $_ } # : {DC=DomainDnsZones,DC=ad,DC=evotec,DC=xyz, DC=DomainDnsZones,DC=ad,DC=evotec,DC=pl, DC=ForestDnsZones,DC=ad,DC=evotec,DC=xyz} CrossForestReferences = $ForestInf.CrossForestReferences | ForEach-Object -Process { $_ } # : {} DomainNamingMaster = $ForestInf.DomainNamingMaster # : AD1.ad.evotec.xyz Domains = $ForestInf.Domains | ForEach-Object -Process { $_ } # : {ad.evotec.xyz, ad.evotec.pl} ForestMode = $ForestInf.ForestMode # : Windows2012R2Forest GlobalCatalogs = $ForestInf.GlobalCatalogs | ForEach-Object -Process { $_ } # : {AD1.ad.evotec.xyz, AD2.ad.evotec.xyz, ADRODC.ad.evotec.pl, AD3.ad.evotec.xyz...} Name = $ForestInf.Name # : ad.evotec.xyz PartitionsContainer = $ForestInf.PartitionsContainer # : CN=Partitions,CN=Configuration,DC=ad,DC=evotec,DC=xyz RootDomain = $ForestInf.RootDomain # : ad.evotec.xyz SchemaMaster = $ForestInf.SchemaMaster # : AD1.ad.evotec.xyz Sites = $ForestInf.Sites | ForEach-Object -Process { $_ } # : {KATOWICE-1, KATOWICE-2} SPNSuffixes = $ForestInf.SPNSuffixes | ForEach-Object -Process { $_ } # : {} UPNSuffixes = $ForestInf.UPNSuffixes | ForEach-Object -Process { $_ } # : {myneva.eu, single.evotec.xyz, newUPN@com, evotec.xyz...} } #> } catch { Write-Warning "Get-WinADForestDetails - Error discovering DC for Forest - $($_.Exception.Message)" return } if (-not $ForestInformation) { return } $Findings['Forest'] = $ForestInformation $Findings['ForestDomainControllers'] = @() $Findings['QueryServers'] = @{ } $Findings['DomainDomainControllers'] = @{ } [Array] $Findings['Domains'] = foreach ($Domain in $ForestInformation.Domains) { if ($IncludeDomains) { if ($Domain -in $IncludeDomains) { $Domain.ToLower() } # We skip checking for exclusions continue } if ($Domain -notin $ExcludeDomains) { $Domain.ToLower() } } # We want to have QueryServers always available for all domains [Array] $DomainsActive = foreach ($Domain in $Findings['Forest'].Domains) { try { $DC = Get-ADDomainController -DomainName $Domain -Discover -ErrorAction Stop -Writable:$PreferWritable.IsPresent $OrderedDC = [ordered] @{ Domain = $DC.Domain Forest = $DC.Forest HostName = [Array] $DC.HostName IPv4Address = $DC.IPv4Address IPv6Address = $DC.IPv6Address Name = $DC.Name Site = $DC.Site } } catch { Write-Warning "Get-WinADForestDetails - Error discovering DC for domain $Domain - $($_.Exception.Message)" continue } if ($Domain -eq $Findings['Forest']['Name']) { $Findings['QueryServers']['Forest'] = $OrderedDC } $Findings['QueryServers']["$Domain"] = $OrderedDC # lets return domain as something that wroks $Domain } # we need to make sure to remove domains that don't have DCs for some reason [Array] $Findings['Domains'] = foreach ($Domain in $Findings['Domains']) { if ($Domain -notin $DomainsActive) { Write-Warning "Get-WinADForestDetails - Domain $Domain doesn't seem to be active (no DCs). Skipping." continue } $Domain } [Array] $Findings['ForestDomainControllers'] = foreach ($Domain in $Findings.Domains) { $QueryServer = $Findings['QueryServers'][$Domain]['HostName'][0] [Array] $AllDC = try { try { $DomainControllers = Get-ADDomainController -Filter $Filter -Server $QueryServer -ErrorAction Stop } catch { Write-Warning "Get-WinADForestDetails - Error listing DCs for domain $Domain - $($_.Exception.Message)" continue } foreach ($S in $DomainControllers) { if ($IncludeDomainControllers.Count -gt 0) { If (-not $IncludeDomainControllers[0].Contains('.')) { if ($S.Name -notin $IncludeDomainControllers) { continue } } else { if ($S.HostName -notin $IncludeDomainControllers) { continue } } } if ($ExcludeDomainControllers.Count -gt 0) { If (-not $ExcludeDomainControllers[0].Contains('.')) { if ($S.Name -in $ExcludeDomainControllers) { continue } } else { if ($S.HostName -in $ExcludeDomainControllers) { continue } } } $Server = [ordered] @{ Domain = $Domain HostName = $S.HostName Name = $S.Name Forest = $ForestInformation.RootDomain Site = $S.Site IPV4Address = $S.IPV4Address IPV6Address = $S.IPV6Address IsGlobalCatalog = $S.IsGlobalCatalog IsReadOnly = $S.IsReadOnly IsSchemaMaster = ($S.OperationMasterRoles -contains 'SchemaMaster') IsDomainNamingMaster = ($S.OperationMasterRoles -contains 'DomainNamingMaster') IsPDC = ($S.OperationMasterRoles -contains 'PDCEmulator') IsRIDMaster = ($S.OperationMasterRoles -contains 'RIDMaster') IsInfrastructureMaster = ($S.OperationMasterRoles -contains 'InfrastructureMaster') OperatingSystem = $S.OperatingSystem OperatingSystemVersion = $S.OperatingSystemVersion OperatingSystemLong = ConvertTo-OperatingSystem -OperatingSystem $S.OperatingSystem -OperatingSystemVersion $S.OperatingSystemVersion LdapPort = $S.LdapPort SslPort = $S.SslPort DistinguishedName = $S.ComputerObjectDN Pingable = $null WinRM = $null PortOpen = $null Comment = '' } if ($TestAvailability) { if ($Test -eq 'All' -or $Test -like 'Ping*') { $Server.Pingable = Test-Connection -ComputerName $Server.IPV4Address -Quiet -Count $PingCount } if ($Test -eq 'All' -or $Test -like '*WinRM*') { $Server.WinRM = (Test-WinRM -ComputerName $Server.HostName).Status } if ($Test -eq 'All' -or '*PortOpen*') { $Server.PortOpen = (Test-ComputerPort -Server $Server.HostName -PortTCP $Ports -Timeout $PortsTimeout).Status } } [PSCustomObject] $Server } } catch { [PSCustomObject]@{ Domain = $Domain HostName = '' Name = '' Forest = $ForestInformation.RootDomain IPV4Address = '' IPV6Address = '' IsGlobalCatalog = '' IsReadOnly = '' Site = '' SchemaMaster = $false DomainNamingMasterMaster = $false PDCEmulator = $false RIDMaster = $false InfrastructureMaster = $false LdapPort = '' SslPort = '' DistinguishedName = '' Pingable = $null WinRM = $null PortOpen = $null Comment = $_.Exception.Message -replace "`n", " " -replace "`r", " " } } if ($SkipRODC) { [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC | Where-Object { $_.IsReadOnly -eq $false } #$Findings[$Domain] = $AllDC | Where-Object { $_.IsReadOnly -eq $false } } else { [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC #$Findings[$Domain] = $AllDC } # Building all DCs for whole Forest [Array] $Findings['DomainDomainControllers'][$Domain] } if ($Extended) { $Findings['DomainsExtended'] = @{ } $Findings['DomainsExtendedNetBIOS'] = @{ } foreach ($DomainEx in $Findings['Domains']) { try { #$Findings['DomainsExtended'][$DomainEx] = Get-ADDomain -Server $Findings['QueryServers'][$DomainEx].HostName[0] $Findings['DomainsExtended'][$DomainEx] = Get-ADDomain -Server $Findings['QueryServers'][$DomainEx].HostName[0] | ForEach-Object { # We need to use ForEach-Object to convert ADPropertyValueCollection to normal strings. Otherwise Copy-Dictionary fails #True False ADPropertyValueCollection System.Collections.CollectionBase [ordered] @{ AllowedDNSSuffixes = $_.AllowedDNSSuffixes | ForEach-Object -Process { $_ } #: { } ChildDomains = $_.ChildDomains | ForEach-Object -Process { $_ } #: { } ComputersContainer = $_.ComputersContainer #: CN = Computers, DC = ad, DC = evotec, DC = xyz DeletedObjectsContainer = $_.DeletedObjectsContainer #: CN = Deleted Objects, DC = ad, DC = evotec, DC = xyz DistinguishedName = $_.DistinguishedName #: DC = ad, DC = evotec, DC = xyz DNSRoot = $_.DNSRoot #: ad.evotec.xyz DomainControllersContainer = $_.DomainControllersContainer #: OU = Domain Controllers, DC = ad, DC = evotec, DC = xyz DomainMode = $_.DomainMode #: Windows2012R2Domain DomainSID = $_.DomainSID.Value #: S - 1 - 5 - 21 - 853615985 - 2870445339 - 3163598659 ForeignSecurityPrincipalsContainer = $_.ForeignSecurityPrincipalsContainer #: CN = ForeignSecurityPrincipals, DC = ad, DC = evotec, DC = xyz Forest = $_.Forest #: ad.evotec.xyz InfrastructureMaster = $_.InfrastructureMaster #: AD1.ad.evotec.xyz LastLogonReplicationInterval = $_.LastLogonReplicationInterval #: LinkedGroupPolicyObjects = $_.LinkedGroupPolicyObjects | ForEach-Object -Process { $_ } #: LostAndFoundContainer = $_.LostAndFoundContainer #: CN = LostAndFound, DC = ad, DC = evotec, DC = xyz ManagedBy = $_.ManagedBy #: Name = $_.Name #: ad NetBIOSName = $_.NetBIOSName #: EVOTEC ObjectClass = $_.ObjectClass #: domainDNS ObjectGUID = $_.ObjectGUID #: bc875580 - 4c70-41ad-a487-c57337e26024 ParentDomain = $_.ParentDomain #: PDCEmulator = $_.PDCEmulator #: AD1.ad.evotec.xyz PublicKeyRequiredPasswordRolling = $_.PublicKeyRequiredPasswordRolling | ForEach-Object -Process { $_ } #: QuotasContainer = $_.QuotasContainer #: CN = NTDS Quotas, DC = ad, DC = evotec, DC = xyz ReadOnlyReplicaDirectoryServers = $_.ReadOnlyReplicaDirectoryServers | ForEach-Object -Process { $_ } #: { } ReplicaDirectoryServers = $_.ReplicaDirectoryServers | ForEach-Object -Process { $_ } #: { AD1.ad.evotec.xyz, AD2.ad.evotec.xyz, AD3.ad.evotec.xyz } RIDMaster = $_.RIDMaster #: AD1.ad.evotec.xyz SubordinateReferences = $_.SubordinateReferences | ForEach-Object -Process { $_ } #: { DC = ForestDnsZones, DC = ad, DC = evotec, DC = xyz, DC = DomainDnsZones, DC = ad, DC = evotec, DC = xyz, CN = Configuration, DC = ad, DC = evotec, DC = xyz } SystemsContainer = $_.SystemsContainer #: CN = System, DC = ad, DC = evotec, DC = xyz UsersContainer = $_.UsersContainer #: CN = Users, DC = ad, DC = evotec, DC = xyz } } $NetBios = $Findings['DomainsExtended'][$DomainEx]['NetBIOSName'] $Findings['DomainsExtendedNetBIOS'][$NetBios] = $Findings['DomainsExtended'][$DomainEx] } catch { Write-Warning "Get-WinADForestDetails - Error gathering Domain Information for domain $DomainEx - $($_.Exception.Message)" continue } } } # Bring back setting as per default if ($TemporaryProgress) { $Global:ProgressPreference = $TemporaryProgress } $Findings } else { # this takes care of limiting output to only what we requested, but based on prior input # this makes sure we ask once for all AD stuff and then subsequent calls just filter out things # this should be much faster then asking again and again for stuff from AD $Findings = Copy-DictionaryManual -Dictionary $ExtendedForestInformation [Array] $Findings['Domains'] = foreach ($_ in $Findings.Domains) { if ($IncludeDomains) { if ($_ -in $IncludeDomains) { $_.ToLower() } # We skip checking for exclusions continue } if ($_ -notin $ExcludeDomains) { $_.ToLower() } } # Now that we have Domains we need to remove all DCs that are not from domains we excluded or included foreach ($_ in [string[]] $Findings.DomainDomainControllers.Keys) { if ($_ -notin $Findings.Domains) { $Findings.DomainDomainControllers.Remove($_) } } # Same as above but for query servers - we don't remove queried servers #foreach ($_ in [string[]] $Findings.QueryServers.Keys) { # if ($_ -notin $Findings.Domains -and $_ -ne 'Forest') { # $Findings.QueryServers.Remove($_) # } #} # Now that we have Domains we need to remove all Domains that are excluded or included foreach ($_ in [string[]] $Findings.DomainsExtended.Keys) { if ($_ -notin $Findings.Domains) { $Findings.DomainsExtended.Remove($_) $NetBiosName = $Findings.DomainsExtended.$_.'NetBIOSName' if ($NetBiosName) { $Findings.DomainsExtendedNetBIOS.Remove($NetBiosName) } } } [Array] $Findings['ForestDomainControllers'] = foreach ($Domain in $Findings.Domains) { [Array] $AllDC = foreach ($S in $Findings.DomainDomainControllers["$Domain"]) { if ($IncludeDomainControllers.Count -gt 0) { If (-not $IncludeDomainControllers[0].Contains('.')) { if ($S.Name -notin $IncludeDomainControllers) { continue } } else { if ($S.HostName -notin $IncludeDomainControllers) { continue } } } if ($ExcludeDomainControllers.Count -gt 0) { If (-not $ExcludeDomainControllers[0].Contains('.')) { if ($S.Name -in $ExcludeDomainControllers) { continue } } else { if ($S.HostName -in $ExcludeDomainControllers) { continue } } } $S } if ($SkipRODC) { [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC | Where-Object { $_.IsReadOnly -eq $false } } else { [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC } # Building all DCs for whole Forest [Array] $Findings['DomainDomainControllers'][$Domain] } $Findings } } function Remove-EmptyValue { [alias('Remove-EmptyValues')] [CmdletBinding()] param( [alias('Splat', 'IDictionary')][Parameter(Mandatory)][System.Collections.IDictionary] $Hashtable, [string[]] $ExcludeParameter, [switch] $Recursive, [int] $Rerun, [switch] $DoNotRemoveNull, [switch] $DoNotRemoveEmpty, [switch] $DoNotRemoveEmptyArray, [switch] $DoNotRemoveEmptyDictionary ) foreach ($Key in [string[]] $Hashtable.Keys) { if ($Key -notin $ExcludeParameter) { if ($Recursive) { if ($Hashtable[$Key] -is [System.Collections.IDictionary]) { if ($Hashtable[$Key].Count -eq 0) { if (-not $DoNotRemoveEmptyDictionary) { $Hashtable.Remove($Key) } } else { Remove-EmptyValue -Hashtable $Hashtable[$Key] -Recursive:$Recursive } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } } if ($Rerun) { for ($i = 0; $i -lt $Rerun; $i++) { Remove-EmptyValue -Hashtable $Hashtable -Recursive:$Recursive } } } function Start-TimeLog { [CmdletBinding()] param() [System.Diagnostics.Stopwatch]::StartNew() } function Stop-TimeLog { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)][System.Diagnostics.Stopwatch] $Time, [ValidateSet('OneLiner', 'Array')][string] $Option = 'OneLiner', [switch] $Continue ) Begin {} Process { if ($Option -eq 'Array') { $TimeToExecute = "$($Time.Elapsed.Days) days", "$($Time.Elapsed.Hours) hours", "$($Time.Elapsed.Minutes) minutes", "$($Time.Elapsed.Seconds) seconds", "$($Time.Elapsed.Milliseconds) milliseconds" } else { $TimeToExecute = "$($Time.Elapsed.Days) days, $($Time.Elapsed.Hours) hours, $($Time.Elapsed.Minutes) minutes, $($Time.Elapsed.Seconds) seconds, $($Time.Elapsed.Milliseconds) milliseconds" } } End { if (-not $Continue) { $Time.Stop() } return $TimeToExecute } } function Write-Color { <# .SYNOPSIS Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options. .DESCRIPTION Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options. It provides: - Easy manipulation of colors, - Logging output to file (log) - Nice formatting options out of the box. - Ability to use aliases for parameters .PARAMETER Text Text to display on screen and write to log file if specified. Accepts an array of strings. .PARAMETER Color Color of the text. Accepts an array of colors. If more than one color is specified it will loop through colors for each string. If there are more strings than colors it will start from the beginning. Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White .PARAMETER BackGroundColor Color of the background. Accepts an array of colors. If more than one color is specified it will loop through colors for each string. If there are more strings than colors it will start from the beginning. Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White .PARAMETER StartTab Number of tabs to add before text. Default is 0. .PARAMETER LinesBefore Number of empty lines before text. Default is 0. .PARAMETER LinesAfter Number of empty lines after text. Default is 0. .PARAMETER StartSpaces Number of spaces to add before text. Default is 0. .PARAMETER LogFile Path to log file. If not specified no log file will be created. .PARAMETER DateTimeFormat Custom date and time format string. Default is yyyy-MM-dd HH:mm:ss .PARAMETER LogTime If set to $true it will add time to log file. Default is $true. .PARAMETER LogRetry Number of retries to write to log file, in case it can't write to it for some reason, before skipping. Default is 2. .PARAMETER Encoding Encoding of the log file. Default is Unicode. .PARAMETER ShowTime Switch to add time to console output. Default is not set. .PARAMETER NoNewLine Switch to not add new line at the end of the output. Default is not set. .PARAMETER NoConsoleOutput Switch to not output to console. Default all output goes to console. .EXAMPLE Write-Color -Text "Red ", "Green ", "Yellow " -Color Red,Green,Yellow .EXAMPLE Write-Color -Text "This is text in Green ", "followed by red ", "and then we have Magenta... ", "isn't it fun? ", "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan .EXAMPLE Write-Color -Text "This is text in Green ", "followed by red ", "and then we have Magenta... ", "isn't it fun? ", "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan -StartTab 3 -LinesBefore 1 -LinesAfter 1 .EXAMPLE Write-Color "1. ", "Option 1" -Color Yellow, Green Write-Color "2. ", "Option 2" -Color Yellow, Green Write-Color "3. ", "Option 3" -Color Yellow, Green Write-Color "4. ", "Option 4" -Color Yellow, Green Write-Color "9. ", "Press 9 to exit" -Color Yellow, Gray -LinesBefore 1 .EXAMPLE Write-Color -LinesBefore 2 -Text "This little ","message is ", "written to log ", "file as well." ` -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" -TimeFormat "yyyy-MM-dd HH:mm:ss" Write-Color -Text "This can get ","handy if ", "want to display things, and log actions to file ", "at the same time." ` -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" .EXAMPLE Write-Color -T "My text", " is ", "all colorful" -C Yellow, Red, Green -B Green, Green, Yellow Write-Color -t "my text" -c yellow -b green Write-Color -text "my text" -c red .EXAMPLE Write-Color -Text "TestujÄâ„¢ czy siÄâ„¢ Å‚adnie zapisze, czy bÄâ„¢dÄ… problemy" -Encoding unicode -LogFile 'C:\temp\testinggg.txt' -Color Red -NoConsoleOutput .NOTES Understanding Custom date and time format strings: https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings Project support: https://github.com/EvotecIT/PSWriteColor Original idea: Josh (https://stackoverflow.com/users/81769/josh) #> [alias('Write-Colour')] [CmdletBinding()] param ( [alias ('T')] [String[]]$Text, [alias ('C', 'ForegroundColor', 'FGC')] [ConsoleColor[]]$Color = [ConsoleColor]::White, [alias ('B', 'BGC')] [ConsoleColor[]]$BackGroundColor = $null, [alias ('Indent')][int] $StartTab = 0, [int] $LinesBefore = 0, [int] $LinesAfter = 0, [int] $StartSpaces = 0, [alias ('L')] [string] $LogFile = '', [Alias('DateFormat', 'TimeFormat')][string] $DateTimeFormat = 'yyyy-MM-dd HH:mm:ss', [alias ('LogTimeStamp')][bool] $LogTime = $true, [int] $LogRetry = 2, [ValidateSet('unknown', 'string', 'unicode', 'bigendianunicode', 'utf8', 'utf7', 'utf32', 'ascii', 'default', 'oem')][string]$Encoding = 'Unicode', [switch] $ShowTime, [switch] $NoNewLine, [alias('HideConsole')][switch] $NoConsoleOutput ) if (-not $NoConsoleOutput) { $DefaultColor = $Color[0] if ($null -ne $BackGroundColor -and $BackGroundColor.Count -ne $Color.Count) { Write-Error "Colors, BackGroundColors parameters count doesn't match. Terminated." return } if ($LinesBefore -ne 0) { for ($i = 0; $i -lt $LinesBefore; $i++) { Write-Host -Object "`n" -NoNewline } } # Add empty line before if ($StartTab -ne 0) { for ($i = 0; $i -lt $StartTab; $i++) { Write-Host -Object "`t" -NoNewline } } # Add TABS before text if ($StartSpaces -ne 0) { for ($i = 0; $i -lt $StartSpaces; $i++) { Write-Host -Object ' ' -NoNewline } } # Add SPACES before text if ($ShowTime) { Write-Host -Object "[$([datetime]::Now.ToString($DateTimeFormat))] " -NoNewline } # Add Time before output if ($Text.Count -ne 0) { if ($Color.Count -ge $Text.Count) { # the real deal coloring if ($null -eq $BackGroundColor) { for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline } } else { for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline } } } else { if ($null -eq $BackGroundColor) { for ($i = 0; $i -lt $Color.Length ; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline } for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -NoNewline } } else { for ($i = 0; $i -lt $Color.Length ; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline } for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -BackgroundColor $BackGroundColor[0] -NoNewline } } } } if ($NoNewLine -eq $true) { Write-Host -NoNewline } else { Write-Host } # Support for no new line if ($LinesAfter -ne 0) { for ($i = 0; $i -lt $LinesAfter; $i++) { Write-Host -Object "`n" -NoNewline } } # Add empty line after } if ($Text.Count -and $LogFile) { # Save to file $TextToFile = "" for ($i = 0; $i -lt $Text.Length; $i++) { $TextToFile += $Text[$i] } $Saved = $false $Retry = 0 Do { $Retry++ try { if ($LogTime) { "[$([datetime]::Now.ToString($DateTimeFormat))] $TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false } else { "$TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false } $Saved = $true } catch { if ($Saved -eq $false -and $Retry -eq $LogRetry) { Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Tried ($Retry/$LogRetry))" } else { Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Retrying... ($Retry/$LogRetry)" } } } Until ($Saved -eq $true -or $Retry -ge $LogRetry) } } function ConvertTo-OperatingSystem { <# .SYNOPSIS Allows easy conversion of OperatingSystem, Operating System Version to proper Windows 10 naming based on WMI or AD .DESCRIPTION Allows easy conversion of OperatingSystem, Operating System Version to proper Windows 10 naming based on WMI or AD .PARAMETER OperatingSystem Operating System as returned by Active Directory .PARAMETER OperatingSystemVersion Operating System Version as returned by Active Directory .EXAMPLE $Computers = Get-ADComputer -Filter * -Properties OperatingSystem, OperatingSystemVersion | ForEach-Object { $OPS = ConvertTo-OperatingSystem -OperatingSystem $_.OperatingSystem -OperatingSystemVersion $_.OperatingSystemVersion Add-Member -MemberType NoteProperty -Name 'OperatingSystemTranslated' -Value $OPS -InputObject $_ -Force $_ } $Computers | Select-Object DNS*, Name, SamAccountName, Enabled, OperatingSystem*, DistinguishedName | Format-Table .EXAMPLE $Registry = Get-PSRegistry -ComputerName 'AD1' -RegistryPath 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion' ConvertTo-OperatingSystem -OperatingSystem $Registry.ProductName -OperatingSystemVersion $Registry.CurrentBuildNumber .NOTES General notes #> [CmdletBinding()] param( [string] $OperatingSystem, [string] $OperatingSystemVersion ) if ($OperatingSystem -like 'Windows 10*' -or $OperatingSystem -like 'Windows 11*') { $Systems = @{ # This is how it's written in AD '10.0 (22000)' = 'Windows 11 21H2' '10.0 (19043)' = 'Windows 10 21H1' '10.0 (19042)' = 'Windows 10 20H2' '10.0 (19041)' = 'Windows 10 2004' '10.0 (18898)' = 'Windows 10 Insider Preview' '10.0 (18363)' = "Windows 10 1909" '10.0 (18362)' = "Windows 10 1903" '10.0 (17763)' = "Windows 10 1809" '10.0 (17134)' = "Windows 10 1803" '10.0 (16299)' = "Windows 10 1709" '10.0 (15063)' = "Windows 10 1703" '10.0 (14393)' = "Windows 10 1607" '10.0 (10586)' = "Windows 10 1511" '10.0 (10240)' = "Windows 10 1507" # This is how WMI/CIM stores it '10.0.22000' = 'Windows 11 21H2' '10.0.19043' = 'Windows 10 21H1' '10.0.19042' = 'Windows 10 20H2' '10.0.19041' = 'Windows 10 2004' '10.0.18898' = 'Windows 10 Insider Preview' '10.0.18363' = "Windows 10 1909" '10.0.18362' = "Windows 10 1903" '10.0.17763' = "Windows 10 1809" '10.0.17134' = "Windows 10 1803" '10.0.16299' = "Windows 10 1709" '10.0.15063' = "Windows 10 1703" '10.0.14393' = "Windows 10 1607" '10.0.10586' = "Windows 10 1511" '10.0.10240' = "Windows 10 1507" # This is how it's written in registry '22000' = 'Windows 11 21H2' '19043' = 'Windows 10 21H1' '19042' = 'Windows 10 20H2' '19041' = 'Windows 10 2004' '18898' = 'Windows 10 Insider Preview' '18363' = "Windows 10 1909" '18362' = "Windows 10 1903" '17763' = "Windows 10 1809" '17134' = "Windows 10 1803" '16299' = "Windows 10 1709" '15063' = "Windows 10 1703" '14393' = "Windows 10 1607" '10586' = "Windows 10 1511" '10240' = "Windows 10 1507" } $System = $Systems[$OperatingSystemVersion] if (-not $System) { $System = $OperatingSystem } } elseif ($OperatingSystem -like 'Windows Server*') { # May need updates https://docs.microsoft.com/en-us/windows-server/get-started/windows-server-release-info # to detect Core $Systems = @{ # This is how it's written in AD '10.0 (20348)' = 'Windows Server 2022' '10.0 (19042)' = 'Windows Server 2019 20H2' '10.0 (19041)' = 'Windows Server 2019 2004' '10.0 (18363)' = 'Windows Server 2019 1909' '10.0 (18362)' = "Windows Server 2019 1903" # (Datacenter Core, Standard Core) '10.0 (17763)' = "Windows Server 2019 1809" # (Datacenter, Essentials, Standard) '10.0 (17134)' = "Windows Server 2016 1803" # (Datacenter, Standard) '10.0 (14393)' = "Windows Server 2016 1607" '6.3 (9600)' = 'Windows Server 2012 R2' '6.1 (7601)' = 'Windows Server 2008 R2' '5.2 (3790)' = 'Windows Server 2003' # This is how WMI/CIM stores it '10.0.20348' = 'Windows Server 2022' '10.0.19042' = 'Windows Server 2019 20H2' '10.0.19041' = 'Windows Server 2019 2004' '10.0.18363' = 'Windows Server 2019 1909' '10.0.18362' = "Windows Server 2019 1903" # (Datacenter Core, Standard Core) '10.0.17763' = "Windows Server 2019 1809" # (Datacenter, Essentials, Standard) '10.0.17134' = "Windows Server 2016 1803" ## (Datacenter, Standard) '10.0.14393' = "Windows Server 2016 1607" '6.3.9600' = 'Windows Server 2012 R2' '6.1.7601' = 'Windows Server 2008 R2' # i think '5.2.3790' = 'Windows Server 2003' # i think # This is how it's written in registry '20348' = 'Windows Server 2022' '19042' = 'Windows Server 2019 20H2' '19041' = 'Windows Server 2019 2004' '18363' = 'Windows Server 2019 1909' '18362' = "Windows Server 2019 1903" # (Datacenter Core, Standard Core) '17763' = "Windows Server 2019 1809" # (Datacenter, Essentials, Standard) '17134' = "Windows Server 2016 1803" # (Datacenter, Standard) '14393' = "Windows Server 2016 1607" '9600' = 'Windows Server 2012 R2' '7601' = 'Windows Server 2008 R2' '3790' = 'Windows Server 2003' } $System = $Systems[$OperatingSystemVersion] if (-not $System) { $System = $OperatingSystem } } else { $System = $OperatingSystem } if ($System) { $System } else { 'Unknown' } } function Copy-DictionaryManual { [CmdletBinding()] param( [System.Collections.IDictionary] $Dictionary ) $clone = @{} foreach ($Key in $Dictionary.Keys) { $value = $Dictionary.$Key $clonedValue = switch ($Dictionary.$Key) { { $null -eq $_ } { $null continue } { $_ -is [System.Collections.IDictionary] } { Copy-DictionaryManual -Dictionary $_ continue } { $type = $_.GetType() $type.IsPrimitive -or $type.IsValueType -or $_ -is [string] } { $_ continue } default { $_ | Select-Object -Property * } } if ($value -is [System.Collections.IList]) { $clone[$Key] = @($clonedValue) } else { $clone[$Key] = $clonedValue } } $clone } function Get-GitHubLatestRelease { <# .SYNOPSIS Gets one or more releases from GitHub repository .DESCRIPTION Gets one or more releases from GitHub repository .PARAMETER Url Url to github repository .EXAMPLE Get-GitHubLatestRelease -Url "https://api.github.com1/repos/evotecit/Testimo/releases" | Format-Table .NOTES General notes #> [CmdLetBinding()] param( [parameter(Mandatory)][alias('ReleasesUrl')][uri] $Url ) $ProgressPreference = 'SilentlyContinue' $Responds = Test-Connection -ComputerName $URl.Host -Quiet -Count 1 if ($Responds) { Try { [Array] $JsonOutput = (Invoke-WebRequest -Uri $Url -ErrorAction Stop | ConvertFrom-Json) foreach ($JsonContent in $JsonOutput) { [PSCustomObject] @{ PublishDate = [DateTime] $JsonContent.published_at CreatedDate = [DateTime] $JsonContent.created_at PreRelease = [bool] $JsonContent.prerelease Version = [version] ($JsonContent.name -replace 'v', '') Tag = $JsonContent.tag_name Branch = $JsonContent.target_commitish Errors = '' } } } catch { [PSCustomObject] @{ PublishDate = $null CreatedDate = $null PreRelease = $null Version = $null Tag = $null Branch = $null Errors = $_.Exception.Message } } } else { [PSCustomObject] @{ PublishDate = $null CreatedDate = $null PreRelease = $null Version = $null Tag = $null Branch = $null Errors = "No connection (ping) to $($Url.Host)" } } $ProgressPreference = 'Continue' } function Test-ComputerPort { [CmdletBinding()] param ( [alias('Server')][string[]] $ComputerName, [int[]] $PortTCP, [int[]] $PortUDP, [int]$Timeout = 5000 ) begin { if ($Global:ProgressPreference -ne 'SilentlyContinue') { $TemporaryProgress = $Global:ProgressPreference $Global:ProgressPreference = 'SilentlyContinue' } } process { foreach ($Computer in $ComputerName) { foreach ($P in $PortTCP) { $Output = [ordered] @{ 'ComputerName' = $Computer 'Port' = $P 'Protocol' = 'TCP' 'Status' = $null 'Summary' = $null 'Response' = $null } <# $TcpClient = [System.Net.Sockets.TcpClient]::new() $Connect = $TcpClient.BeginConnect($Computer, $P, $null, $null) $Wait = $Connect.AsyncWaitHandle.WaitOne($Timeout, $false) if (!$Wait) { $TcpClient.Close() $Output['Status'] = $false $Output['Summary'] = "TCP $P Failed" } else { $TcpClient.EndConnect($Connect) $TcpClient.Close() $Output['Status'] = $true $Output['Summary'] = "TCP $P Successful" } $TcpClient.Close() $TcpClient.Dispose() #> $TcpClient = Test-NetConnection -ComputerName $Computer -Port $P -InformationLevel Detailed -WarningAction SilentlyContinue if ($TcpClient.TcpTestSucceeded) { $Output['Status'] = $TcpClient.TcpTestSucceeded $Output['Summary'] = "TCP $P Successful" } else { $Output['Status'] = $false $Output['Summary'] = "TCP $P Failed" $Output['Response'] = $Warnings } [PSCustomObject]$Output } foreach ($P in $PortUDP) { $Output = [ordered] @{ 'ComputerName' = $Computer 'Port' = $P 'Protocol' = 'UDP' 'Status' = $null 'Summary' = $null } $UdpClient = [System.Net.Sockets.UdpClient]::new($Computer, $P) $UdpClient.Client.ReceiveTimeout = $Timeout # $UdpClient.Connect($Computer, $P) $Encoding = [System.Text.ASCIIEncoding]::new() $byte = $Encoding.GetBytes("Evotec") [void]$UdpClient.Send($byte, $byte.length) $RemoteEndpoint = [System.Net.IPEndPoint]::new([System.Net.IPAddress]::Any, 0) try { $Bytes = $UdpClient.Receive([ref]$RemoteEndpoint) [string]$Data = $Encoding.GetString($Bytes) If ($Data) { $Output['Status'] = $true $Output['Summary'] = "UDP $P Successful" $Output['Response'] = $Data } } catch { $Output['Status'] = $false $Output['Summary'] = "UDP $P Failed" $Output['Response'] = $_.Exception.Message } $UdpClient.Close() $UdpClient.Dispose() [PSCustomObject]$Output } } } end { # Bring back setting as per default if ($TemporaryProgress) { $Global:ProgressPreference = $TemporaryProgress } } } function Test-WinRM { [CmdletBinding()] param ( [alias('Server')][string[]] $ComputerName ) $Output = foreach ($Computer in $ComputerName) { $Test = [PSCustomObject] @{ Output = $null Status = $null ComputerName = $Computer } try { $Test.Output = Test-WSMan -ComputerName $Computer -ErrorAction Stop $Test.Status = $true } catch { $Test.Status = $false } $Test } $Output } function Add-ManagerInformation { [CmdletBinding()] param( [System.Collections.IDictionary] $SummaryDictionary, [string] $Type, [string] $ManagerType, [Object] $Key, [PSCustomObject] $User, [PSCustomObject] $Rule # [bool] $Enabled ) #if ($Enabled) { if ($Key) { if ($Key -is [string]) { $KeyDN = $Key } else { $KeyDN = $Key.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 New-HTMLReport { [CmdletBinding()] param( $Report, $EmailParameters, $Logging, $FilePath, $SearchPath, $Rules, $UserSection, $ManagerSection, $SecuritySection, $AdminSection, $CachedUsers, $Summary, $SummaryUsersEmails, $SummaryManagersEmails, $SummaryEscalationEmails, $SummarySearch, $Locations, $AllSkipped ) $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" } # Create report New-HTML { 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 } } New-TableOption -DataStore JavaScript -ArrayJoin -BoolAsString 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 "Report Options" { New-HTMLList { foreach ($Key in $Report.Keys) { if ($Key -ne 'Password') { New-HTMLListItem -Text $Key, ": ", $HTMLReportPrimary[$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', ": ", $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) { New-HTMLListItem -Text "Default: ", $Rule.SendToManager.SecurityEscalation.Reminders.Default.Enable } else { New-HTMLListItem -Text "Default rule is ", "disabled" -FontWeight bold, bold -Color None, Red } #> 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) { 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.ShowSearchUsers) { [Array] $UsersSent = $SummarySearch['EmailSent'].Values #| ForEach-Object { if ($_ -ne $null) { $_ } } if ($UsersSent.Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'Users notified' -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 #| ForEach-Object { if ($_ -ne $null) { $_ } } if ($ShowSearchManagers.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 $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 #| ForEach-Object { if ($_ -ne $null) { $_ } } if ($ShowSearchEscalations.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 $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-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-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 ) if ($Template) { $SourceParameters = [ordered] @{ ManagerDisplayName = $User.DisplayName ManagerUsersTable = $ManagedUsers ManagerUsersTableManagerNotCompliant = $ManagedUsersManagerNotCompliant SummaryEscalationEmails = $SummaryEscalationEmails SummaryManagersEmails = $SummaryManagersEmails SummaryUsersEmails = $SummaryUsersEmails TimeToProcess = $TimeToProcess # Only works if User is set UserPrincipalName = $User.UserPrincipalName # : adm.pklys@ad.evotec.xyz SamAccountName = $User.SamAccountName # : adm.pklys Domain = $User.Domain # : ad.evotec.xyz Enabled = $User.Enabled EmailAddress = $User.EmailAddress # : DateExpiry = $User.DateExpiry # : DaysToExpire = $User.DaysToExpire # : PasswordExpired = $User.PasswordExpired # : False PasswordLastSet = $User.PasswordLastSet # : 05.09.2020 11:07:29 PasswordNotRequired = $User.PasswordNotRequired # : False PasswordNeverExpires = $User.PasswordNeverExpires # : True ManagerSamAccountName = $User.ManagerSamAccountName # : przemyslaw.klys ManagerEmail = $User.ManagerEmail # : przemyslaw.klys@evotec.pl ManagerStatus = $User.ManagerStatus # : Enabled ManagerLastLogonDays = $User.ManagerLastLogonDays # : 0 Manager = $User.Manager # : PrzemysÅ‚aw KÅ‚ys DisplayName = $User.DisplayName # : Administrator PrzemysÅ‚aw KÅ‚ys GivenName = $User.GivenName # : Administrator PrzemysÅ‚aw Surname = $User.Surname # : KÅ‚ys OrganizationalUnit = $User.OrganizationalUnit # : OU=Special,OU=Accounts,OU=Production,DC=ad,DC=evotec,DC=xyz MemberOf = $User.MemberOf # : {CN=GDS-TestGroup4,OU=Security,OU=Groups,OU=Production,DC=ad,DC=evotec,DC=xyz, CN=GDS-TestGroup2,OU=Security,OU=Groups,OU=Production,DC=ad,DC=evotec,DC=xyz, CN=Domain Admins,CN=Users,DC=ad,DC=evotec,DC=xyz} DistinguishedName = $User.DistinguishedName # : CN=Administrator PrzemysÅ‚aw KÅ‚ys,OU=Special,OU=Accounts,OU=Production,DC=ad,DC=evotec,DC=xyz ManagerDN = $User.ManagerDN # : CN=PrzemysÅ‚aw KÅ‚ys,OU=Users,OU=Accounts,OU=Production,DC=ad,DC=evotec,DC=xyz } $Body = EmailBody -EmailBody $Template -Parameter $SourceParameters # Below command would require to define variables as they are used in scriptblock #$EmailParameters.Subject = $ExecutionContext.InvokeCommand.ExpandString($Subject) # following replacement is a bit more cumbersome the the one above but a bit more secure and doesn't require creating 20+ unused variables $EmailParameters.Subject = Add-ParametersToString -String $Subject -Parameter $SourceParameters $EmailParameters.Body = $Body if ($Attachments) { $EmailParameters.Attachment = $Attachments } else { $EmailParameters.Attachment = @() } Send-EmailMessage @EmailParameters } } 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 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 .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()][string[]] $RulesProperties ) $Today = Get-Date $Properties = @( 'Manager', 'DisplayName', 'GivenName', 'Surname', 'SamAccountName', 'EmailAddress', 'msDS-UserPasswordExpiryTimeComputed', 'PasswordExpired', 'PasswordLastSet', 'PasswordNotRequired', 'Enabled', 'PasswordNeverExpires', 'Mail', 'MemberOf', 'LastLogonDate', 'Name' 'userAccountControl' 'msExchMailboxGuid' 'pwdLastSet', 'ObjectClass' 'LastLogonDate' 'Country' if ($OverwriteEmailProperty) { $OverwriteEmailProperty } foreach ($Rule in $RulesProperties) { $Rule } ) $Properties = $Properties | Sort-Object -Unique # lets build extended properties that need [Array] $ExtendedProperties = foreach ($Rule in $RulesProperties) { $Rule } [Array] $ExtendedProperties = $ExtendedProperties | Sort-Object -Unique $PropertiesContacts = @( 'SamAccountName', 'CanonicalName', 'WhenChanged', 'WhenChanged', 'DisplayName', 'DistinguishedName', 'Name', 'Mail', 'TargetAddress', 'ObjectClass' ) # We're caching all users to make sure it's speedy gonzales when querying for Managers if (-not $CachedUsers) { $CachedUsers = [ordered] @{ } } if (-not $Cache) { $Cache = [ordered] @{ } } Write-Color -Text '[i] ', "Discovering forest information" -Color Yellow, White $ForestInformation = Get-WinADForestDetails -PreferWritable -Extended -Forest $Forest -ExcludeDomains $ExcludeDomains -IncludeDomains $IncludeDomains -ExtendedForestInformation $ExtendedForestInformation # lets get domain name / netbios hashtable for easy use $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 } } foreach ($User in $Users) { $Cache[$User.DistinguishedName] = $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)" $DateExpiry = $null $DaysToExpire = $null $PasswordDays = $null $PasswordNeverExpires = $null $PasswordAtNextLogon = $null $HasMailbox = $null #$UserManager = $Cache["$($User.Manager)"] if ($User.Manager) { $Manager = $Cache[$User.Manager].DisplayName $ManagerSamAccountName = $Cache[$User.Manager].SamAccountName $ManagerDisplayName = $Cache[$User.Manager].DisplayName $ManagerEmail = $Cache[$User.Manager].Mail $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' } $Manager = $null $ManagerSamAccountName = $null $ManagerDisplayName = $null $ManagerEmail = $null $ManagerEnabled = $null $ManagerLastLogon = $null $ManagerLastLogonDays = $null $ManagerType = $null } if ($OverwriteEmailProperty) { # fix this for a user $EmailTemp = $User.$OverwriteEmailProperty if ($EmailTemp -like '*@*') { $EmailAddress = $EmailTemp } else { $EmailAddress = $User.EmailAddress } # Fix this for manager as well if ($Cache["$($User.Manager)"]) { if ($Cache["$($User.Manager)"].$OverwriteEmailProperty -like '*@*') { # $UserManager.Mail = $UserManager.$OverwriteEmailProperty $ManagerEmail = $Cache["$($User.Manager)"].$OverwriteEmailProperty } } } else { $EmailAddress = $User.EmailAddress } if ($User.PasswordLastSet) { $PasswordDays = (New-TimeSpan -Start ($User.PasswordLastSet) -End ($Today)).Days } else { $PasswordDays = $null } # Since we fixed manager above, we now check for status 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' # we need to treat it as always enabled } else { $ManagerStatus = 'Disabled' } } if ($User."msDS-UserPasswordExpiryTimeComputed" -ne 9223372036854775807) { # This is standard situation where users password is expiring as needed try { $DateExpiry = ([datetime]::FromFileTime($User."msDS-UserPasswordExpiryTimeComputed")) } catch { $DateExpiry = $User."msDS-UserPasswordExpiryTimeComputed" } try { $DaysToExpire = (New-TimeSpan -Start ($Today) -End ([datetime]::FromFileTime($User."msDS-UserPasswordExpiryTimeComputed"))).Days } catch { $DaysToExpire = $null } $PasswordNeverExpires = $User.PasswordNeverExpires } else { # This is non-standard situation. This basically means most likely Fine Grained Group Policy is in action where it makes PasswordNeverExpires $true # Since FGP policies are a bit special they do not tick the PasswordNeverExpires box, but at the same time value for "msDS-UserPasswordExpiryTimeComputed" is set to 9223372036854775807 $PasswordNeverExpires = $true } if ($User.pwdLastSet -eq 0 -and $DateExpiry.Year -eq 1601) { $PasswordAtNextLogon = $true } else { $PasswordAtNextLogon = $false } if ($PasswordNeverExpires -or $null -eq $User.PasswordLastSet) { # If password last set is null or password never expires is set to true, then date of expiry and days to expire is not applicable $DateExpiry = $null $DaysToExpire = $null } $UserAccountControl = Convert-UserAccountControl -UserAccountControl $User.UserAccountControl if ($UserAccountControl -contains 'INTERDOMAIN_TRUST_ACCOUNT') { continue } if ($User.'msExchMailboxGuid') { $HasMailbox = $true } else { $HasMailbox = $false } 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 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 = ConvertFrom-DistinguishedName -DistinguishedName $User.DistinguishedName -ToOrganizationalUnit MemberOf = $User.MemberOf DistinguishedName = $User.DistinguishedName ManagerDN = $User.Manager Country = $Country CountryCode = $CountryCode Type = 'User' } $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 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' } } 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++ Write-Verbose -Message "Processing $($Contact.DisplayName) - $($CountContacts)/$($Contacts.Count)" # create dummy objects for manager contacts $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 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 = ConvertFrom-DistinguishedName -DistinguishedName $Contact.DistinguishedName -ToOrganizationalUnit MemberOf = $Contact.MemberOf DistinguishedName = $Contact.DistinguishedName ManagerDN = $null Country = $null CountryCode = $null Type = 'Contact' } # this allows to extend the object with custom properties requested by user # especially custom extensions for use within rules foreach ($E in $ExtendedProperties) { $MyUser[$E] = $User.$E } if ($HashtableField -eq 'NetBiosSamAccountName') { # Contacts do not have 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 #$SummarySearch = Get-Content -LiteralPath $SearchPath -Raw | ConvertFrom-Json } 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 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, [switch] $IncludeStatistics, [alias('ForestName')][string] $Forest, [string[]] $ExcludeDomains, [alias('Domain', 'Domains')][string[]] $IncludeDomains, [System.Collections.IDictionary] $ExtendedForestInformation ) $PropertiesToAdd = @( 'ClearTextPassword' 'LMHash' 'EmptyPassword' 'WeakPassword' #'DefaultComputerPassword' #'PasswordNotRequired' #'PasswordNeverExpires' 'AESKeysMissing' 'PreAuthNotRequired' 'DESEncryptionOnly' 'Kerberoastable' 'DelegatableAdmins' 'SmartCardUsersWithPassword' 'DuplicatePasswordGroups' ) $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 } 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 -IncludeDisabledAccounts 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 #DefaultComputerPassword = $PasswordsInHash.DefaultComputerPassword.Count #DefaultComputerPasswordEnabledOnly = 0 #DefaultComputerPasswordDisabledOnly = 0 PasswordNotRequired = 0 # $PasswordsInHash.PasswordNotRequired.Count PasswordNotRequiredEnabledOnly = 0 PasswordNotRequiredDisabledOnly = 0 PasswordNeverExpires = 0 #$PasswordsInHash.PasswordNeverExpires.Count 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 -ReturnHashTable $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++ } # we keep stats per country for weak passwords and duplicate passwords $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"]++ } # we keep stats per country for weak passwords and duplicate passwords 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 } } else { $OutputUsers } } 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 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, [string[]] $WeakPasswords, [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 # since the first entry didn't go to log file, this will Write-Color -InformationAction SilentlyContinue -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 $PasswordQuality = Find-PasswordQuality -IncludeStatistics -WeakPasswords $WeakPasswords -Forest $Forest -ExcludeDomains $ExcludeDomains -IncludeDomains $IncludeDomains -ExtendedForestInformation $ExtendedForestInformation if (-not $PasswordQuality) { # most likely DSInternals not installed 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 New-HTMLText -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 #New-HTMLText -LineBreak New-HTMLText -Text "Here's a short overview of what this report shows:" -Color None -FontSize 12px #New-HTMLText -LineBreak 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 " $ValueDisabled " 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) } } # # Define event # New-ChartEvent -DataTableID 'NewIDtoSearchInChart' -ColumnID 0 } } } $PropertiesHighlight = @( 'ClearTextPassword' 'LMHash' 'EmptyPassword' 'WeakPassword' #'DefaultComputerPassword' #'PasswordNotRequired' #'PasswordNeverExpires' 'AESKeysMissing' 'PreAuthNotRequired' 'DESEncryptionOnly' 'Kerberoastable' 'DelegatableAdmins' 'SmartCardUsersWithPassword' #'DuplicatePasswordGroups' ) 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.UsersTotal = $Group.Users.Count $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 { # add the map areas # we will add unknown countries to the Greenland area 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 } } } # configure legend New-MapLegendOption -Type 'Area' -Mode horizontal New-MapLegendOption -Type 'Plot' -Mode horizontal # add legend 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 #-AreaTitle "Duplicate Passwords Users" 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 { # add the map areas # we will add unknown countries to the Greenland area 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 } } } # configure legend New-MapLegendOption -Type 'Area' -Mode horizontal New-MapLegendOption -Type 'Plot' -Mode horizontal # add legend 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 #-AreaTitle "Weak Password Users" } } } 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 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 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()] param( [Parameter(Mandatory)][System.Collections.IDictionary] $EmailParameters, [string] $OverwriteEmailProperty, [Parameter(Mandatory)][System.Collections.IDictionary] $UserSection, [Parameter(Mandatory)][System.Collections.IDictionary] $ManagerSection, [Parameter(Mandatory)][System.Collections.IDictionary] $SecuritySection, [Parameter(Mandatory)][System.Collections.IDictionary] $AdminSection, [Parameter(Mandatory)][Array] $Rules, [scriptblock] $TemplatePreExpiry, [string] $TemplatePreExpirySubject, [scriptblock] $TemplatePostExpiry, [string] $TemplatePostExpirySubject, [Parameter(Mandatory)][scriptblock] $TemplateManager, [Parameter(Mandatory)][string] $TemplateManagerSubject, [Parameter(Mandatory)][scriptblock] $TemplateSecurity, [Parameter(Mandatory)][string] $TemplateSecuritySubject, [Parameter(Mandatory)][scriptblock] $TemplateManagerNotCompliant, [Parameter(Mandatory)][string] $TemplateManagerNotCompliantSubject, [Parameter(Mandatory)][scriptblock] $TemplateAdmin, [Parameter(Mandatory)][string] $TemplateAdminSubject, [Parameter(Mandatory)][System.Collections.IDictionary] $Logging, [Array] $HTMLReports, [string] $SearchPath ) $TimeStart = Start-TimeLog $Script:Reporting = [ordered] @{} $Script:Reporting['Version'] = Get-GitHubVersion -Cmdlet 'Start-PasswordSolution' -RepositoryOwner 'evotecit' -RepositoryName 'PasswordSolution' $TodayDate = Get-Date $Today = Get-Date -Format "yyyy-MM-dd HH:mm:ss" if (-not $Logging) { $Logging = @{ ShowTime = $true LogFile = "" TimeFormat = "yyyy-MM-dd HH:mm:ss" } } $PSDefaultParameterValues = @{ "Write-Color:LogFile" = $Logging.LogFile "Write-Color:ShowTime" = $Logging.ShowTime "Write-Color:TimeFormat" = $Logging.TimeFormat } if ($Logging.LogFile) { $FolderPath = [io.path]::GetDirectoryName($Logging.LogFile) if (-not (Test-Path -LiteralPath $FolderPath)) { $null = New-Item -Path $FolderPath -ItemType Directory -Force } $CurrentLogs = Get-ChildItem -LiteralPath $FolderPath | Sort-Object -Property CreationTime -Descending | Select-Object -Skip $Logging.LogMaximum if ($CurrentLogs) { Write-Color -Text '[i] ', "Logs directory has more than ", $Logging.LogMaximum, " log files. Cleanup required..." -Color Yellow, DarkCyan, Red, DarkCyan foreach ($Log in $CurrentLogs) { try { Remove-Item -LiteralPath $Log.FullName -Confirm:$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 } } } } 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] @{} } } $Summary = [ordered] @{} $Summary['Notify'] = [ordered] @{} $Summary['NotifyManager'] = [ordered] @{} $Summary['NotifySecurity'] = [ordered] @{} $Summary['Rules'] = [ordered] @{} $AllSkipped = [ordered] @{} $Locations = [ordered] @{} Write-Color -Text "[i]", " Starting process to find expiring users" -Color Yellow, White, Green, White, Green, White, Green, White $CachedUsers = Find-Password -AsHashTable -OverwriteEmailProperty $OverwriteEmailProperty foreach ($Rule in $Rules) { # Go for each rule and check if the user is in any of those rules if ($Rule.Enable -eq $true) { Write-Color -Text "[i]", " Processing rule ", $Rule.Name, ' status: ', $Rule.Enable -Color Yellow, White, Green, White, Green, White, Green, White # Lets create summary for the rule if (-not $Summary['Rules'][$Rule.Name] ) { $Summary['Rules'][$Rule.Name] = [ordered] @{} } # this will make sure to expand array of multiple arrays of ints if provided # for example: (-150..-100),(-60..0), 1, 2, 3 $Rule.Reminders = $Rule.Reminders | ForEach-Object { $_ } foreach ($User in $CachedUsers.Values) { if ($User.Enabled -eq $false) { # We don't want to have disabled users continue } if ($Rule.ExcludeOU.Count -gt 0) { $FoundOU = $false foreach ($OU in $Rule.ExcludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } # if OU is found we need to exclude the user if ($FoundOU) { continue } } if ($Rule.IncludeOU.Count -gt 0) { # Rule defined that only user withi specific OU has to be found $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) { # Rule defined that only user withi specific group has to be found $FoundGroup = $false foreach ($Group in $Rule.ExcludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } # If found, we need to skip user if ($FoundGroup) { continue } } if ($Rule.IncludeGroup.Count -gt 0) { # Rule defined that only user withi specific group has to be found $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 ($Summary['Notify'][$User.DistinguishedName] -and $Summary['Notify'][$User.DistinguishedName].ProcessManagersOnly -ne $true) { # User already exists in the notifications - rules are overlapping, we only take the first one # We also check for ProcessManagersOnly because we don't want first rule to ignore any other rules for users 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) { # we skip those that never expire continue } } elseif ($Rule.IncludePasswordNeverExpires) { if ($User.PasswordNeverExpires -eq $true) { $DaysToPasswordExpiry = $Rule.PasswordNeverExpiresDays - $User.PasswordDays $User.DaysToExpire = $DaysToPasswordExpiry } else { # we skip users who expire 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) { # This is to track users that our account may not have permissions over 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 } # if days to expire is not set, password last set is not set either # this means account either was never used or account we're using to has no permissions over that account $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) } } # Lets find users that expire, and match our rule if ($null -ne $User.DaysToExpire -and $User.DaysToExpire -in $Rule.Reminders) { # check if we need to notify user or just manager 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 } $Summary['Notify'][$User.DistinguishedName] = [ordered] @{ User = $User Rule = $Rule ProcessManagersOnly = $Rule.ProcessManagersOnly } # If we need to send an email to manager we need to update rules, just in case the user has not matched for user section if ($Summary['Rules'][$Rule.Name][$User.DistinguishedName]) { # User exists, update reason $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleOptions.Add('User') $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleName = $Rule.Name } else { # User doesn't exists in rules, add it $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 } } } # Lets find users that we need to notify manager about 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 # Manager is enabled and has an email, this is standard situation for manager in AD # But before we go and do that, maybe user wants to send emails to managers if those users are in specific group or OU if ($Rule.SendToManager.Manager.IncludeOU.Count -gt 0) { # Rule defined that only user withi specific OU has to be found $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 OU is found we need to exclude the user if ($FoundOU) { $SendToManager = $false } } if ($SendToManager -and $Rule.SendToManager.Manager.ExcludeGroup.Count -gt 0) { # Rule defined that only user withi specific group has to be found $FoundGroup = $false foreach ($Group in $Rule.SendToManager.Manager.ExcludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } # if Group found, we need to skip this user if ($FoundGroup) { $SendToManager = $false } } if ($SendToManager -and $Rule.SendToManager.Manager.IncludeGroup.Count -gt 0) { # Rule defined that only user withi specific group has to be found $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) { # Use default reminder as per user, not per manager $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) { # User manager reminder as per manager config $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 } <# $Summary['Notify'][$User.DistinguishedName] = [ordered] @{ User = $User Rule = $Rule ProcessManagersOnly = $Rule.ProcessManagersOnly } #> # If we need to send an email to manager we need to update rules, just in case the user has not matched for user section if ($Summary['Rules'][$Rule.Name][$User.DistinguishedName]) { # User exists, update reason $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleOptions.Add('Manager') $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleName = $Rule.Name } else { # User doesn't exists in rules, add it $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 } # Push manager to list $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifyManager'] Type = 'ManagerDefault' ManagerType = 'Ok' Key = $User.ManagerDN User = $User Rule = $Rule } Add-ManagerInformation @Splat } } } } # Lets find users that have no manager, manager is not enabled or manager has no email if ($Rule.SendToManager -and $Rule.SendToManager.ManagerNotCompliant -and $Rule.SendToManager.ManagerNotCompliant.Enable -eq $true -and $Rule.SendToManager.ManagerNotCompliant.Manager) { # Not compliant (missing, disabled, no email), covers all the below options if ($Rule.SendToManager.ManagerNotCompliant -and $Rule.SendToManager.ManagerNotCompliant.Enable -and $Rule.SendToManager.ManagerNotCompliant.Manager) { $ManagerNotCompliant = $true # But before we go and do that, maybe user wants to send emails to managers only if those users are in specific group or OU if ($Rule.SendToManager.ManagerNotCompliant.IncludeOU.Count -gt 0) { # Rule defined that only user withi specific OU has to be found $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 OU is found we need to exclude the user if ($FoundOU) { $ManagerNotCompliant = $false } } if ($ManagerNotCompliant -and $Rule.SendToManager.ManagerNotCompliant.ExcludeGroup.Count -gt 0) { # Rule defined that only user withi specific group has to be found $FoundGroup = $false foreach ($Group in $Rule.SendToManager.ManagerNotCompliant.ExcludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } # if Group found, we need to skip this user if ($FoundGroup) { $ManagerNotCompliant = $false } } if ($ManagerNotCompliant -and $Rule.SendToManager.ManagerNotCompliant.IncludeGroup.Count -gt 0) { # Rule defined that only user withi specific group has to be found $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') { # Manager is enabled but missing 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') { # Manager is disabled, regardless if he/she has email $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifyManager'] Type = 'ManagerNotCompliant' ManagerType = 'Manager disabled' Key = $Rule.SendToManager.ManagerNotCompliant.Manager User = $User Rule = $Rule } Add-ManagerInformation @Splat $ManagerNotCompliantMatched = $true } elseif ($Rule.SendToManager.ManagerNotCompliant.LastLogon -and $User.ManagerLastLogonDays -ge $Rule.SendToManager.ManagerNotCompliant.LastLogonDays) { # Manager Last Logon over X days $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifyManager'] Type = 'ManagerNotCompliant' ManagerType = 'Manager not logging in' Key = $Rule.SendToManager.ManagerNotCompliant.Manager User = $User Rule = $Rule } Add-ManagerInformation @Splat $ManagerNotCompliantMatched = $true } elseif ($Rule.SendToManager.ManagerNotCompliant.Missing -and $User.ManagerStatus -eq 'Missing') { # Manager is missing $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifyManager'] Type = 'ManagerNotCompliant' ManagerType = 'Manager not set' Key = $Rule.SendToManager.ManagerNotCompliant.Manager User = $User Rule = $Rule } Add-ManagerInformation @Splat $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 we need to send an email to manager we need to update rules, just in case the user has not matched for user section if ($Summary['Rules'][$Rule.Name][$User.DistinguishedName]) { # User exists, update reason $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleOptions.Add('Manager Not Compliant') $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleName = $Rule.Name } else { # User doesn't exists in rules, add it $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') { # do nothing } else { # This shouldn't happen, but just in case - we can log if this happens 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 } } } } } } # Lets find users that require escalation 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) { # Rule defined that only user withi specific OU has to be found $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 OU is found we need to exclude the user if ($FoundOU) { $SecurityEscalation = $false } } if ($SecurityEscalation -and $Rule.SendToManager.SecurityEscalation.ExcludeGroup.Count -gt 0) { # Rule defined that only user withi specific group has to be found $FoundGroup = $false foreach ($Group in $Rule.SendToManager.SecurityEscalation.ExcludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } # if Group found, we need to skip this user if ($FoundGroup) { $SecurityEscalation = $false } } if ($SecurityEscalation -and $Rule.SendToManager.SecurityEscalation.IncludeGroup.Count -gt 0) { # Rule defined that only user withi specific group has to be found $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 we need to send an email to manager we need to update rules, just in case the user has not matched for user section if ($Summary['Rules'][$Rule.Name][$User.DistinguishedName]) { # User exists, update reason $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleOptions.Add('Security esclation') $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleName = $Rule.Name } else { # User doesn't exists in rules, add it $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 { Write-Color -Text "[i]", " Processing rule ", $Rule.Name, ' status: ', $Rule.Enable -Color Red, White, Red, White, Red, White, Red, White } } 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 # This shouldn't happen, but just in case, to be removed later on, as ProcessManagerOnly is skipping earlier on 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) { # User uses template per rule $EmailSplat.Template = $Notify.Rule.TemplatePreExpiry } elseif ($TemplatePreExpiry) { # User uses global template $EmailSplat.Template = $TemplatePreExpiry } else { # User uses built-in template $EmailSplat.Template = { } } if ($Notify.Rule.TemplatePreExpirySubject) { $EmailSplat.Subject = $Notify.Rule.TemplatePreExpirySubject } elseif ($TemplatePreExpirySubject) { $EmailSplat.Subject = $TemplatePreExpirySubject } else { $EmailSplat.Subject = '[Password] Your password will expire on $DateExpiry ($TimeToExpire days)' } } else { if ($Notify.Rule.TemplatePostExpiry) { $EmailSplat.Template = $Notify.Rule.TemplatePostExpiry } elseif ($TemplatePostExpiry) { $EmailSplat.Template = $TemplatePostExpiry } else { $EmailSplat.Template = { } } if ($Notify.Rule.TemplatePostExpirySubject) { $EmailSplat.Subject = $Notify.Rule.TemplatePostExpirySubject } elseif ($TemplatePostExpirySubject) { $EmailSplat.Subject = $TemplatePostExpirySubject } else { $EmailSplat.Subject = '[Password] Your password expired on $DateExpiry ($TimeToExpire days ago)' } } $EmailSplat.User = $Notify.User $EmailSplat.EmailParameters = $EmailParameters if ($UserSection.SendToDefaultEmail -ne $true) { $EmailSplat.EmailParameters.To = $Notify.User.EmailAddress } else { $EmailSplat.EmailParameters.To = $UserSection.DefaultEmail } if ($Notify.User.EmailAddress -like "*@*") { # Regardless if we send email to default email or to user, if user doesn't have email address we shouldn't send an email $EmailResult = Send-PasswordEmail @EmailSplat [PSCustomObject] @{ UserPrincipalName = $EmailSplat.User.UserPrincipalName SamAccountName = $EmailSplat.User.SamAccountName Domain = $EmailSplat.User.Domain Rule = $Notify.Rule.Name Status = $EmailResult.Status StatusWhen = Get-Date -Format "yyyy-MM-dd HH:mm:ss" StatusError = $EmailResult.Error SentTo = $EmailResult.SentTo DateExpiry = $EmailSplat.User.DateExpiry DaysToExpire = $EmailSplat.User.DaysToExpire PasswordExpired = $EmailSplat.User.PasswordExpired PasswordNeverExpires = $EmailSplat.User.PasswordNeverExpires PasswordLastSet = $EmailSplat.User.PasswordLastSet } } else { # Email not sent $EmailResult = @{ Status = $false Error = 'No email address for user' SentTo = '' } [PSCustomObject] @{ UserPrincipalName = $EmailSplat.User.UserPrincipalName SamAccountName = $EmailSplat.User.SamAccountName Domain = $EmailSplat.User.Domain Rule = $Notify.Rule.Name Status = $EmailResult.Status StatusWhen = Get-Date -Format "yyyy-MM-dd HH:mm:ss" StatusError = $EmailResult.Error SentTo = $EmailResult.SentTo DateExpiry = $EmailSplat.User.DateExpiry DaysToExpire = $EmailSplat.User.DaysToExpire PasswordExpired = $EmailSplat.User.PasswordExpired PasswordNeverExpires = $EmailSplat.User.PasswordNeverExpires PasswordLastSet = $EmailSplat.User.PasswordLastSet } } if ($Logging.NotifyOnUserSend) { Write-Color -Text "[i]", " Sending notifications to users ", $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 } 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, DarkMagenta 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 } else { Write-Color -Text "[i] Sending notifications to users is ", "disabled!" -Color White, Yellow, DarkMagenta } 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]) { # This user is "findable" in AD $ManagerUser = $CachedUsers[$Manager] } else { # This user is provided by user in config file $ManagerUser = $Summary['NotifyManager'][$Manager]['Manager'] } [Array] $ManagedUsers = $Summary['NotifyManager'][$Manager]['ManagerDefault'].Values.Output [Array] $ManagedUsersManagerNotCompliant = $Summary['NotifyManager'][$Manager]['ManagerNotCompliant'].Values.Output $EmailSplat = [ordered] @{} if ($Summary['NotifyManager'][$Manager].ManagerDefault.Count -gt 0) { if ($TemplateManager) { # User uses global template $EmailSplat.Template = $TemplateManager } else { # User uses built-in template $EmailSplat.Template = { } } if ($TemplateManagerSubject) { $EmailSplat.Subject = $TemplateManagerSubject } else { $EmailSplat.Subject = "[Password Expiring] Dear Manager - Your accounts are expiring!" } } elseif ($Summary['NotifyManager'][$Manager].ManagerNotCompliant.Count -gt 0) { if ($TemplateManagerNotCompliant) { # User uses global template $EmailSplat.Template = $TemplateManagerNotCompliant } else { # User uses built-in template $EmailSplat.Template = { } } if ($TemplateManagerNotCompliantSubject) { $EmailSplat.Subject = $TemplateManagerNotCompliantSubject } else { $EmailSplat.Subject = "[Password Escalation] Accounts are expiring with non-compliant manager" } } $EmailSplat.User = $ManagerUser $EmailSplat.ManagedUsers = $ManagedUsers $EmailSplat.ManagedUsersManagerNotCompliant = $ManagedUsersManagerNotCompliant $EmailSplat.EmailParameters = $EmailParameters 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) { 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 } [PSCustomObject] @{ DisplayName = $ManagerUser.DisplayName SamAccountName = $ManagerUser.SamAccountName Domain = $ManagerUser.Domain Status = $EmailResult.Status StatusWhen = Get-Date -Format "yyyy-MM-dd HH:mm:ss" SentTo = $EmailResult.SentTo StatusError = $EmailResult.Error Accounts = $ManagedUsers.SamAccountName AccountsCount = $ManagedUsers.Count Template = 'Unknown' ManagerNotCompliant = $ManagedUsersManagerNotCompliant.SamAccountName ManagerNotCompliantCount = $ManagedUsersManagerNotCompliant.Count #ManagerDisabled = $ManagedUsersManagerDisabled.SamAccountName #ManagerDisabledCount = $ManagedUsersManagerDisabled.Count #ManagerMissing = $ManagedUsersManagerMissing.SamAccountName #ManagerMissingCount = $ManagedUsersManagerMissing.Count #ManagerMissingEmail = $ManagedUsersManagerMissingEmail.SamAccountName #ManagerMissingEmailCount = $ManagedUsersManagerMissingEmail.Count } if ($ManagerSection.SendCountMaximum -gt 0) { if ($ManagerSection.SendCountMaximum -le $CountManagers) { Write-Color -Text "[i]", " Send count maximum reached. There may be more managers that match the rule." -Color Red, DarkMagenta 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 #Write-Color -Text "[i] Sending notifications to managers (sent: ", $SummaryManagersEmails.Count, ")" -Color White, Yellow, White, Yellow, White, Yellow, White } else { Write-Color -Text "[i] Sending notifications to managers is ", "disabled!" -Color White, Yellow, DarkMagenta } 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++ # This user is provided by user in config file $ManagerUser = $Summary['NotifySecurity'][$Manager]['Manager'] [Array] $ManagedUsers = $Summary['NotifySecurity'][$Manager]['Security'].Values.Output $EmailSplat = [ordered] @{} if ($Summary['NotifySecurity'][$Manager].Security.Count -gt 0) { # User uses global template $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.ManagedUsersManagerNotCompliant = $ManagedUsersManagerNotCompliant #$EmailSplat.ManagedUsersManagerDisabled = $ManagedUsersManagerDisabled #$EmailSplat.ManagedUsersManagerMissing = $ManagedUsersManagerMissing #$EmailSplat.ManagedUsersManagerMissingEmail = $ManagedUsersManagerMissingEmail $EmailSplat.EmailParameters = $EmailParameters 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' # ManagerNotCompliant = $ManagedUsersManagerNotCompliant.SamAccountName # ManagerNotCompliantCount = $ManagedUsersManagerNotCompliant.Count #ManagerDisabled = $ManagedUsersManagerDisabled.SamAccountName #ManagerDisabledCount = $ManagedUsersManagerDisabled.Count #ManagerMissing = $ManagedUsersManagerMissing.SamAccountName #ManagerMissingCount = $ManagedUsersManagerMissing.Count #ManagerMissingEmail = $ManagedUsersManagerMissingEmail.SamAccountName #ManagerMissingEmailCount = $ManagedUsersManagerMissingEmail.Count } if ($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, DarkMagenta 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 } else { Write-Color -Text "[i] Sending notifications to security is ", "disabled!" -Color White, Yellow, DarkMagenta } $TimeEnd = Stop-TimeLog -Time $TimeStart -Option OneLiner $HtmlAttachments = [System.Collections.Generic.List[string]]::new() foreach ($Report in $HTMLReports) { if ($Report.Enable) { $ReportSettings = @{ Report = $Report EmailParameters = $EmailParameters Logging = $Logging FilePath = $FilePath 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 } 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 } } } } if ($SearchPath) { Write-Color -Text "[i]" , " Saving Search report " -Color White, Yellow, Green $SummarySearch['EmailSent'][$Today] = $SummaryUsersEmails $SummarySearch['EmailEscalations'][$Today] = $SummaryEscalationEmails $SummarySearch['EmailManagers'][$Today] = $SummaryManagersEmails try { $SummarySearch | Export-Clixml -LiteralPath $SearchPath } 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 } if ($AdminSection.Enable) { Write-Color -Text "[i] Sending summary information " -Color White, Yellow, White, Yellow, White, Yellow, White $CountSecurity = 0 [Array] $SummaryEmail = @( $CountSecurity++ # This user is provided by user in config file $ManagerUser = $AdminSection.Manager $EmailSplat = [ordered] @{} # User uses global template $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 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 [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 } } # Export functions and aliases as required Export-ModuleMember -Function @('Find-Password', 'Find-PasswordNotification', 'Find-PasswordQuality', 'Show-PasswordQuality', 'Start-PasswordSolution') -Alias @() # SIG # Begin signature block # MIInPgYJKoZIhvcNAQcCoIInLzCCJysCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC/Dv1lxH11GSOM # wDtfAQhCiJtlzbQ2M5WVOjdCyeF6j6CCITcwggO3MIICn6ADAgECAhAM5+DlF9hG # /o/lYPwb8DA5MA0GCSqGSIb3DQEBBQUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBa # Fw0zMTExMTAwMDAwMDBaMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lD # ZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC # AQoCggEBAK0OFc7kQ4BcsYfzt2D5cRKlrtwmlIiq9M71IDkoWGAM+IDaqRWVMmE8 # tbEohIqK3J8KDIMXeo+QrIrneVNcMYQq9g+YMjZ2zN7dPKii72r7IfJSYd+fINcf # 4rHZ/hhk0hJbX/lYGDW8R82hNvlrf9SwOD7BG8OMM9nYLxj+KA+zp4PWw25EwGE1 # lhb+WZyLdm3X8aJLDSv/C3LanmDQjpA1xnhVhyChz+VtCshJfDGYM2wi6YfQMlqi # uhOCEe05F52ZOnKh5vqk2dUXMXWuhX0irj8BRob2KHnIsdrkVxfEfhwOsLSSplaz # vbKX7aqn8LfFqD+VFtD/oZbrCF8Yd08CAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGG # MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEXroq/0ksuCMS1Ri6enIZ3zbcgP # MB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBBQUA # A4IBAQCiDrzf4u3w43JzemSUv/dyZtgy5EJ1Yq6H6/LV2d5Ws5/MzhQouQ2XYFwS # TFjk0z2DSUVYlzVpGqhH6lbGeasS2GeBhN9/CTyU5rgmLCC9PbMoifdf/yLil4Qf # 6WXvh+DfwWdJs13rsgkq6ybteL59PyvztyY1bV+JAbZJW58BBZurPSXBzLZ/wvFv # hsb6ZGjrgS2U60K3+owe3WLxvlBnt2y98/Efaww2BxZ/N3ypW2168RJGYIPXJwS+ # S86XvsNnKmgR34DnDDNmvxMNFG7zfx9jEB76jRslbWyPpbdhAbHSoyahEHGdreLD # +cOZUbcrBwjOLuZQsqf6CkUvovDyMIIFMDCCBBigAwIBAgIQBAkYG1/Vu2Z1U0O1 # b5VQCDANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGln # aUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtE # aWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMTMxMDIyMTIwMDAwWhcNMjgx # MDIyMTIwMDAwWjByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5j # MRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBT # SEEyIEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0BAQEF # AAOCAQ8AMIIBCgKCAQEA+NOzHH8OEa9ndwfTCzFJGc/Q+0WZsTrbRPV/5aid2zLX # cep2nQUut4/6kkPApfmJ1DcZ17aq8JyGpdglrA55KDp+6dFn08b7KSfH03sjlOSR # I5aQd4L5oYQjZhJUM1B0sSgmuyRpwsJS8hRniolF1C2ho+mILCCVrhxKhwjfDPXi # TWAYvqrEsq5wMWYzcT6scKKrzn/pfMuSoeU7MRzP6vIK5Fe7SrXpdOYr/mzLfnQ5 # Ng2Q7+S1TqSp6moKq4TzrGdOtcT3jNEgJSPrCGQ+UpbB8g8S9MWOD8Gi6CxR93O8 # vYWxYoNzQYIH5DiLanMg0A9kczyen6Yzqf0Z3yWT0QIDAQABo4IBzTCCAckwEgYD # VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYB # BQUHAwMweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k # aWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0 # LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4 # oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJv # b3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy # dEFzc3VyZWRJRFJvb3RDQS5jcmwwTwYDVR0gBEgwRjA4BgpghkgBhv1sAAIEMCow # KAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCgYIYIZI # AYb9bAMwHQYDVR0OBBYEFFrEuXsqCqOl6nEDwGD5LfZldQ5YMB8GA1UdIwQYMBaA # FEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBCwUAA4IBAQA+7A1aJLPz # ItEVyCx8JSl2qB1dHC06GsTvMGHXfgtg/cM9D8Svi/3vKt8gVTew4fbRknUPUbRu # pY5a4l4kgU4QpO4/cY5jDhNLrddfRHnzNhQGivecRk5c/5CxGwcOkRX7uq+1UcKN # JK4kxscnKqEpKBo6cSgCPC6Ro8AlEeKcFEehemhor5unXCBc2XGxDI+7qPjFEmif # z0DLQESlE/DmZAwlCEIysjaKJAL+L3J+HNdJRZboWR3p+nRka7LrZkPas7CM1ekN # 3fYBIM6ZMWM9CBoYs4GbT8aTEAb8B4H6i9r5gkn3Ym6hU/oSlBiFLpKR6mhsRDKy # ZqHnGKSaZFHvMIIFPTCCBCWgAwIBAgIQBNXcH0jqydhSALrNmpsqpzANBgkqhkiG # 9w0BAQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkw # FwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEy # IEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTIwMDYyNjAwMDAwMFoXDTIz # MDcwNzEyMDAwMFowejELMAkGA1UEBhMCUEwxEjAQBgNVBAgMCcWabMSFc2tpZTER # MA8GA1UEBxMIS2F0b3dpY2UxITAfBgNVBAoMGFByemVteXPFgmF3IEvFgnlzIEVW # T1RFQzEhMB8GA1UEAwwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMIIBIjANBgkq # hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7KB3iyBrhkLUbbFe9qxhKKPBYqDBqln # r3AtpZplkiVjpi9dMZCchSeT5ODsShPuZCIxJp5I86uf8ibo3vi2S9F9AlfFjVye # 3dTz/9TmCuGH8JQt13ozf9niHecwKrstDVhVprgxi5v0XxY51c7zgMA2g1Ub+3ti # i0vi/OpmKXdL2keNqJ2neQ5cYly/GsI8CREUEq9SZijbdA8VrRF3SoDdsWGf3tZZ # zO6nWn3TLYKQ5/bw5U445u/V80QSoykszHRivTj+H4s8ABiforhi0i76beA6Ea41 # zcH4zJuAp48B4UhjgRDNuq8IzLWK4dlvqrqCBHKqsnrF6BmBrv+BXQIDAQABo4IB # xTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0OBBYE # FBixNSfoHFAgJk4JkDQLFLRNlJRmMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK # BggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdpY2Vy # dC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2NybDQu # ZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUwQzA3 # BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu # Y29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNpZ25p # bmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAmr1sz4ls # LARi4wG1eg0B8fVJFowtect7SnJUrp6XRnUG0/GI1wXiLIeow1UPiI6uDMsRXPHU # F/+xjJw8SfIbwava2eXu7UoZKNh6dfgshcJmo0QNAJ5PIyy02/3fXjbUREHINrTC # vPVbPmV6kx4Kpd7KJrCo7ED18H/XTqWJHXa8va3MYLrbJetXpaEPpb6zk+l8Rj9y # G4jBVRhenUBUUj3CLaWDSBpOA/+sx8/XB9W9opYfYGb+1TmbCkhUg7TB3gD6o6ES # Jre+fcnZnPVAPESmstwsT17caZ0bn7zETKlNHbc1q+Em9kyBjaQRcEQoQQNpezQu # g9ufqExx6lHYDjCCBY0wggR1oAMCAQICEA6bGI750C3n79tQ4ghAGFowDQYJKoZI # hvcNAQEMBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ # MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNz # dXJlZCBJRCBSb290IENBMB4XDTIyMDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1OVow # YjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ # d3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290 # IEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAv+aQc2jeu+RdSjww # IjBpM+zCpyUuySE98orYWcLhKac9WKt2ms2uexuEDcQwH/MbpDgW61bGl20dq7J5 # 8soR0uRf1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNwwrK6dZlqczKU0RBEEC7fgvMH # hOZ0O21x4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs06wXGXuxbGrzryc/NrDRAX7F6 # Zu53yEioZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e5TXnMcvak17cjo+A2raRmECQ # ecN4x7axxLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtVgkEy19sEcypukQF8IUzUvK4b # A3VdeGbZOjFEmjNAvwjXWkmkwuapoGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2mHY9 # WV1CdoeJl2l6SPDgohIbZpp0yt5LHucOY67m1O+SkjqePdwA5EUlibaaRBkrfsCU # tNJhbesz2cXfSwQAzH0clcOP9yGyshG3u3/y1YxwLEFgqrFjGESVGnZifvaAsPvo # ZKYz0YkH4b235kOkGLimdwHhD5QMIR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxfjT/J # vNNBERJb5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFrb7GrhotPwtZFX50g/KEexcCP # orF+CiaZ9eRpL5gdLfXZqbId5RsCAwEAAaOCATowggE2MA8GA1UdEwEB/wQFMAMB # Af8wHQYDVR0OBBYEFOzX44LScV1kTN8uZz/nupiuHA9PMB8GA1UdIwQYMBaAFEXr # oq/0ksuCMS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQEAwIBhjB5BggrBgEFBQcBAQRt # MGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEF # BQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJl # ZElEUm9vdENBLmNydDBFBgNVHR8EPjA8MDqgOKA2hjRodHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMBEGA1UdIAQKMAgw # BgYEVR0gADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3v1cH # vZqsoYcs7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy3iS8 # UgPITtAq3votVs/59PesMHqai7Je1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cnRNTn # f+hZqPC/Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4oVaO7KTVPeix3P0c2PR3WlxU # jG/voVA9/HYJaISfb8rbII01YBwCA8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2zm8j # LfR+cWojayL/ErhULSd+2DrZ8LaHlv1b0VysGMNNn3O3AamfV6peKOK5lDCCBq4w # ggSWoAMCAQICEAc2N7ckVHzYR6z9KGYqXlswDQYJKoZIhvcNAQELBQAwYjELMAkG # A1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRp # Z2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MB4X # DTIyMDMyMzAwMDAwMFoXDTM3MDMyMjIzNTk1OVowYzELMAkGA1UEBhMCVVMxFzAV # BgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVk # IEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFtcGluZyBDQTCCAiIwDQYJKoZIhvcN # AQEBBQADggIPADCCAgoCggIBAMaGNQZJs8E9cklRVcclA8TykTepl1Gh1tKD0Z5M # om2gsMyD+Vr2EaFEFUJfpIjzaPp985yJC3+dH54PMx9QEwsmc5Zt+FeoAn39Q7SE # 2hHxc7Gz7iuAhIoiGN/r2j3EF3+rGSs+QtxnjupRPfDWVtTnKC3r07G1decfBmWN # lCnT2exp39mQh0YAe9tEQYncfGpXevA3eZ9drMvohGS0UvJ2R/dhgxndX7RUCyFo # bjchu0CsX7LeSn3O9TkSZ+8OpWNs5KbFHc02DVzV5huowWR0QKfAcsW6Th+xtVhN # ef7Xj3OTrCw54qVI1vCwMROpVymWJy71h6aPTnYVVSZwmCZ/oBpHIEPjQ2OAe3Vu # JyWQmDo4EbP29p7mO1vsgd4iFNmCKseSv6De4z6ic/rnH1pslPJSlRErWHRAKKtz # Q87fSqEcazjFKfPKqpZzQmiftkaznTqj1QPgv/CiPMpC3BhIfxQ0z9JMq++bPf4O # uGQq+nUoJEHtQr8FnGZJUlD0UfM2SU2LINIsVzV5K6jzRWC8I41Y99xh3pP+OcD5 # sjClTNfpmEpYPtMDiP6zj9NeS3YSUZPJjAw7W4oiqMEmCPkUEBIDfV8ju2TjY+Cm # 4T72wnSyPx4JduyrXUZ14mCjWAkBKAAOhFTuzuldyF4wEr1GnrXTdrnSDmuZDNIz # tM2xAgMBAAGjggFdMIIBWTASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBS6 # FtltTYUvcyl2mi91jGogj57IbzAfBgNVHSMEGDAWgBTs1+OC0nFdZEzfLmc/57qY # rhwPTzAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgwdwYIKwYB # BQUHAQEEazBpMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w # QQYIKwYBBQUHMAKGNWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy # dFRydXN0ZWRSb290RzQuY3J0MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwz # LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3JsMCAGA1UdIAQZ # MBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAgEAfVmO # wJO2b5ipRCIBfmbW2CFC4bAYLhBNE88wU86/GPvHUF3iSyn7cIoNqilp/GnBzx0H # 6T5gyNgL5Vxb122H+oQgJTQxZ822EpZvxFBMYh0MCIKoFr2pVs8Vc40BIiXOlWk/ # R3f7cnQU1/+rT4osequFzUNf7WC2qk+RZp4snuCKrOX9jLxkJodskr2dfNBwCnzv # qLx1T7pa96kQsl3p/yhUifDVinF2ZdrM8HKjI/rAJ4JErpknG6skHibBt94q6/ae # sXmZgaNWhqsKRcnfxI2g55j7+6adcq/Ex8HBanHZxhOACcS2n82HhyS7T6NJuXdm # kfFynOlLAlKnN36TU6w7HQhJD5TNOXrd/yVjmScsPT9rp/Fmw0HNT7ZAmyEhQNC3 # EyTN3B14OuSereU0cZLXJmvkOHOrpgFPvT87eK1MrfvElXvtCl8zOYdBeHo46Zzh # 3SP9HSjTx/no8Zhf+yvYfvJGnXUsHicsJttvFXseGYs2uJPU5vIXmVnKcPA3v5gA # 3yAWTyf7YGcWoWa63VXAOimGsJigK+2VQbc61RWYMbRiCQ8KvYHZE/6/pNHzV9m8 # BPqC3jLfBInwAM1dwvnQI38AC+R2AibZ8GV2QqYphwlHK+Z/GqSFD/yYlvZVVCsf # gPrA8g4r5db7qS9EFUrnEw4d2zc4GqEr9u3WfPwwggbAMIIEqKADAgECAhAMTWly # S5T6PCpKPSkHgD1aMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVTMRcwFQYD # VQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBH # NCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwHhcNMjIwOTIxMDAwMDAw # WhcNMzMxMTIxMjM1OTU5WjBGMQswCQYDVQQGEwJVUzERMA8GA1UEChMIRGlnaUNl # cnQxJDAiBgNVBAMTG0RpZ2lDZXJ0IFRpbWVzdGFtcCAyMDIyIC0gMjCCAiIwDQYJ # KoZIhvcNAQEBBQADggIPADCCAgoCggIBAM/spSY6xqnya7uNwQ2a26HoFIV0Mxom # rNAcVR4eNm28klUMYfSdCXc9FZYIL2tkpP0GgxbXkZI4HDEClvtysZc6Va8z7GGK # 6aYo25BjXL2JU+A6LYyHQq4mpOS7eHi5ehbhVsbAumRTuyoW51BIu4hpDIjG8b7g # L307scpTjUCDHufLckkoHkyAHoVW54Xt8mG8qjoHffarbuVm3eJc9S/tjdRNlYRo # 44DLannR0hCRRinrPibytIzNTLlmyLuqUDgN5YyUXRlav/V7QG5vFqianJVHhoV5 # PgxeZowaCiS+nKrSnLb3T254xCg/oxwPUAY3ugjZNaa1Htp4WB056PhMkRCWfk3h # 3cKtpX74LRsf7CtGGKMZ9jn39cFPcS6JAxGiS7uYv/pP5Hs27wZE5FX/NurlfDHn # 88JSxOYWe1p+pSVz28BqmSEtY+VZ9U0vkB8nt9KrFOU4ZodRCGv7U0M50GT6Vs/g # 9ArmFG1keLuY/ZTDcyHzL8IuINeBrNPxB9ThvdldS24xlCmL5kGkZZTAWOXlLimQ # prdhZPrZIGwYUWC6poEPCSVT8b876asHDmoHOWIZydaFfxPZjXnPYsXs4Xu5zGcT # B5rBeO3GiMiwbjJ5xwtZg43G7vUsfHuOy2SJ8bHEuOdTXl9V0n0ZKVkDTvpd6kVz # HIR+187i1Dp3AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/ # BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEE # AjALBglghkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8w # HQYDVR0OBBYEFGKK3tBh/I8xFO2XC809KpQU31KcMFoGA1UdHwRTMFEwT6BNoEuG # SWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQw # OTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQG # CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKG # TGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJT # QTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIB # AFWqKhrzRvN4Vzcw/HXjT9aFI/H8+ZU5myXm93KKmMN31GT8Ffs2wklRLHiIY1UJ # RjkA/GnUypsp+6M/wMkAmxMdsJiJ3HjyzXyFzVOdr2LiYWajFCpFh0qYQitQ/Bu1 # nggwCfrkLdcJiXn5CeaIzn0buGqim8FTYAnoo7id160fHLjsmEHw9g6A++T/350Q # p+sAul9Kjxo6UrTqvwlJFTU2WZoPVNKyG39+XgmtdlSKdG3K0gVnK3br/5iyJpU4 # GYhEFOUKWaJr5yI+RCHSPxzAm+18SLLYkgyRTzxmlK9dAlPrnuKe5NMfhgFknADC # 6Vp0dQ094XmIvxwBl8kZI4DXNlpflhaxYwzGRkA7zl011Fk+Q5oYrsPJy8P7mxNf # arXH4PMFw1nfJ2Ir3kHJU7n/NBBn9iYymHv+XEKUgZSCnawKi8ZLFUrTmJBFYDOA # 4CPe+AOk9kVH5c64A0JH6EE2cXet/aLol3ROLtoeHYxayB6a1cLwxiKoT5u92Bya # UcQvmvZfpyeXupYuhVfAYOd4Vn9q78KVmksRAsiCnMkaBXy6cbVOepls9Oie1FqY # yJ+/jbsYXEP10Cro4mLueATbvdH7WwqocH7wl4R44wgDXUcsY6glOJcB0j862uXl # 9uab3H4szP8XTE0AotjWAQ64i+7m4HJViSwnGWH2dwGMMYIFXTCCBVkCAQEwgYYw # cjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ # d3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVk # IElEIENvZGUgU2lnbmluZyBDQQIQBNXcH0jqydhSALrNmpsqpzANBglghkgBZQME # AgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEM # BgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqG # SIb3DQEJBDEiBCDsKaVoyydzs1t4nv3e5kGq6NvB6KoJE55djAqHEXse8TANBgkq # hkiG9w0BAQEFAASCAQCkO0nxaAAY2d5s+5HEn2he0ARJvu20Tf7h4mVL4wIm7uxj # Y3ONaAnV3ZSH7bKkeJJ60Ujo7gLNn9ILHEdQRD/HP8+b82muLsU9o1qQ192sXtNf # sbFY/krb8SXE+pOB1Tpj8ffRg+L1yMLnvQbd6WNerXZP6L9TTig1evrHQ0FjixUK # Z6OJ1zmCMKuW8WH8hZXW8MXwhO1KuFWD382ja/gcTvcpaGFa/XTwCGYbCxAly+Wk # oBGmzTHvgZm2pkIaUnenx+Rfcts534LpsrU2avIA5QGhEoJ3Y+PNK2F/21yqBFI4 # XKoBQb7JmEYm0BoMKUtCoENO2m4PVxEPPUIDS+pfoYIDIDCCAxwGCSqGSIb3DQEJ # BjGCAw0wggMJAgEBMHcwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0 # LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hB # MjU2IFRpbWVTdGFtcGluZyBDQQIQDE1pckuU+jwqSj0pB4A9WjANBglghkgBZQME # AgEFAKBpMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X # DTIzMDQyNTA3MjMyNlowLwYJKoZIhvcNAQkEMSIEID5bpLTE12MWYoBqFOhfX54W # tBHwE2WcoUnB6L0a0MTBMA0GCSqGSIb3DQEBAQUABIICABIehAu1ftglJ6A3CPN0 # 2OXRklmJBTABgJ84yFXAbPzDjMgf7Camkn/D3dMxab/Hz/O3gsyHHTm5TFCjrp5P # tMimMeT4c9HyaFECb+FhSA0vxeh+GW0fRlpmMQatKOStUmXSpkUU2iijsJGGC7pb # M9fuq5TjakADJB9i5ZptQnebkBkbxrlu8K3GfXjB3tz7YcYETEKQ3ZPheHbjzzA7 # WydEhnbUy/ZvY6jmlzH/K8Kj8muWQx8JkKp69dY7TFUqH8yBrIbD12VD6RRjTdlB # CWiVbo9Jgi5YL49QXI0vEpD+7wRrbvTAY2CaCOGZGsXTLm90pC2Pxa5TrDq+C7K9 # 2+sL4iSpwZEadImvda/Qjv25cZqsmudDvhp4iK53NpqW3IwYzv+ZUKgeEXHqqgig # ifkkXdY3Qlvdu7fyl4EU+Nq/gRHiBbP/ONMlGFi/apj41camBw7zMJM0KQjhn6tJ # LI2M0zHV3prps7fhGY+LoFBN2T8QOPo67cPIShfr51N5AsRWpoawnVC/xwShaZuM # TylnJj80vwKhK1Elr9k/aGICW4/fZUy9qaYACOgu/zZpAZhYkJ5xT6y1sHtC12Ho # 3c+fXzG9kY2mInRlG8SGTc3fM9dEEoFTzeOCNvSBCY8nIQDkeq3oqDE7MxDfD0bf # r11Lt476+SpGGRsZlKiFsvk/ # SIG # End signature block |