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 ($PSBoundParameters.ContainsKey('CountryCode')) { if ($All) { $Script:QuickSearch[$CountryCode] } else { $Script:QuickSearch[$CountryCode].RegionInformation.EnglishName } } else { $Script:QuickSearch } } else { $Script:QuickSearch = [ordered] @{} $AllCultures = [cultureinfo]::GetCultures([System.Globalization.CultureTypes]::SpecificCultures) foreach ($Culture in $AllCultures) { $RegionInformation = [System.Globalization.RegionInfo]::new($Culture) $Script:QuickSearch[$RegionInformation.TwoLetterISORegionName] = @{ 'Culture' = $Culture 'RegionInformation' = $RegionInformation } $Script:QuickSearch[$RegionInformation.ThreeLetterISORegionName] = @{ 'Culture' = $Culture 'RegionInformation' = $RegionInformation } } if ($PSBoundParameters.ContainsKey('CountryCode')) { if ($All) { $Script:QuickSearch[$CountryCode] } else { $Script:QuickSearch[$CountryCode].RegionInformation.EnglishName } } else { $Script:QuickSearch } } } function Convert-CountryToContinent { <# .SYNOPSIS Convert country to continent .DESCRIPTION Convert country to continent or return a hashtable of countries and their corresponding continent. If the country is not found (for example empty), it will return "Unknown" .PARAMETER Country Country to convert. If country is not given it will return a hashtable of countries and their corresponding continent. .EXAMPLE Convert-CountryToContinent -Country "Poland" .EXAMPLE Convert-CountryToContinent .NOTES General notes #> [CmdletBinding()] param( [string] $Country, [switch] $ReturnHashTable ) $CountryToContinent = [ordered] @{ "Afghanistan" = "Asia" "Albania" = "Europe" "Algeria" = "Africa" "Andorra" = "Europe" "Angola" = "Africa" "Antigua and Barbuda" = "North America" "Argentina" = "South America" "Armenia" = "Asia" "Australia" = "Australia/Oceania" "Austria" = "Europe" "Azerbaijan" = "Asia" "Bahamas" = "North America" "Bahrain" = "Asia" "Bangladesh" = "Asia" "Barbados" = "North America" "Belarus" = "Europe" "Belgium" = "Europe" "Belize" = "North America" "Benin" = "Africa" "Bhutan" = "Asia" "Bolivia" = "South America" "Bosnia and Herzegovina" = "Europe" "Botswana" = "Africa" "Brazil" = "South America" "Brunei" = "Asia" "Bulgaria" = "Europe" "Burkina Faso" = "Africa" "Burundi" = "Africa" "Cabo Verde" = "Africa" "Cambodia" = "Asia" "Cameroon" = "Africa" "Canada" = "North America" "Central African Republic" = "Africa" "Chad" = "Africa" "Chile" = "South America" "China" = "Asia" "Colombia" = "South America" "Comoros" = "Africa" "Congo, Democratic Republic of the" = "Africa" "Congo, Republic of the" = "Africa" "Costa Rica" = "North America" "Cote d'Ivoire" = "Africa" "Croatia" = "Europe" "Cuba" = "North America" "Cyprus" = "Asia" "Czechia" = "Europe" "Denmark" = "Europe" "Djibouti" = "Africa" "Dominica" = "North America" "Dominican Republic" = "North America" "Ecuador" = "South America" "Egypt" = "Africa" "El Salvador" = "North America" "Equatorial Guinea" = "Africa" "Eritrea" = "Africa" "Estonia" = "Europe" "Eswatini" = "Africa" "Ethiopia" = "Africa" "Fiji" = "Australia/Oceania" "Finland" = "Europe" "France" = "Europe" "Gabon" = "Africa" "Gambia" = "Africa" "Georgia" = "Asia" "Germany" = "Europe" "Ghana" = "Africa" "Greece" = "Europe" "Grenada" = "North America" "Guatemala" = "North America" "Guinea" = "Africa" "Guinea-Bissau" = "Africa" "Guyana" = "South America" "Haiti" = "North America" "Honduras" = "North America" "Hungary" = "Europe" "Iceland" = "Europe" "India" = "Asia" "Indonesia" = "Asia" "Iran" = "Asia" "Iraq" = "Asia" "Ireland" = "Europe" "Israel" = "Asia" "Italy" = "Europe" "Jamaica" = "North America" "Japan" = "Asia" "Jordan" = "Asia" "Kazakhstan" = "Asia" "Kenya" = "Africa" "Kiribati" = "Australia/Oceania" "Kosovo" = "Europe" "Kuwait" = "Asia" "Kyrgyzstan" = "Asia" "Laos" = "Asia" "Latvia" = "Europe" "Lebanon" = "Asia" "Lesotho" = "Africa" "Liberia" = "Africa" "Libya" = "Africa" "Liechtenstein" = "Europe" "Lithuania" = "Europe" "Luxembourg" = "Europe" "Madagascar" = "Africa" "Malawi" = "Africa" "Malaysia" = "Asia" "Maldives" = "Asia" "Mali" = "Africa" "Malta" = "Europe" "Marshall Islands" = "Australia/Oceania" "Mauritania" = "Africa" "Mauritius" = "Africa" "Mexico" = "North America" "Micronesia" = "Australia/Oceania" "Moldova" = "Europe" "Monaco" = "Europe" "Mongolia" = "Asia" "Montenegro" = "Europe" "Morocco" = "Africa" "Mozambique" = "Africa" "Myanmar" = "Asia" "Namibia" = "Africa" "Nauru" = "Australia/Oceania" "Nepal" = "Asia" "Netherlands" = "Europe" "New Zealand" = "Australia/Oceania" "Nicaragua" = "North America" "Niger" = "Africa" "Nigeria" = "Africa" "North Korea" = "Asia" "North Macedonia" = "Europe" "Norway" = "Europe" "Oman" = "Asia" "Pakistan" = "Asia" "Palau" = "Australia/Oceania" "Panama" = "North America" "Papua New Guinea" = "Australia/Oceania" "Paraguay" = "South America" "Peru" = "South America" "Philippines" = "Asia" "Poland" = "Europe" "Portugal" = "Europe" "Qatar" = "Asia" "Romania" = "Europe" "Russia" = "Asia" "Rwanda" = "Africa" "Saint Kitts and Nevis" = "North America" "Saint Lucia" = "North America" "Saint Vincent and the Grenadines" = "North America" "Samoa" = "Australia/Oceania" "San Marino" = "Europe" "Sao Tome and Principe" = "Africa" "Saudi Arabia" = "Asia" "Senegal" = "Africa" "Serbia" = "Europe" "Seychelles" = "Africa" "Sierra Leone" = "Africa" "Singapore" = "Asia" "Slovakia" = "Europe" "Slovenia" = "Europe" "Solomon Islands" = "Australia/Oceania" "Somalia" = "Africa" "South Africa" = "Africa" "South Korea" = "Asia" "South Sudan" = "Africa" "Spain" = "Europe" "Sri Lanka" = "Asia" "Sudan" = "Africa" "Suriname" = "South America" "Sweden" = "Europe" "Switzerland" = "Europe" "Syria" = "Asia" "Taiwan" = "Asia" "Tajikistan" = "Asia" "Tanzania" = "Africa" "Thailand" = "Asia" "Timor-Leste" = "Asia" "Togo" = "Africa" "Tonga" = "Australia/Oceania" "Trinidad and Tobago" = "North America" "Tunisia" = "Africa" "Turkey" = "Asia" "Turkmenistan" = "Asia" "Tuvalu" = "Australia/Oceania" "Uganda" = "Africa" "Ukraine" = "Europe" "United Arab Emirates" = "Asia" "United Kingdom" = "Europe" "United States of America" = "North America" "Uruguay" = "South America" "Uzbekistan" = "Asia" "Vanuatu" = "Australia/Oceania" "Vatican City (Holy See)" = "Europe" "Venezuela" = "South America" "Vietnam" = "Asia" "Yemen" = "Asia" "Zambia" = "Africa" "Zimbabwe" = "Africa" } if ($PSBoundParameters.ContainsKey('Country')) { if ($CountryToContinent[$Country]) { $CountryToContinent[$Country] } else { "Unknown" } } else { $CountryToContinent } } function 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 (22621)' = 'Windows 11 22H2' '10.0 (22000)' = 'Windows 11 21H2' '10.0 (19045)' = 'Windows 10 22H2' '10.0 (19044)' = 'Windows 10 21H2' '10.0 (19043)' = 'Windows 10 21H1' '10.0 (19042)' = 'Windows 10 20H2' '10.0 (19041)' = 'Windows 10 2004' '10.0 (18898)' = 'Windows 10 Insider Preview' '10.0 (18363)' = "Windows 10 1909" '10.0 (18362)' = "Windows 10 1903" '10.0 (17763)' = "Windows 10 1809" '10.0 (17134)' = "Windows 10 1803" '10.0 (16299)' = "Windows 10 1709" '10.0 (15063)' = "Windows 10 1703" '10.0 (14393)' = "Windows 10 1607" '10.0 (10586)' = "Windows 10 1511" '10.0 (10240)' = "Windows 10 1507" # This is how WMI/CIM stores it '10.0.22621' = 'Windows 11 22H2' '10.0.22000' = 'Windows 11 21H2' '10.0.19045' = 'Windows 10 22H2' '10.0.19044' = 'Windows 10 21H2' '10.0.19043' = 'Windows 10 21H1' '10.0.19042' = 'Windows 10 20H2' '10.0.19041' = 'Windows 10 2004' '10.0.18898' = 'Windows 10 Insider Preview' '10.0.18363' = "Windows 10 1909" '10.0.18362' = "Windows 10 1903" '10.0.17763' = "Windows 10 1809" '10.0.17134' = "Windows 10 1803" '10.0.16299' = "Windows 10 1709" '10.0.15063' = "Windows 10 1703" '10.0.14393' = "Windows 10 1607" '10.0.10586' = "Windows 10 1511" '10.0.10240' = "Windows 10 1507" # This is how it's written in registry '22621' = 'Windows 11 22H2' '22000' = 'Windows 11 21H2' '19045' = 'Windows 10 22H2' '19044' = 'Windows 10 21H2' '19043' = 'Windows 10 21H1' '19042' = 'Windows 10 20H2' '19041' = 'Windows 10 2004' '18898' = 'Windows 10 Insider Preview' '18363' = "Windows 10 1909" '18362' = "Windows 10 1903" '17763' = "Windows 10 1809" '17134' = "Windows 10 1803" '16299' = "Windows 10 1709" '15063' = "Windows 10 1703" '14393' = "Windows 10 1607" '10586' = "Windows 10 1511" '10240' = "Windows 10 1507" } $System = $Systems[$OperatingSystemVersion] if (-not $System) { $System = $OperatingSystemVersion } } elseif ($OperatingSystem -like 'Windows Server*') { # 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 = $OperatingSystemVersion } } 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 ) if ($Key) { if ($Key -is [string]) { $KeyDN = $Key } else { $KeyDN = $Key.DisplayName } if (-not $SummaryDictionary[$KeyDN]) { $SummaryDictionary[$KeyDN] = [ordered] @{ Manager = $Key ManagerDefault = [ordered] @{} ManagerNotCompliant = [ordered] @{} Security = [ordered] @{} } } $SummaryDictionary[$KeyDN][$Type][$User.DistinguishedName] = [ordered] @{ Manager = $User.ManagerDN User = $User Rule = $Rule ManagerOption = $Type Output = [ordered] @{} } $Default = [ordered] @{ DisplayName = $User.DisplayName Enabled = $User.Enabled SamAccountName = $User.SamAccountName Domain = $User.Domain DateExpiry = $User.DateExpiry DaysToExpire = $User.DaysToExpire PasswordLastSet = $User.PasswordLastSet PasswordExpired = $User.PasswordExpired } if ($Type -ne 'ManagerDefault') { $Extended = [ordered] @{ 'Status' = $ManagerType 'Manager' = $User.Manager 'Manager Email' = $User.ManagerEmail } $SummaryDictionary[$KeyDN][$Type][$User.DistinguishedName]['Output'] = [PSCustomObject] ( $Extended + $Default) } else { $SummaryDictionary[$KeyDN][$Type][$User.DistinguishedName]['Output'] = [PSCustomObject] $Default } } } function Add-ParametersToString { <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER String Parameter description .PARAMETER Parameter Parameter description .EXAMPLE $Test = 'this is a string $Test - and $Test2 AND $tEST3' Add-ParametersToString -String $Test -Parameter @{ Testooo = 'sdsds' Test = 'oh my god' Test2 = 'ole ole' TEST3 = '56555' } .NOTES General notes #> [CmdletBinding()] param( [string] $String, [System.Collections.IDictionary] $Parameter ) $Sorted = $Parameter.Keys | Sort-Object { $_.length } -Descending foreach ($Key in $Sorted) { $String = $String -ireplace [Regex]::Escape("`$$Key"), $Parameter[$Key] } $String } function Export-SearchInformation { [CmdletBinding()] param( [string] $SearchPath, [System.Collections.IDictionary] $SummarySearch, [string] $Today, [Array] $SummaryUsersEmails, [Array] $SummaryManagersEmails, [Array] $SummaryEscalationEmails ) if ($SearchPath) { Write-Color -Text "[i]" , " Saving Search report " -Color White, Yellow, Green if ($SummaryUsersEmails) { $SummarySearch['EmailSent'][$Today] += $SummaryUsersEmails } if ($SummaryEscalationEmails) { $SummarySearch['EmailEscalations'][$Today] += $SummaryEscalationEmails } if ($SummaryManagersEmails) { $SummarySearch['EmailManagers'][$Today] += $SummaryManagersEmails } try { $SummarySearch | Export-Clixml -LiteralPath $SearchPath -ErrorAction Stop } catch { Write-Color -Text "[e]", " Couldn't save to file $SearchPath", ". Error: ", $_.Exception.Message -Color White, Yellow, White, Yellow, White, Yellow, White } Write-Color -Text "[i]" , " Saving Search report ", "Done" -Color White, Yellow, Green } } function Import-SearchInformation { [CmdletBinding()] param( [string] $SearchPath ) if ($SearchPath) { if (Test-Path -LiteralPath $SearchPath) { try { Write-Color -Text "[i]", " Loading file ", $SearchPath -Color White, Yellow, White, Yellow, White, Yellow, White $SummarySearch = Import-Clixml -LiteralPath $SearchPath -ErrorAction Stop } catch { Write-Color -Text "[e]", " Couldn't load the file $SearchPath", ". Skipping...", $_.Exception.Message -Color White, Yellow, White, Yellow, White, Yellow, White } } } if (-not $SummarySearch) { $SummarySearch = [ordered] @{ EmailSent = [ordered] @{} EmailManagers = [ordered] @{} EmailEscalations = [ordered] @{} } } $SummarySearch } function Invoke-PasswordRuleProcessing { [CmdletBinding()] param( [System.Collections.IDictionary] $Rule, [System.Collections.IDictionary] $Summary, [System.Collections.IDictionary] $CachedUsers, [System.Collections.IDictionary] $AllSkipped, [System.Collections.IDictionary] $Locations, [System.Collections.IDictionary] $Logging, [DateTime] $TodayDate ) # 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 within 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 within 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 within 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 ($Rule.ExcludeName.Count -gt 0) { $ExcludeName = $false foreach ($Name in $Rule.ExcludeName) { foreach ($Property in $Rule.ExcludeNameProperties) { if ($User.$Property -like $Name) { $ExcludeName = $true break } } if ($ExcludeName) { break } } if ($ExcludeName) { continue } } if ($Summary['Notify'][$User.DistinguishedName] -and $Summary['Notify'][$User.DistinguishedName].ProcessManagersOnly -ne $true) { # 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 } # This is required for email notification to different email address # User wanted to use different email address for notifications based on external property such as # employeeID, employeeNumber, extensionAttributes, etc. # normally this wouldn't be required if it's global setting, but if only a handful of users need to have their address changed # the per rule overwriteemailproperty should be used if ($Rule.OverwriteEmailProperty) { $NewPropertyWithEmail = $Rule.OverwriteEmailProperty if ($NewPropertyWithEmail -and $User.$NewPropertyWithEmail) { $User.EmailAddress = $User.$NewPropertyWithEmail } } $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 } } } # this is to overwrite manager by using extensionAttribute or any other field in AD # it works on SamAccountName, DistinguishedName only if ($Rule.OverwriteManagerProperty) { $NewPropertyWithManager = $Rule.OverwriteManagerProperty if ($NewPropertyWithManager -and $User.$NewPropertyWithManager) { $NewManager = $CachedUsers[$User.$NewPropertyWithManager] if ($NewManager -and $NewManager.Mail -like "*@*") { $User.ManagerEmail = $NewManager.Mail $User.Manager = $NewManager.DisplayName $User.ManagerSamAccountName = $NewManager.SamAccountName $User.ManagerEnabled = $NewManager.Enabled $User.ManagerLastLogon = $NewManager.LastLogonDate if ($User.ManagerLastLogon) { $User.ManagerLastLogonDays = $( - $($User.ManagerLastLogon - $Today).Days) } else { $User.ManagerLastLogonDays = $null } $User.ManagerType = $NewManager.ObjectClass $User.ManagerDN = $NewManager.DistinguishedName } } } # 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 } # 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 { if ($null -ne $Rule.Name -and $null -ne $Rule.Enable) { Write-Color -Text "[i]", " Processing rule ", $Rule.Name, ' status: ', $Rule.Enable -Color Red, White, Red, White, Red, White, Red, White } } } function New-HTMLReport { [CmdletBinding()] param( [System.Collections.IDictionary] $Report, [System.Collections.IDictionary] $EmailParameters, [System.Collections.IDictionary] $Logging, [string] $SearchPath, [Array] $Rules, [System.Collections.IDictionary] $UserSection, [System.Collections.IDictionary] $ManagerSection, [System.Collections.IDictionary] $SecuritySection, [System.Collections.IDictionary] $AdminSection, [System.Collections.IDictionary] $CachedUsers, [System.Collections.IDictionary] $Summary, [Array] $SummaryUsersEmails, [Array] $SummaryManagersEmails, [Array] $SummaryEscalationEmails, [System.Collections.IDictionary] $SummarySearch, [System.Collections.IDictionary] $Locations, [System.Collections.IDictionary] $AllSkipped ) $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-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow New-HTMLPanelStyle -BorderRadius 0px New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ', ' -ArrayJoin New-HTMLHeader { New-HTMLSection -Invisible { New-HTMLSection { New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue } -JustifyContent flex-start -Invisible New-HTMLSection { New-HTMLText -Text "Password Solution - $($Script:Reporting['Version'])" -Color Blue } -JustifyContent flex-end -Invisible } } if ($Report.ShowConfiguration) { New-HTMLTab -Name "About" { New-HTMLTab -Name "Configuration" { New-HTMLSection -Invisible { New-HTMLSection -HeaderText "Email Configuration" { New-HTMLList { foreach ($Key in $EmailParameters.Keys) { if ($Key -eq 'Body') { } elseif ($Key -ne 'Password') { New-HTMLListItem -Text $Key, ": ", $EmailParameters[$Key] -FontWeight normal, normal, bold } else { New-HTMLListItem -Text $Key, ": ", "REDACTED" -FontWeight normal, normal, bold } } } } New-HTMLSection -HeaderText "Logging" { New-HTMLList { foreach ($Key in $Logging.Keys) { if ($Key -ne 'Password') { New-HTMLListItem -Text $Key, ": ", $Logging[$Key] -FontWeight normal, normal, bold } else { New-HTMLListItem -Text $Key, ": ", "REDACTED" -FontWeight normal, normal, bold } } } } New-HTMLSection -HeaderText "Other" { New-HTMLList { New-HTMLListItem -Text 'FilePath', ": ", $Report.FilePath -FontWeight normal, normal, bold New-HTMLListItem -Text 'SearchPath', ": ", $SearchPath -FontWeight normal, normal, bold } } } New-HTMLSection -Invisible { New-HTMLSection -HeaderText "User Section" { New-HTMLList { New-HTMLListItem -Text "Enabled: ", $UserSection.Enable -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "SendCountMaximum: ", $UserSection.SendCountMaximum -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "SendToDefaultEmail: ", $UserSection.SendToDefaultEmail -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "DefaultEmail: ", ($UserSection.DefaultEmail -join ", ") -FontWeight normal, bold -TextDecoration underline, none } } New-HTMLSection -HeaderText "Manager Section" { New-HTMLList { New-HTMLListItem -Text "Enabled: ", $ManagerSection.Enable -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "SendCountMaximum: ", $ManagerSection.SendCountMaximum -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "DefaultEmail: ", ($ManagerSection.DefaultEmail -join ", ") -FontWeight normal, bold -TextDecoration underline, none } } New-HTMLSection -HeaderText "Security Section" { New-HTMLList { New-HTMLListItem -Text "Enabled: ", $SecuritySection.Enable -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "SendCountMaximum: ", $SecuritySection.SendCountMaximum -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "SendToDefaultEmail: ", $SecuritySection.SendToDefaultEmail -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "DefaultEmail: ", ($SecuritySection.DefaultEmail -join ", ") -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "Attach CSV: ", ($SecuritySection.AttachCSV -join ",") -FontWeight normal, bold -TextDecoration underline, none } } New-HTMLSection -HeaderText "Admin Section" { New-HTMLList { New-HTMLListItem -Text "Enabled: ", $AdminSection.Enable -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "Subject: ", $AdminSection.Subject -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "Manager: ", $AdminSection.Manager.DisplayName -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "Manager Email: ", ($AdminSection.Manager.EmailAddress -join ", ") -FontWeight normal, bold -TextDecoration underline, none } } } } New-HTMLTab -Name 'Rules Configuration' { New-HTMLText -Text "There are ", $Rules.Count, " rules defined in the Password Solution. ", "Please keep in mind that order of the rules matter." -FontWeight normal, bold, normal -Color None, Blue, None foreach ($Rule in $Rules) { if ($Rule.Enable) { $SectionColor = 'SpringGreen' } else { $SectionColor = 'Coral' } New-HTMLSection -HeaderText "Rule $($Rule.Name)" -CanCollapse -HeaderBackGroundColor $SectionColor { New-HTMLList { if ($Rule.Enable) { New-HTMLListItem -Text "Rule ", $Rule.Name, " is ", "enabled" -FontWeight normal, bold, normal, bold, normal, normal -Color None, None, None, Green } else { New-HTMLListItem -Text "Rule ", $Rule.Name, " is ", "disabled" -FontWeight normal, bold, normal, bold, normal, normal -Color None, None, None, Red } New-HTMLList { New-HTMLListItem -Text "Notify till expiry on ", $($Rule.Reminders -join ","), " day " -FontWeight normal, bold, normal if ($Rule.IncludeExpiring) { New-HTMLListItem -Text "Include expiring accounts is ", "enabled" -FontWeight bold, bold -Color None, Green } else { New-HTMLListItem -Text "Include expiring accounts is ", "disabled" -FontWeight bold, bold -Color None, Red } if ($Rule.IncludePasswordNeverExpires) { New-HTMLListItem -Text "Include passwords never expiring with ", $Rule.PasswordNeverExpiresDays, " days rule" -FontWeight bold -Color Amethyst } else { New-HTMLListItem -Text "Do not include passwords that never expire." -FontWeight bold -Color Blue } if ($Rule.IncludeName.Count -gt 0 -and $Rule.IncludeNameProperties.Count -gt 0) { New-HTMLListItem -Text "Apply naming rule to require that account contains of of names ", $($Rule.IncludeName -join ", "), " in at least one property ", ($Rule.IncludeNameProperties -join ", ") -FontWeight normal, bold, normal, bold, normal -Color None, Blue, None, Blue } else { New-HTMLListItem -Text "Do not apply special name rules" -Color Blue -FontWeight bold } if ($Rule.IncludeOU) { New-HTMLListItem -Text "Apply Organizational Unit inclusion on ", ($Rule.IncludeOU -join ", ") -FontWeight normal, bold -Color None, Blue } else { New-HTMLListItem -Text "Do not apply Organizational Unit limit" -Color Blue -FontWeight bold } if ($Rule.ExcludeOU) { New-HTMLListItem -Text "Apply Organizational Unit exclusion on ", $Rule.ExcludeOU -FontWeight normal, bold -Color None, Green } else { New-HTMLListItem -Text "Do not exclude any Organizational Unit" -Color Blue -FontWeight bold } if ($Rule.IncludeGroup) { New-HTMLListItem -Text "Appply Group Membership inclusion (direct only) ", ($Rule.IncludeGroup -join ", ") } else { New-HTMLListItem -Text "Do not apply Group Membership limit" } if ($Rule.ExcludeGroup) { New-HTMLListItem -Text "Apply Group Membership exclusion (direct only): ", ($Rule.ExcludeGroup -join ", ") } else { New-HTMLListItem -Text "Do not apply Group Membership exclusion" } New-HTMLListItem -Text "Send to manager" -NestedListItems { New-HTMLList { if ($Rule.SendToManager.Manager.Enable) { New-HTMLListItem -Text "Manager ", " is ", 'enabled' -FontWeight bold, normal, bold -Color None, None, Green { New-HTMLList { New-HTMLListItem -Text "Rules: " { New-HTMLList { if ($Rule.SendToManager.Manager.Reminders.Default.Enable) { if ($Rule.SendToManager.Manager.Reminders.Default.Reminder) { New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $($Rule.SendToManager.Manager.Reminders.Default.Reminder -join ", "), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green } else { New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $($Rule.Reminders -join ", "), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green } } else { New-HTMLListItem -Text "Default rule is ", "disabled" -FontWeight bold, bold -Color None, Red } if ($Rule.SendToManager.Manager.Reminders.OnDay.Enable) { New-HTMLListItem -Text @( "On day of the week ", "is ", "enabled" " on days: ", ($Rule.SendToManager.Manager.Reminders.OnDay.Days -join ", "), " with comparison ", $TranslateOperators[$Rule.SendToManager.Manager.Reminders.OnDay.ComparisonType], ' value ', $Rule.SendToManager.Manager.Reminders.OnDay.Reminder ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green } else { New-HTMLListItem -Text "On day of week rule is ", "disabled" -FontWeight bold, bold -Color None, Red } if ($Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Enable) { New-HTMLListItem -Text @( "On day of the month rule ", "is", " enabled", " on days ", ($Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Days -join ","), " with comparison ", $TranslateOperators[$Rule.SendToManager.Manager.Reminders.OnDayOfMonth.ComparisonType], ' value ', $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Reminder ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green } else { New-HTMLListItem -Text "On day of month rule is ", "disabled" -FontWeight bold, bold -Color None, Red } } } } } } else { New-HTMLListItem -Text "Manager ", " is ", 'disabled' -FontWeight bold, normal, bold -Color None, None, Red } if ($Rule.SendToManager.ManagerNotCompliant.Enable) { New-HTMLListItem -Text "Manager Escalation", " is ", 'enabled' -FontWeight bold, normal, bold -Color None, None, Green { New-HTMLList { New-HTMLListItem -Text "Manager Name: ", $Rule.SendToManager.ManagerNotCompliant.Manager.DisplayName -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "Manager Email Address: ", $Rule.SendToManager.ManagerNotCompliant.Manager.EmailAddress -FontWeight normal, bold -TextDecoration underline, none } New-HTMLList { New-HTMLListItem -Text "Rules: " { New-HTMLList { if ($Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Enable) { if ($Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Reminder) { New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $($Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Reminder -join ", "), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green } else { New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $($Rule.Reminders -join ", "), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green } } else { New-HTMLListItem -Text "Default rule is ", "disabled" -FontWeight bold, bold -Color None, Red } if ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Enable) { New-HTMLListItem -Text @( "On day of the week ", "is ", "enabled" " on days: ", ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Days -join ", "), " with comparison ", $TranslateOperators[$Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.ComparisonType], ' value ', $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Reminder ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green } else { New-HTMLListItem -Text "On day of week rule is ", "disabled" -FontWeight bold, bold -Color None, Red } if ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Enable) { New-HTMLListItem -Text @( "On day of the month rule ", "is", " enabled", " on days ", ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Days -join ", "), " with comparison ", $TranslateOperators[$Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.ComparisonType], ' value ', $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Reminder ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green } else { New-HTMLListItem -Text "On day of month rule is ", "disabled" -FontWeight bold, bold -Color None, Red } } } } } } else { New-HTMLListItem -Text "Manager Escalation", " is ", "disabled" -FontWeight bold, normal, bold -Color None, None, Red } if ($Rule.SendToManager.SecurityEscalation.Enable) { New-HTMLListItem -Text "Security Escalation ", "is", " enabled" -FontWeight bold, normal, bold -Color None, None, Green { New-HTMLList { New-HTMLListItem -Text "Manager Name: ", $Rule.SendToManager.SecurityEscalation.Manager.DisplayName -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "Manager Email Address: ", $Rule.SendToManager.SecurityEscalation.Manager.EmailAddress -FontWeight normal, bold -TextDecoration underline, none } New-HTMLList { New-HTMLListItem -Text "Rules: " { New-HTMLList { <# if ($Rule.SendToManager.SecurityEscalation.Reminders.Default.Enable) { 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) { if ($Report.NestedRules) { # nested rules view under single tab if ($Summary['Rules'].Keys.Count -gt 0) { New-HTMLTab -Name 'Rules Information' { foreach ($Rule in $Summary['Rules'].Keys) { if ((Measure-Object -InputObject $Summary['Rules'][$Rule].Values.User).Count -gt 0) { $Color = 'LawnGreen' $IconSolid = 'Star' } else { $Color = 'Salmon' $IconSolid = 'Stop' } New-HTMLTab -Name $Rule -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $Summary['Rules'][$Rule].Values.User -Filtering { New-TableCondition -Name 'Enabled' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string New-TableCondition -Name 'HasMailbox' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor BlueSmoke -Value $true -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Missing', 'Disabled' -BackgroundColor Salmon -Operator in New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Enabled' -BackgroundColor LawnGreen New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Not available' -BackgroundColor BlueSmoke } } } } } } else { foreach ($Rule in $Summary['Rules'].Keys) { if ((Measure-Object -InputObject $Summary['Rules'][$Rule].Values.User).Count -gt 0) { $Color = 'LawnGreen' $IconSolid = 'Star' } else { $Color = 'Salmon' $IconSolid = 'Stop' } New-HTMLTab -Name $Rule -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $Summary['Rules'][$Rule].Values.User -Filtering { New-TableCondition -Name 'Enabled' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string New-TableCondition -Name 'HasMailbox' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor BlueSmoke -Value $true -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Missing', 'Disabled' -BackgroundColor Salmon -Operator in New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Enabled' -BackgroundColor LawnGreen New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Not available' -BackgroundColor BlueSmoke } } } } } if ($Report.ShowUsersSent) { if ((Measure-Object -InputObject $SummaryUsersEmails).Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'Email sent to users' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $SummaryUsersEmails { New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string } -Filtering } } if ($Report.ShowManagersSent) { if ((Measure-Object -InputObject $SummaryManagersEmails).Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'Email sent to manager' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $SummaryManagersEmails { New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' } -Filtering } } if ($Report.ShowEscalationSent) { if ((Measure-Object -InputObject $SummaryEscalationEmails).Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'Email sent to Security' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $SummaryEscalationEmails { New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' } -Filtering } } if ($Report.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 'History Emails To Users' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $UsersSent { New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string } -Filtering } } if ($Report.ShowSearchManagers) { [Array] $ShowSearchManagers = $SummarySearch['EmailManagers'].Values #| ForEach-Object { if ($_ -ne $null) { $_ } } if ($ShowSearchManagers.Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'History Emails To Managers' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $ShowSearchManagers { New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' } -Filtering } } if ($Report.ShowSearchEscalations) { [Array] $ShowSearchEscalations = $SummarySearch['EmailEscalations'].Values #| ForEach-Object { if ($_ -ne $null) { $_ } } if ($ShowSearchEscalations.Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'History Email To Security' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $ShowSearchEscalations { New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' } -Filtering } } if ($Report.ShowSkippedUsers) { New-HTMLTab -Name 'Skipped Users' -IconSolid users { $SkippedUsers = foreach ($User in $AllSkipped.Values) { if ($User.Type -ne 'Contact') { $User } } New-HTMLPanel -AlignContentText center { New-HTMLText -FontSize 15pt -Text "Those users have no password date set. This means account running expiration checks doesn't have permissions or acccout never had password set or account is set to change password on logon. " } -Invisible New-HTMLTable -DataTable $SkippedUsers -Filtering { New-TableCondition -Name 'Enabled' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq New-TableCondition -Name 'HasMailbox' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor BlueSmoke -Value $true -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Missing', 'Disabled' -BackgroundColor Salmon -Operator in New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Enabled' -BackgroundColor LawnGreen New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Not available' -BackgroundColor BlueSmoke } } } if ($Report.ShowSkippedLocations) { New-HTMLTab -Name 'Skipped Locations' -IconSolid building { New-HTMLPanel -AlignContentText center { New-HTMLText -FontSize 15pt -Text "Users in those Organizational Units have no password date set. This means account running expiration checks doesn't have permissions or acccout never had password set or account is set to change password on logon. " } -Invisible New-HTMLTable -DataTable $Locations.Values -Filtering { New-TableHeader -ResponsiveOperations none -Names 'Names', 'NamesExpired' } } } } -ShowHTML:$Report.ShowHTML -FilePath $Report.FilePath -Online:$Report.Online -WarningAction $WarningAction -TitleText $Report.Title Write-Color -Text "[i]" , " Generating HTML report ", $Report.Title, ". Done" -Color White, Yellow, Green } function Send-PasswordAdminNotifications { [CmdletBinding()] param( $AdminSection, $TemplateAdmin, $TemplateAdminSubject, $TimeEnd, $EmailParameters, $HtmlAttachments, $Logging ) if ($AdminSection.Enable) { Write-Color -Text "[i] Sending summary information " -Color White, Yellow, White, Yellow, White, Yellow, White $CountSecurity = 0 [Array] $SummaryEmail = @( $CountSecurity++ # 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 $EmailSplat.EmailDateFormat = $Logging.EmailDateFormat $EmailSplat.EmailDateFormatUTCConversion = $Logging.EmailDateFormatUTCConversion if ($HtmlAttachments.Count -gt 0) { $EmailSplat.Attachments = $HtmlAttachments } Write-Color -Text "[i] Sending summary information ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow $EmailResult = Send-PasswordEmail @EmailSplat Write-Color -Text "[r] Sending summary information ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ") (status: ", $EmailResult.Status, " sent to: ", $EmailResult.SentTo, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow [PSCustomObject] @{ DisplayName = $ManagerUser.DisplayName SamAccountName = $ManagerUser.SamAccountName Domain = $ManagerUser.Domain Status = $EmailResult.Status StatusWhen = Get-Date -Format "yyyy-MM-dd HH:mm:ss" SentTo = $EmailResult.SentTo StatusError = $EmailResult.Error Template = 'Unknown' } ) Write-Color -Text "[i] Sending summary information (sent: ", $SummaryEmail.Count, ")" -Color White, Yellow, White, Yellow, White, Yellow, White } else { Write-Color -Text "[i] Sending summary information is ", "disabled!" -Color White, Yellow, DarkMagenta } } function Send-PasswordEmail { [CmdletBinding()] param( [scriptblock] $Template, [PSCustomObject] $User, [Array] $ManagedUsers, [Array] $ManagedUsersManagerNotCompliant, [Array] $SummaryUsersEmails, [Array] $SummaryManagersEmails, [Array] $SummaryEscalationEmails, [string] $TimeToProcess, [Array] $Attachments, [System.Collections.IDictionary] $EmailParameters, [string] $Subject, [string] $EmailDateFormat, [switch] $EmailDateFormatUTCConversion ) if ($Template) { if ($User.PasswordLastSet) { if ($EmailDateFormat) { if ($EmailDateFormatUTCConversion) { $PasswordLastSet = $User.PasswordLastSet.ToUniversalTime().ToString($EmailDateFormat) } else { $PasswordLastSet = $User.PasswordLastSet.ToString($EmailDateFormat) } } else { if ($EmailDateFormatUTCConversion) { $PasswordLastSet = $User.PasswordLastSet.ToUniversalTime() } else { $PasswordLastSet = $User.PasswordLastSet } } } else { $PasswordLastSet = $User.PasswordLastSet } if ($User.DateExpiry) { if ($EmailDateFormat) { if ($EmailDateFormatUTCConversion) { $ExpiryDate = $User.DateExpiry.ToUniversalTime().ToString($EmailDateFormat) } else { $ExpiryDate = $User.DateExpiry.ToString($EmailDateFormat) } } else { if ($EmailDateFormatUTCConversion) { $ExpiryDate = $User.DateExpiry.ToUniversalTime() } else { $ExpiryDate = $User.DateExpiry } } } else { $ExpiryDate = $User.DateExpiry } $SourceParameters = [ordered] @{ ManagerDisplayName = $User.DisplayName ManagerUsersTable = $ManagedUsers ManagerUsersTableManagerNotCompliant = $ManagedUsersManagerNotCompliant SummaryEscalationEmails = $SummaryEscalationEmails SummaryManagersEmails = $SummaryManagersEmails SummaryUsersEmails = $SummaryUsersEmails TimeToProcess = $TimeToProcess # 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 = $ExpiryDate # : DaysToExpire = $User.DaysToExpire # : PasswordExpired = $User.PasswordExpired # : False PasswordLastSet = $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 = @() } try { Send-EmailMessage @EmailParameters -ErrorAction Stop -WarningAction SilentlyContinue } catch { if ($_.Exception.Message -like "*Credential*") { Write-Color -Text "[e] " , "Failed to send email to $($EmailParameters.EmailParameters) because error: $($_.Exception.Message)" -Color Yellow, White, Red Write-Color -Text "[i] " , "Please make sure you have valid credentials in your configuration file (graph encryption issue?)" -Color Yellow, White, Red } else { Write-Color -Text "[e] " , "Failed to send email to $($EmailParameters.EmailParameters) because error: $($_.Exception.Message)" -Color Yellow, White, Red } } } } function Send-PasswordManagerNofifications { [CmdletBinding()] param( $ManagerSection, $Summary, $CachedUsers, $TemplateManager, $TemplateManagerSubject, $TemplateManagerNotCompliant, $TemplateManagerNotCompliantSubject, $EmailParameters, $Logging ) 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 $EmailSplat.EmailDateFormat = $Logging.EmailDateFormat $EmailSplat.EmailDateFormatUTCConversion = $Logging.EmailDateFormatUTCConversion if ($ManagerSection.SendToDefaultEmail -ne $true) { $EmailSplat.EmailParameters.To = $ManagerUser.EmailAddress } else { $EmailSplat.EmailParameters.To = $ManagerSection.DefaultEmail } if ($Logging.NotifyOnManagerSend) { Write-Color -Text "[i] Sending notifications to managers ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } $EmailResult = Send-PasswordEmail @EmailSplat if ($Logging.NotifyOnManagerSend) { 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 $SummaryManagersEmails } else { Write-Color -Text "[i] Sending notifications to managers is ", "disabled!" -Color White, Yellow, DarkMagenta } } function Send-PasswordSecurityNotifications { [CmdletBinding()] param( $SecuritySection, $Summary, $TemplateSecurity, $TemplateSecuritySubject, $Logging ) if ($SecuritySection.Enable) { Write-Color -Text "[i] Sending notifications to security " -Color White, Yellow, White, Yellow, White, Yellow, White $CountSecurity = 0 [Array] $SummaryEscalationEmails = foreach ($Manager in $Summary['NotifySecurity'].Keys) { $CountSecurity++ # 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 $EmailSplat.EmailDateFormat = $Logging.EmailDateFormat $EmailSplat.EmailDateFormatUTCConversion = $Logging.EmailDateFormatUTCConversion if ($SecuritySection.SendToDefaultEmail -ne $true) { $EmailSplat.EmailParameters.To = $ManagerUser.EmailAddress } else { $EmailSplat.EmailParameters.To = $SecuritySection.DefaultEmail } if ($Logging.NotifyOnSecuritySend) { Write-Color -Text "[i] Sending notifications to security ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } $EmailResult = Send-PasswordEmail @EmailSplat if ($Logging.NotifyOnSecuritySend) { Write-Color -Text "[r] Sending notifications to security ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ") (status: ", $EmailResult.Status, " sent to: ", $EmailResult.SentTo, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } [PSCustomObject] @{ DisplayName = $ManagerUser.DisplayName SamAccountName = $ManagerUser.SamAccountName Domain = $ManagerUser.Domain Status = $EmailResult.Status StatusWhen = Get-Date -Format "yyyy-MM-dd HH:mm:ss" SentTo = $EmailResult.SentTo StatusError = $EmailResult.Error Accounts = $ManagedUsers.SamAccountName AccountsCount = $ManagedUsers.Count Template = 'Unknown' # 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 $SummaryEscalationEmails } else { Write-Color -Text "[i] Sending notifications to security is ", "disabled!" -Color White, Yellow, DarkMagenta } } function Send-PasswordUserNofifications { [CmdletBinding()] param( $UserSection, $Summary, $Logging, $TemplatePreExpiry, $TemplatePreExpirySubject, $TemplatePostExpiry, $TemplatePostExpirySubject, $EmailParameters ) if ($UserSection.Enable) { Write-Color -Text "[i] Sending notifications to users " -Color White, Yellow, White, Yellow, White, Yellow, White $CountUsers = 0 [Array] $SummaryUsersEmails = foreach ($Notify in $Summary['Notify'].Values) { $CountUsers++ $User = $Notify.User $Rule = $Notify.Rule # 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 ($DaysToExpire days)' } } else { if ($Notify.Rule.TemplatePostExpiry) { $EmailSplat.Template = $Notify.Rule.TemplatePostExpiry } elseif ($TemplatePostExpiry) { $EmailSplat.Template = $TemplatePostExpiry } else { $EmailSplat.Template = { } } if ($Notify.Rule.TemplatePostExpirySubject) { $EmailSplat.Subject = $Notify.Rule.TemplatePostExpirySubject } elseif ($TemplatePostExpirySubject) { $EmailSplat.Subject = $TemplatePostExpirySubject } else { $EmailSplat.Subject = '[Password] Your password expired on $DateExpiry ($DaysToExpire days ago)' } } $EmailSplat.User = $Notify.User $EmailSplat.EmailParameters = $EmailParameters $EmailSplat.EmailDateFormat = $Logging.EmailDateFormat $EmailSplat.EmailDateFormatUTCConversion = $Logging.EmailDateFormatUTCConversion if ($UserSection.SendToDefaultEmail -ne $true) { $EmailSplat.EmailParameters.To = $Notify.User.EmailAddress } else { $EmailSplat.EmailParameters.To = $UserSection.DefaultEmail } if ($Notify.User.EmailAddress -like "*@*") { # 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 $SummaryUsersEmails } else { Write-Color -Text "[i] Sending notifications to users is ", "disabled!" -Color White, Yellow, DarkMagenta } } function Set-LoggingCapabilities { [CmdletBinding()] param( [string] $LogPath, [int] $LogMaximum, [switch] $ShowTime, [string] $TimeFormat ) $Script:PSDefaultParameterValues = @{ "Write-Color:LogFile" = $LogPath "Write-Color:ShowTime" = if ($PSBoundParameters.ContainsKey('ShowTime')) { $ShowTime.IsPresent } else { $null } "Write-Color:TimeFormat" = $TimeFormat } Remove-EmptyValue -Hashtable $Script:PSDefaultParameterValues if ($LogPath) { $FolderPath = [io.path]::GetDirectoryName($LogPath) if (-not (Test-Path -LiteralPath $FolderPath)) { $null = New-Item -Path $FolderPath -ItemType Directory -Force -WhatIf:$false } if ($LogMaximum -gt 0) { $CurrentLogs = Get-ChildItem -LiteralPath $FolderPath | Sort-Object -Property CreationTime -Descending | Select-Object -Skip $LogMaximum if ($CurrentLogs) { Write-Color -Text '[i] ', "Logs directory has more than ", $LogMaximum, " log files. Cleanup required..." -Color Yellow, DarkCyan, Red, DarkCyan foreach ($Log in $CurrentLogs) { try { Remove-Item -LiteralPath $Log.FullName -Confirm:$false -WhatIf:$false Write-Color -Text '[+] ', "Deleted ", "$($Log.FullName)" -Color Yellow, White, Green } catch { Write-Color -Text '[-] ', "Couldn't delete log file $($Log.FullName). Error: ', "$($_.Exception.Message) -Color Yellow, White, Red } } } } else { Write-Color -Text '[i] ', "LogMaximum is set to 0 (Unlimited). No log files will be deleted." -Color Yellow, DarkCyan } } } function Set-PasswordConfiguration { [CmdletBinding()] param( [System.Collections.IDictionary] $Logging, [scriptblock] $ConfigurationDSL, [scriptblock] $TemplatePreExpiry, [string] $TemplatePreExpirySubject, [scriptblock] $TemplatePostExpiry, [string] $TemplatePostExpirySubject, [scriptblock] $TemplateManager, [string] $TemplateManagerSubject, [scriptblock] $TemplateSecurity, [string] $TemplateSecuritySubject, [scriptblock] $TemplateManagerNotCompliant, [string] $TemplateManagerNotCompliantSubject, [scriptblock] $TemplateAdmin, [string] $TemplateAdminSubject, [System.Collections.IDictionary] $EmailParameters, [System.Collections.IDictionary] $UserSection, [System.Collections.IDictionary] $ManagerSection, [System.Collections.IDictionary] $SecuritySection, [System.Collections.IDictionary] $AdminSection, [Array] $HTMLReports, [Array] $Rules, [string] $SearchPath, [string] $OverwriteEmailProperty, [string] $OverwriteManagerProperty ) if (-not $Rules) { $Rules = @() # not worth the effort for generic list } if (-not $HTMLReports) { $HTMLReports = @() # not worth the effort for generic list } if ($ConfigurationDSL) { try { $ConfigurationExecuted = & $ConfigurationDSL foreach ($Configuration in $ConfigurationExecuted) { if ($Configuration.Type -eq 'PasswordConfigurationOption') { if ($Configuration.Settings.SearchPath) { $SearchPath = $Configuration.Settings.SearchPath } elseif ($Configuration.Settings.OverwriteEmailProperty) { $OverwriteEmailProperty = $Configuration.Settings.OverwriteEmailProperty } elseif ($Configuration.Settings.OverwriteManagerProperty) { $OverwriteManagerProperty = $Configuration.Settings.OverwriteManagerProperty } foreach ($Setting in $Configuration.Settings.Keys) { if ($Setting -notin 'SearchPath', 'OverwriteEmailProperty', 'OverwriteManagerProperty') { $Logging[$Setting] = $Configuration.Settings[$Setting] } } } elseif ($Configuration.Type -eq 'PasswordConfigurationEmail') { $EmailParameters = $Configuration.Settings } elseif ($Configuration.Type -eq 'PasswordConfigurationTypeUser') { $UserSection = $Configuration.Settings } elseif ($Configuration.Type -eq 'PasswordConfigurationTypeManager') { $ManagerSection = $Configuration.Settings } elseif ($Configuration.Type -eq 'PasswordConfigurationTypeSecurity') { $SecuritySection = $Configuration.Settings } elseif ($Configuration.Type -eq 'PasswordConfigurationTypeAdmin') { $AdminSection = $Configuration.Settings } elseif ($Configuration.Type -eq 'PasswordConfigurationReport') { $HTMLReports += $Configuration.Settings } elseif ($Configuration.Type -eq 'PasswordConfigurationRule') { $Rules += $Configuration.Settings } elseif ($Configuration.Type -eq "PasswordConfigurationTemplatePreExpiry") { $TemplatePreExpiry = $Configuration.Settings.Template $TemplatePreExpirySubject = $Configuration.Settings.Subject } elseif ($Configuration.Type -eq "PasswordConfigurationTemplatePostExpiry") { $TemplatePostExpiry = $Configuration.Settings.Template $TemplatePostExpirySubject = $Configuration.Settings.Subject } elseif ($Configuration.Type -eq "PasswordConfigurationTemplateManager") { $TemplateManager = $Configuration.Settings.Template $TemplateManagerSubject = $Configuration.Settings.Subject } elseif ($Configuration.Type -eq "PasswordConfigurationTemplateSecurity") { $TemplateSecurity = $Configuration.Settings.Template $TemplateSecuritySubject = $Configuration.Settings.Subject } elseif ($Configuration.Type -eq "PasswordConfigurationTemplateManagerNotCompliant") { $TemplateManagerNotCompliant = $Configuration.Settings.Template $TemplateManagerNotCompliantSubject = $Configuration.Settings.Subject } elseif ($Configuration.Type -eq "PasswordConfigurationTemplateAdmin") { $TemplateAdmin = $Configuration.Settings.Template $TemplateAdminSubject = $Configuration.Settings.Subject } } } catch { Write-Color -Text "[e]", " Processing configuration failed because of error in line ", $_.InvocationInfo.ScriptLineNumber, " in ", $_.InvocationInfo.InvocationName, " with message: ", $_.Exception.Message -Color Yellow, White, Red return } } if (-not $TemplatePreExpiry) { Write-Color -Text "[i]", " TemplatePreExpiry not defined. Using default template (built-in)" -Color Yellow, Red $TemplatePreExpiry = { EmailText -LineBreak EmailText -Text "Dear ", "$DisplayName," -LineBreak EmailText -Text "Your password will expire in $DaysToExpire days and if you do not change it, you will not be able to connect to the Network and IT services. " EmailText -Text "Depending on your situation, please follow one of the methods below to change your password." -LineBreak EmailText -Text "If you are connected to the Internal Network (either directly or through VPN):" EmailList { EmailListItem -Text "Press CTRL+ALT+DEL" EmailListItem -Text "Choose Change password" EmailListItem -Text "Type in your old password and then type the new one according to the password policy (twice)" EmailListItem -Text "After the change is complete you will be prompted with information that the password has been changed" } EmailText -Text "If you are not connected to the Internal Network:" EmailList { EmailListItem -Text "Open [Password Change Link](https://account.activedirectory.windowsazure.com/ChangePassword.aspx) using your web browser" EmailListItem -Text "Login using your current credentials" EmailListItem -Text "On the change password form, type your old password and the new password that you want to set (twice)" EmailListItem -Text "Click Submit" } EmailText -Text "Please also remember to modify your password on the email configuration of your Smartphone or Tablet." -LineBreak EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } } if (-not $TemplatePreExpirySubject) { Write-Color -Text "[i]", " TemplatePreExpirySubject not defined. Using default template (built-in)" -Color Yellow, Red $TemplatePreExpirySubject = '[Password Expiring] Your password will expire on $DateExpiry ($DaysToExpire days)' } if (-not $TemplatePostExpiry) { Write-Color -Text "[i]", " TemplatePostExpiry not defined. Using default template (built-in)" -Color Yellow, Red # Template to user when sending email to user after password expires $TemplatePostExpiry = { EmailText -LineBreak EmailText -Text "Dear ", "$DisplayName," -LineBreak EmailText -Text "Your password already expired on $PasswordLastSet. If you do not change it, you will not be able to connect to the Network and IT services. " EmailText -Text "Depending on your situation, please follow one of the methods below to change your password." -LineBreak EmailText -Text "If you are connected to the Network (either directly or through VPN):" EmailList { EmailListItem -Text "Press CTRL+ALT+DEL" EmailListItem -Text "Choose Change password" EmailListItem -Text "Type in your old password and then type the new one according to the password policy (twice)" EmailListItem -Text "After the change is complete you will be prompted with information that the password has been changed" } EmailText -Text "If you are not connected to the Internal Network:" EmailList { EmailListItem -Text "Open [Password Change Link](https://account.activedirectory.windowsazure.com/ChangePassword.aspx) using your web browser" EmailListItem -Text "Login using your current credentials" EmailListItem -Text "On the change password form, type your old password and the new password that you want to set (twice)" EmailListItem -Text "Click Submit" } EmailText -Text "Please also remember to modify your password on the email configuration of your Smartphone or Tablet." -LineBreak EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } } if (-not $TemplatePostExpirySubject) { Write-Color -Text "[i]", " TemplatePostExpirySubject not defined. Using default template (built-in)" -Color Yellow, Red $TemplatePostExpirySubject = '[Password Expired] Your password expired on $DateExpiry ($DaysToExpire days ago)' } # Template to security team with all service accounts that have expired passwords and password never expires set to true if (-not $TemplateSecurity) { Write-Color -Text "[i]", " TemplateSecurity not defined. Using default template (built-in)" -Color Yellow, Red $TemplateSecurity = { EmailText -LineBreak EmailText -Text "Hello ", "$ManagerDisplayName", "," -LineBreak -FontWeight normal, bold, normal EmailText -Text @( "Below is a summary of ", "all service accounts", " where the passwords have exceeded the time limit stipulated in the password policy. These accounts are all in violation of the policy and immediate action/escalation should take place." ) -LineBreak -FontWeight normal, bold, normal EmailText -Text "It has been agreed that the ", "password never expires", " flag has been set to ", "true", " to avoid business disruption/loss of service. As a result we require your escalation to the managers of the account to take immediate action to change the password ASAP." -LineBreak -FontWeight normal, bold, normal, bold, normal EmailText -Text "Numerous automated reminders have been sent to the Manager, but no response/action has been taken yet." -LineBreak EmailText -Text "Please reach out directly to the manager/site to ensure that these passwords are changed immediately." -LineBreak EmailText -Text "If there is still lack of responses/action taken, it will be in your (IT Security) discretion to disable the account(s) question and take any appropriate action." -LineBreak -FontWeight bold EmailTable -DataTable $ManagerUsersTable -HideFooter EmailText -LineBreak EmailText -Text "Many thanks in advance." -LineBreak EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } } if (-not $TemplateSecuritySubject) { Write-Color -Text "[i]", " TemplateSecuritySubject not defined. Using default template (built-in)" -Color Yellow, Red $TemplateSecuritySubject = "[Passsword Expired] Following accounts are expired!" } if (-not $TemplateManager) { Write-Color -Text "[i]", " TemplateManager not defined. Using default template (built-in)" -Color Yellow, Red $TemplateManager = { EmailText -LineBreak EmailText -Text "Hello $ManagerDisplayName," -LineBreak EmailText -Text "Below is a summary of accounts where the password is due to expire soon. These accounts are either:" EmailList { EmailListItem -Text 'Managed by you' EmailListItem -Text 'You are the manager of the owner of these accounts.' } EmailText -Text "Where you are the owner, please action the password change on each account outlined below, according to the rules specified by Password Policy." -LineBreak EmailTable -DataTable $ManagerUsersTable -HideFooter EmailText -LineBreak EmailText -Text @( "Please note that for Service Accounts, even though the ", "'password never expires' " "flag remains set to " "'true' " ", the password MUST be changed before the expiry date specified in the above table. " "It is the responsibility of the manager of the account to ensure that this takes place. " ) -FontWeight normal, bold, normal, bold, normal, normal -LineBreak EmailText -Text @( "Please make an effort " "to change password yourself using known methods rather than asking the Service Desk to change the password for you. " "If password is changed by Service Desk agent, there are at least 2 people knowing the password - Service Desk Agent and You! " "Do you really want the Service Desk agent to know the password to critical system you manage/own? " "Be responsible!" ) -FontWeight bold, normal, normal, normal, bold -LineBreak -Color None, None, None, None, Red EmailText -Text "One of the ways to change the password is: " -FontWeight bold EmailList { EmailListItem -Text "Press CTRL+ALT+DEL" EmailListItem -Text "Choose Change password" EmailListItem -Text "In the account name - change it to the account you want to change password for." -FontWeight bold EmailListItem -Text "Type in current password for the account and then type the new one according to the rules specified in the password policy." EmailListItem -Text "After the change is complete you will be provided with information that the password has been changed" } EmailText -Text "Failure to take action could result in loss of service/escalation to the IT Security team." -LineBreak -FontWeight bold EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } } if (-not $TemplateManagerSubject) { Write-Color -Text "[i]", " TemplateManagerSubject not defined. Using default template (built-in)" -Color Yellow, Red $TemplateManagerSubject = "[Passsword Expiring] Accounts you manage/own are expiring or already expired" } # Template to Service Desk with information about manager missing, disabled, last logon >90 days, missing email for service accounts if (-not $TemplateManagerNotCompliant) { $TemplateManagerNotCompliant = { EmailText -LineBreak EmailText -Text "Hello $ManagerDisplayName," -LineBreak EmailText -Text "Below is a summary of accounts where there is missing 'critical' information. These accounts are either:" EmailList { EmailListItem -Text "Missing a Manager in the AD - please add an active manager" EmailListItem -Text "The Manager in AD is Disabled - please add an active manager" EmailListItem -Text "Manager Last logon >90 days - please confirm if the manager is still an employee/change the manager to an active manager" EmailListItem -Text "Manager is missing email - add manager email" } EmailText -Text "Please contact the respective local IT Service Desk (outlined in the below table) to update this Manager's attributes in the AD directly. The suggested action to take can be found in the below table." -LineBreak EmailTable -DataTable $ManagerUsersTableManagerNotCompliant -HideFooter EmailText -LineBreak EmailText -Text "Kind regards," -LineBreak EmailText -Text "IT Service Desk" -LineBreak } } if (-not $TemplateManagerNotCompliantSubject) { $TemplateManagerNotCompliantSubject = "[Password Escalation] Accounts are expiring with non-compliant manager" } if (-not $TemplateAdmin) { # Template to Admins with information summarizing what happened $TemplateAdmin = { EmailText -LineBreak EmailText -Text "Hello $ManagerDisplayName," -LineBreak EmailText -Text "Here's the summary of password notifications:" EmailList { EmailListItem -Text "Found users matching rule to send emails: ", $SummaryUsersEmails.Count EmailListItem -Text "Sent emails to users: ", ($SummaryUsersEmails | Where-Object { $_.Status -eq $true }).Count EmailListItem -Text "Couldn't send emails because of no email: ", ($SummaryUsersEmails | Where-Object { $_.Status -eq $false -and $_.StatusError -eq 'No email address for user' }).Count EmailListItem -Text "Couldn't send emails because other reasons: ", ($SummaryUsersEmails | Where-Object { $_.Status -eq $false -and $_.StatusError -ne 'No email address for user' }).Count EmailListItem -Text "Sent emails to managers: ", $SummaryManagersEmails.Count EmailListItem -Text "Sent emails to security: ", $SummaryEscalationEmails.Count } EmailText -Text "It took ", $TimeToProcess , " seconds to process the template." -LineBreak EmailText -Text "Hope everything works correctly! " -LineBreak EmailText -Text "Kind regards," -LineBreak EmailText -Text "IT Service Desk" -LineBreak } if (-not $TemplateAdminSubject) { $TemplateAdminSubject = '[Password Summary] Passwords summary' } } # Lets return information to the caller $OutputInformation = [ordered] @{ EmailParameters = $EmailParameters UserSection = $UserSection ManagerSection = $ManagerSection SecuritySection = $SecuritySection AdminSection = $AdminSection HTMLReports = $HTMLReports Rules = $Rules SearchPath = $SearchPath OverwriteEmailProperty = $OverwriteEmailProperty OverwriteManagerProperty = $OverwriteManagerProperty Logging = $Logging TemplatePreExpiry = $TemplatePreExpiry TemplatePreExpirySubject = $TemplatePreExpirySubject TemplatePostExpiry = $TemplatePostExpiry TemplatePostExpirySubject = $TemplatePostExpirySubject TemplateManager = $TemplateManager TemplateManagerSubject = $TemplateManagerSubject TemplateSecurity = $TemplateSecurity TemplateSecuritySubject = $TemplateSecuritySubject TemplateManagerNotCompliant = $TemplateManagerNotCompliant TemplateManagerNotCompliantSubject = $TemplateManagerNotCompliantSubject TemplateAdmin = $TemplateAdmin TemplateAdminSubject = $TemplateAdminSubject } $OutputInformation } function Find-Password { <# .SYNOPSIS Scan Active Directory forest for all users and their password expiration date .DESCRIPTION Scan Active Directory forest for all users and their password expiration date .PARAMETER Forest Target different Forest, by default current forest is used .PARAMETER ExcludeDomains Exclude domain from search, by default whole forest is scanned .PARAMETER IncludeDomains Include only specific domains, by default whole forest is scanned .PARAMETER ExtendedForestInformation Ability to provide Forest Information from another command to speed up processing .PARAMETER OverwriteEmailProperty Overwrite EmailAddress property with different property name .PARAMETER OverwriteManagerProperty Overwrite Manager property with different property name. Can use DistinguishedName or SamAccountName .PARAMETER RulesProperties Add additional properties to be returned from rules .EXAMPLE Find-Password | ft .NOTES General notes #> [CmdletBinding()] param( [alias('ForestName')][string] $Forest, [string[]] $ExcludeDomains, [alias('Domain', 'Domains')][string[]] $IncludeDomains, [System.Collections.IDictionary] $ExtendedForestInformation, [string] $OverwriteEmailProperty, [Parameter(DontShow)][switch] $AsHashTable, [Parameter(DontShow)][string] $HashtableField = 'DistinguishedName', [ValidateSet('Users', 'Contacts')][string[]] $ReturnObjectsType = @('Users', 'Contacts'), [Parameter(DontShow)][switch] $AsHashTableObject, [Parameter(DontShow)][string[]] $AddEmptyProperties = @(), [Parameter(DontShow)][string[]] $RulesProperties, [string] $OverwriteManagerProperty ) $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 } if ($OverwriteManagerProperty) { $OverwriteManagerProperty } 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 in their inital form to make sure it's speedy gonzales when querying for Managers if (-not $Cache) { $Cache = [ordered] @{ } } # We're caching all processed users to make sure it's easier later on to find users if (-not $CachedUsers) { $CachedUsers = [ordered] @{ } } Write-Color -Text '[i] ', "Discovering forest information" -Color Yellow, White $ForestInformation = Get-WinADForestDetails -PreferWritable -Extended -Forest $Forest -ExcludeDomains $ExcludeDomains -IncludeDomains $IncludeDomains -ExtendedForestInformation $ExtendedForestInformation # 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 # SAmAccountName will overwrite itself when we have multiple domains and there are duplicates # but sicne we use only on in case manager is used in special fields such as extensionAttribute, it shouldn't affect much $Cache[$User.SamAccountName] = $User } if ($ReturnObjectsType -contains 'Contacts') { [Array] $Contacts = foreach ($Domain in $ForestInformation.Domains) { Write-Color -Text "[i] ", "Discovering DC for domain ", "$($Domain)", " in forest ", $ForestInformation.Name -Color Yellow, White, Yellow, White $Server = $ForestInformation['QueryServers'][$Domain]['HostName'][0] Write-Color -Text "[i] ", "Getting contacts from ", "$($Domain)", " using ", $Server -Color Yellow, White, Yellow, White try { Get-ADObject -LDAPFilter "objectClass=Contact" -Server $Server -Properties $PropertiesContacts -ErrorAction Stop } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Color '[e] Error: ', $ErrorMessage -Color White, Red } } foreach ($Contact in $Contacts) { $Cache[$Contact.DistinguishedName] = $Contact } } Write-Color -Text "[i] ", "Preparing all users for password expirations in forest ", $Forest.Name -Color Yellow, White, Yellow, White $CountUsers = 0 foreach ($User in $Users) { $CountUsers++ Write-Verbose -Message "Processing $($User.DisplayName) - $($CountUsers)/$($Users.Count)" $DateExpiry = $null $DaysToExpire = $null $PasswordDays = $null $PasswordNeverExpires = $null $PasswordAtNextLogon = $null $HasMailbox = $null # This is a special case for users that have a manager in a special field such as extensionAttributes # This is useful for service accounts or other accounts that don't have a manager in AD if ($OverwriteManagerProperty) { # fix this for a user $ManagerTemp = $User.$OverwriteManagerProperty if ($ManagerTemp) { $ManagerSpecial = $Cache[$ManagerTemp] } else { $ManagerSpecial = $null } } else { $ManagerSpecial = $null } if ($ManagerSpecial) { # We have manager in different field such as extensionAttribute $Manager = $ManagerSpecial.DisplayName $ManagerSamAccountName = $ManagerSpecial.SamAccountName $ManagerDisplayName = $ManagerSpecial.DisplayName $ManagerEmail = $ManagerSpecial.Mail $ManagerEnabled = $ManagerSpecial.Enabled $ManagerLastLogon = $ManagerSpecial.LastLogonDate if ($ManagerLastLogon) { $ManagerLastLogonDays = $( - $($ManagerLastLogon - $Today).Days) } else { $ManagerLastLogonDays = $null } $ManagerType = $ManagerSpecial.ObjectClass } elseif ($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 $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 New-PasswordConfigurationEmail { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [alias('SmtpServer')][string] $Server, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [int] $Port, [Parameter(Mandatory, ParameterSetName = 'SecureString')] [Parameter(Mandatory, ParameterSetName = 'oAuth')] [Parameter(Mandatory, ParameterSetName = 'Graph')] [Parameter(Mandatory, ParameterSetName = 'MgGraphRequest')] [Parameter(Mandatory, ParameterSetName = 'Compatibility')] [Parameter(Mandatory, ParameterSetName = 'SendGrid')] [object] $From, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [Parameter(ParameterSetName = 'Compatibility')] [Parameter(ParameterSetName = 'SendGrid')] [string] $ReplyTo, # [Parameter(ParameterSetName = 'SecureString')] # [Parameter(ParameterSetName = 'oAuth')] # [Parameter(ParameterSetName = 'Graph')] # [Parameter(ParameterSetName = 'MgGraphRequest')] # [Parameter(ParameterSetName = 'Compatibility')] # [Parameter(ParameterSetName = 'SendGrid')] # [string[]] $Cc, # [Parameter(ParameterSetName = 'SecureString')] # [Parameter(ParameterSetName = 'oAuth')] # [Parameter(ParameterSetName = 'Graph')] # [Parameter(ParameterSetName = 'MgGraphRequest')] # [Parameter(ParameterSetName = 'Compatibility')] # [Parameter(ParameterSetName = 'SendGrid')] # [string[]] $Bcc, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [Parameter(ParameterSetName = 'Compatibility')] [Parameter(ParameterSetName = 'SendGrid')] [alias('Importance')][ValidateSet('Low', 'Normal', 'High')][string] $Priority, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [ValidateSet('None', 'OnSuccess', 'OnFailure', 'Delay', 'Never')][string[]] $DeliveryNotificationOption, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [MailKit.Net.Smtp.DeliveryStatusNotificationType] $DeliveryStatusNotificationType, [Parameter(ParameterSetName = 'oAuth')] [Parameter(Mandatory, ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'Compatibility')] [Parameter(Mandatory, ParameterSetName = 'SendGrid')] [pscredential] $Credential, [Parameter(ParameterSetName = 'SecureString')] [string] $Username, [Parameter(ParameterSetName = 'SecureString')] [string] $Password, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [MailKit.Security.SecureSocketOptions] $SecureSocketOptions, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [switch] $UseSsl, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [switch] $SkipCertificateRevocation, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [switch] $SkipCertificateValidatation, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [int] $Timeout, [Parameter(ParameterSetName = 'oAuth')] [alias('oAuth')][switch] $oAuth2, [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [switch] $RequestReadReceipt, [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [switch] $RequestDeliveryReceipt, [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [switch] $Graph, [Parameter(ParameterSetName = 'MgGraphRequest')] [switch] $MgGraphRequest, [Parameter(ParameterSetName = 'SecureString')] [switch] $AsSecureString, [Parameter(ParameterSetName = 'SendGrid')] [switch] $SendGrid, [Parameter(ParameterSetName = 'SendGrid')] [switch] $SeparateTo, [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [switch] $DoNotSaveToSentItems, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [string] $LocalDomain ) $Output = [ordered] @{ Type = 'PasswordConfigurationEmail' Settings = [ordered] @{ Server = if ($PSBoundParameters.ContainsKey('Server')) { $Server } else { $null } Port = if ($PSBoundParameters.ContainsKey('Port')) { $Port } else { $null } From = if ($PSBoundParameters.ContainsKey('From')) { $From } else { $null } ReplyTo = if ($PSBoundParameters.ContainsKey('ReplyTo')) { $ReplyTo } else { $null } #Cc = if ($PSBoundParameters.ContainsKey('Cc')) { $Cc } else { $null } #Bcc = if ($PSBoundParameters.ContainsKey('Bcc')) { $Bcc } else { $null } Priority = if ($PSBoundParameters.ContainsKey('Priority')) { $Priority } else { $null } DeliveryNotificationOption = if ($PSBoundParameters.ContainsKey('DeliveryNotificationOption')) { $DeliveryNotificationOption } else { $null } DeliveryStatusNotificationType = if ($PSBoundParameters.ContainsKey('DeliveryStatusNotificationType')) { $DeliveryStatusNotificationType } else { $null } Credential = if ($PSBoundParameters.ContainsKey('Credential')) { $Credential } else { $null } Username = if ($PSBoundParameters.ContainsKey('Username')) { $Username } else { $null } Password = if ($PSBoundParameters.ContainsKey('Password')) { $Password } else { $null } SecureSocketOptions = if ($PSBoundParameters.ContainsKey('SecureSocketOptions')) { $SecureSocketOptions } else { $null } UseSsl = if ($PSBoundParameters.ContainsKey('UseSsl')) { $UseSsl } else { $null } SkipCertificateRevocation = if ($PSBoundParameters.ContainsKey('SkipCertificateRevocation')) { $SkipCertificateRevocation } else { $null } SkipCertificateValidatation = if ($PSBoundParameters.ContainsKey('SkipCertificateValidatation')) { $SkipCertificateValidatation } else { $null } Timeout = if ($PSBoundParameters.ContainsKey('Timeout')) { $Timeout } else { $null } oAuth2 = if ($PSBoundParameters.ContainsKey('oAuth2')) { $oAuth2 } else { $null } RequestReadReceipt = if ($PSBoundParameters.ContainsKey('RequestReadReceipt')) { $RequestReadReceipt } else { $null } RequestDeliveryReceipt = if ($PSBoundParameters.ContainsKey('RequestDeliveryReceipt')) { $RequestDeliveryReceipt } else { $null } Graph = if ($PSBoundParameters.ContainsKey('Graph')) { $Graph } else { $null } MgGraphRequest = if ($PSBoundParameters.ContainsKey('MgGraphRequest')) { $MgGraphRequest } else { $null } AsSecureString = if ($PSBoundParameters.ContainsKey('AsSecureString')) { $AsSecureString } else { $null } SendGrid = if ($PSBoundParameters.ContainsKey('SendGrid')) { $SendGrid } else { $null } SeparateTo = if ($PSBoundParameters.ContainsKey('SeparateTo')) { $SeparateTo } else { $null } DoNotSaveToSentItems = if ($PSBoundParameters.ContainsKey('DoNotSaveToSentItems')) { $DoNotSaveToSentItems } else { $null } WhatIf = $WhatIfPreference } } Remove-EmptyValue -Hashtable $Output.Settings $Output } function New-PasswordConfigurationOption { [CmdletBinding()] param( [switch] $ShowTime , #= $true [string] $LogFile , #= "$PSScriptRoot\Logs\PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).log" [string] $TimeFormat , #= "yyyy-MM-dd HH:mm:ss" [int] $LogMaximum , #= 365 [switch] $NotifyOnSkipUserManagerOnly , #= $false [switch] $NotifyOnSecuritySend , #= $true [switch] $NotifyOnManagerSend , #= $true [switch] $NotifyOnUserSend , #= $true [switch] $NotifyOnUserMatchingRule , #= $true [switch] $NotifyOnUserDaysToExpireNull , #= $true [string] $SearchPath, [string] $EmailDateFormat, [switch] $EmailDateFormatUTCConversion, [string] $OverwriteEmailProperty, [string] $OverwriteManagerProperty ) $Output = [ordered] @{ Type = "PasswordConfigurationOption" Settings = [ordered] @{ ShowTime = $ShowTime.IsPresent LogFile = $LogFile TimeFormat = $TimeFormat LogMaximum = $LogMaximum NotifyOnSkipUserManagerOnly = $NotifyOnSkipUserManagerOnly.IsPresent NotifyOnSecuritySend = $NotifyOnSecuritySend.IsPresent NotifyOnManagerSend = $NotifyOnManagerSend.IsPresent NotifyOnUserSend = $NotifyOnUserSend.IsPresent NotifyOnUserMatchingRule = $NotifyOnUserMatchingRule.IsPresent NotifyOnUserDaysToExpireNull = $NotifyOnUserDaysToExpireNull.IsPresent SearchPath = $SearchPath # conversion for DateExpiry/PasswordLastSet only EmailDateFormat = $EmailDateFormat EmailDateFormatUTCConversion = $EmailDateFormatUTCConversion.IsPresent # email property conversion (global) OverwriteEmailProperty = $OverwriteEmailProperty # manager property conversion (global) OverwriteManagerProperty = $OverwriteManagerProperty } } Remove-EmptyValue -Hashtable $Output.Settings $Output } function New-PasswordConfigurationReport { <# .SYNOPSIS Provides HTML report configuration for Password Notifications in Password Solution. .DESCRIPTION Provides HTML report configuration for Password Notifications in Password Solution. The New-PasswordConfigurationReport function generates configuration for HTML report. .PARAMETER Enable Specifies whether to enable the report generation. The default value is $false. .PARAMETER ShowHTML Specifies whether to display the report in HTML format right after it's generated in default browser. The default value is $false. .PARAMETER Title Specifies the title of the report. The default value is "Password Solution Summary". .PARAMETER Online Specifies whether to generate the report using CDN for CSS and JS scripts, or use it locally. It doesn't require internet connectivity during generation. Makes the final output 3MB smaller. The default value is $false. .PARAMETER DisableWarnings Specifies whether to disable warning messages during report generation. The default value is $false. .PARAMETER ShowConfiguration Specifies whether to display the current Password Solution configuration settings. The default value is $false. .PARAMETER ShowAllUsers Specifies whether to display information about all user accounts. The default value is $false. .PARAMETER ShowRules Specifies whether to display information from the rules. The default value is $false. .PARAMETER ShowUsersSent Specifies whether to display information about users who have received (or not) password expiry notifications. The default value is $false. .PARAMETER ShowManagersSent Specifies whether to display information about managers who have received password expiry notifications. The default value is $false. .PARAMETER ShowEscalationSent Specifies whether to display information about escalation contacts who have received password expiry notifications. The default value is $false. .PARAMETER ShowSkippedUsers Specifies whether to display information about users who were during password expiry notifications because of inability to asses their expiration date. The default value is $false. .PARAMETER ShowSkippedLocations Specifies whether to display information about locations where skipped users are located. The default value is $false. .PARAMETER ShowSearchUsers Specifies whether to display information for searching who got password expiry notifications. The default value is $false. .PARAMETER ShowSearchManagers Specifies whether to display information for searching who got password expiry notifications and for which accounts from managers. The default value is $false. .PARAMETER ShowSearchEscalations Specifies whether to display information for searching who got password escalation notifications and what's the status of that message. The default value is $false. .PARAMETER FilePath Specifies the file path for the report .PARAMETER AttachToEmail Specifies whether to attach the report to an administrative email. The default value is $false. .PARAMETER NestedRules Specifies whether to display nested password rules. Each rule has it's own tab with output. Having many rules and all other settings enabled can result in a very long list of tabs that's hard to navigate. This setting forces separate tab for all rules. The default value is $false. .OUTPUTS The function returns an ordered dictionary that contains the report settings. .EXAMPLE New-PasswordConfigurationReport -ShowHTML -Title "Password Configuration Report" -FilePath "C:\Reports\PasswordReport.html" .EXAMPLE $Date = Get-Date $Report = [ordered] @{ Enable = $true ShowHTML = $true Title = "Password Solution Summary" Online = $true DisableWarnings = $true ShowConfiguration = $true ShowAllUsers = $true ShowRules = $true ShowUsersSent = $true ShowManagersSent = $true ShowEscalationSent = $true ShowSkippedUsers = $true ShowSkippedLocations = $true ShowSearchUsers = $true ShowSearchManagers = $true ShowSearchEscalations = $true NestedRules = $false FilePath = "$PSScriptRoot\Reporting\PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).html" AttachToEmail = $true } New-PasswordConfigurationReport @Report #> [CmdletBinding()] param( [switch] $Enable, [switch] $ShowHTML, [string] $Title, [switch] $Online, [switch] $DisableWarnings, [switch] $ShowConfiguration, [switch] $ShowAllUsers, [switch] $ShowRules, [switch] $ShowUsersSent, [switch] $ShowManagersSent, [switch] $ShowEscalationSent, [switch] $ShowSkippedUsers, [switch] $ShowSkippedLocations, [switch] $ShowSearchUsers, [switch] $ShowSearchManagers, [switch] $ShowSearchEscalations , [string] $FilePath, [switch] $AttachToEmail, [switch] $NestedRules ) $Output = [ordered] @{ Type = "PasswordConfigurationReport" Settings = [ordered] @{ Enable = $Enable.IsPresent ShowHTML = $ShowHTML.IsPresent Title = $Title Online = $Online.IsPresent DisableWarnings = $DisableWarnings.IsPresent ShowConfiguration = $ShowConfiguration.IsPresent ShowAllUsers = $ShowAllUsers.IsPresent ShowRules = $ShowRules.IsPresent ShowUsersSent = $ShowUsersSent.IsPresent ShowManagersSent = $ShowManagersSent.IsPresent ShowEscalationSent = $ShowEscalationSent.IsPresent ShowSkippedUsers = $ShowSkippedUsers.IsPresent ShowSkippedLocations = $ShowSkippedLocations.IsPresent ShowSearchUsers = $ShowSearchUsers.IsPresent ShowSearchManagers = $ShowSearchManagers.IsPresent ShowSearchEscalations = $ShowSearchEscalations.IsPresent FilePath = $FilePath AttachToEmail = $AttachToEmail.IsPresent } } $Output } function New-PasswordConfigurationRule { <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER ReminderConfiguration Parameter description .PARAMETER Name Parameter description .PARAMETER Enable Parameter description .PARAMETER IncludeExpiring Parameter description .PARAMETER IncludePasswordNeverExpires Parameter description .PARAMETER PasswordNeverExpiresDays Parameter description .PARAMETER IncludeName Include user in rule if any of the properties match the value of Name in the properties defined in IncludeNameProperties .PARAMETER IncludeNameProperties Include user in rule if any of the properties match the value as defined in IncludeName .PARAMETER ExcludeName Exclude user from rule if any of the properties match the value of Name in the properties defined in ExcludeNameProperties .PARAMETER ExcludeNameProperties Exclude user from rule if any of the properties match the value as defined in ExcludeName .PARAMETER IncludeOU Parameter description .PARAMETER ExcludeOU Parameter description .PARAMETER IncludeGroup Parameter description .PARAMETER ExcludeGroup Parameter description .PARAMETER ReminderDays Parameter description .PARAMETER ManagerReminder Parameter description .PARAMETER ManagerNotCompliant Parameter description .PARAMETER ManagerNotCompliantDisplayName Parameter description .PARAMETER ManagerNotCompliantEmailAddress Parameter description .PARAMETER ManagerNotCompliantDisabled Parameter description .PARAMETER ManagerNotCompliantMissing Parameter description .PARAMETER ManagerNotCompliantMissingEmail Parameter description .PARAMETER ManagerNotCompliantLastLogonDays Parameter description .PARAMETER SecurityEscalation Parameter description .PARAMETER SecurityEscalationDisplayName Parameter description .PARAMETER SecurityEscalationEmailAddress Parameter description .PARAMETER OverwriteEmailProperty Parameter description .PARAMETER OverwriteManagerProperty Parameter description .PARAMETER ProcessManagersOnly This parameters is used to process users, but only managers will be notified. Sending emails to users within the rule will be skipped completly. This is useful if users would have email addresses, that would normally trigger an email to them. .EXAMPLE An example .NOTES General notes #> [CmdletBinding()] param( [scriptblock] $ReminderConfiguration, [parameter(Mandatory)][string] $Name, [switch] $Enable, [switch] $IncludeExpiring, [switch] $IncludePasswordNeverExpires, [nullable[int]]$PasswordNeverExpiresDays, [string[]] $IncludeNameProperties, [string[]] $IncludeName, [string[]] $ExcludeNameProperties, [string[]] $ExcludeName, [string[]] $IncludeOU, [string[]] $ExcludeOU, [string[]] $IncludeGroup, [string[]] $ExcludeGroup, [parameter(Mandatory)][alias('ExpirationDays', 'Days')][Array] $ReminderDays, [switch] $ManagerReminder, [switch] $ManagerNotCompliant, [string] $ManagerNotCompliantDisplayName, [string] $ManagerNotCompliantEmailAddress, [switch] $ManagerNotCompliantDisabled, [switch] $ManagerNotCompliantMissing, [switch]$ManagerNotCompliantMissingEmail, [nullable[int]] $ManagerNotCompliantLastLogonDays, [switch] $SecurityEscalation, [string] $SecurityEscalationDisplayName, [string] $SecurityEscalationEmailAddress, [string] $OverwriteEmailProperty, [string] $OverwriteManagerProperty, [switch] $ProcessManagersOnly ) $Output = [ordered] @{ Name = $Name Enable = $Enable.IsPresent IncludeExpiring = $IncludeExpiring.IsPresent IncludePasswordNeverExpires = $IncludePasswordNeverExpires.IsPresent Reminders = $ReminderDays PasswordNeverExpiresDays = $PasswordNeverExpiresDays IncludeNameProperties = $IncludeNameProperties IncludeName = $IncludeName IncludeOU = $IncludeOU ExcludeOU = $ExcludeOU SendToManager = [ordered] @{} ProcessManagersOnly = $ProcessManagersOnly.IsPresent OverwriteEmailProperty = $OverwriteEmailProperty # properties to overwrite manager based on different field OverwriteManagerProperty = $OverwriteManagerProperty } $Output.SendToManager['Manager'] = [ordered] @{ Enable = $false Reminders = [ordered] @{} } $Output.SendToManager['ManagerNotCompliant'] = [ordered] @{ Enable = $false Manager = [ordered] @{ DisplayName = $ManagerNotCompliantDisplayName EmailAddress = $ManagerNotCompliantEmailAddress } Disabled = $ManagerNotCompliantDisabled Missing = $ManagerNotCompliantMissing MissingEmail = $ManagerNotCompliantMissingEmail LastLogon = if ($PSBoundParameters.ContainsKey('ManagerNotCompliantLastLogonDays')) { $true } else { $false } LastLogonDays = $ManagerNotCompliantLastLogonDays Reminders = [ordered] @{ } } $Output.SendToManager['SecurityEscalation'] = [ordered] @{ Enable = $false Manager = [ordered] @{ DisplayName = $SecurityEscalationDisplayName EmailAddress = $SecurityEscalationEmailAddress } Reminders = [ordered] @{} } if ($ManagerReminder) { $Output.SendToManager['Manager'].Enable = $true } if ($ManagerNotCompliant) { $Output.SendToManager['ManagerNotCompliant'].Enable = $true } if ($SecurityEscalation) { $Output.SendToManager['SecurityEscalation'].Enable = $true } if ($ReminderConfiguration) { try { $RemindersExecution = & $ReminderConfiguration } catch { Write-Color -Text "[e]", " Processing rule ", $Output.Name, " failed because of error: ", $_.Exception.Message -Color Yellow, White, Red return } foreach ($Reminder in $RemindersExecution) { if ($Reminder.Type -eq 'Manager') { foreach ($ReminderReminders in $Reminder.Reminders) { $Output.SendToManager['Manager'].Reminders += $ReminderReminders } } elseif ($Reminder.Type -eq 'ManagerNotCompliant') { foreach ($ReminderReminders in $Reminder.Reminders) { $Output.SendToManager['ManagerNotCompliant'].Reminders += $ReminderReminders } } elseif ($Reminder.Type -eq 'Security') { foreach ($ReminderReminders in $Reminder.Reminders) { $Output.SendToManager['SecurityEscalation'].Reminders += $ReminderReminders } } else { # Should not happen throw "Invalid reminder type: $($Reminder.Type)" } } } Remove-EmptyValue -Hashtable $Output -Recursive -Rerun 2 $Configuration = [ordered] @{ Type = 'PasswordConfigurationRule' Settings = $Output } $Configuration } function New-PasswordConfigurationRuleReminder { [CmdletBinding(DefaultParameterSetName = 'Daily')] param( [Parameter(Mandatory, ParameterSetName = 'Daily')] [Parameter(Mandatory, ParameterSetName = 'DayOfWeek')] [Parameter(Mandatory, ParameterSetName = 'DayOfMonth')] [ValidateSet('Manager', 'ManagerNotCompliant', 'Security')][string] $Type, [Parameter(Mandatory, ParameterSetName = 'DayOfWeek')] [Parameter(Mandatory, ParameterSetName = 'DayOfMonth')] [alias('ConditionDays', 'Days')][Array] $ExpirationDays, [Parameter(Mandatory, ParameterSetName = 'DayOfWeek')] [ValidateSet( 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' )][Array] $DayOfWeek, [Parameter(Mandatory, ParameterSetName = 'DayOfMonth')] [Array] $DayOfMonth, [Parameter(ParameterSetName = 'DayOfWeek')] [Parameter(ParameterSetName = 'DayOfMonth')] [ValidateSet('lt', 'gt', 'eq', 'in')][string] $ComparisonType ) # if user didn't choose anything, lets set the default if (-not $ComparisonType) { $ComparisonType = 'eq' } if ($PSCmdlet.ParameterSetName -eq 'Daily') { $Reminders = [ordered] @{ Type = $Type Reminders = @{ Default = [ordered] @{ Enable = $true } } } } elseif ($PSCmdlet.ParameterSetName -eq 'DayOfWeek') { $Reminders = [ordered] @{ Type = $Type Reminders = @{ OnDayOfWeek = [ordered] @{ Enable = $true Reminder = $ExpirationDays ComparisonType = $ComparisonType Days = $DayOfWeek } } } } elseif ($PSCmdlet.ParameterSetName -eq 'DayOfMonth') { $Reminders = [ordered] @{ Type = $Type Reminders = @{ OnDayOfMonth = [ordered] @{ Enable = $true Reminder = $ExpirationDays ComparisonType = $ComparisonType Days = $DayOfMonth } } } } $Reminders } function New-PasswordConfigurationTemplate { [CmdletBinding()] param( [parameter(Mandatory)][ScriptBlock] $Template, [parameter(Mandatory)][string] $Subject, [parameter(Mandatory)][ValidateSet('PreExpiry', 'PostExpiry', 'Manager', 'ManagerNotCompliant', 'Security', 'Admin')] $Type ) $Output = [ordered] @{ Type = "PasswordConfigurationTemplate$Type" Settings = [ordered] @{ Template = $Template Subject = $Subject } } $Output } function New-PasswordConfigurationType { [CmdletBinding()] param( [Parameter(Mandatory)][ValidateSet('User', 'Manager', 'Security', 'Admin')][string] $Type, [switch] $Enable, [int] $SendCountMaximum, [string] $DefaultEmail, [switch] $AttachCSV ) $Output = [ordered] @{ Type = "PasswordConfigurationType$Type" Settings = @{ Enable = $Enable.IsPresent SendCountMaximum = $SendCountMaximum SendToDefaultEmail = if ($DefaultEmail) { $true } else { $false } DefaultEmail = $DefaultEmail OverwriteEmailProperty = $OverwriteEmailProperty AttachCSV = $AttachCSV.IsPresent } } $Output } function Show-PasswordQuality { <# .SYNOPSIS Creates an HTML report showing password quality for all user objects in Active Directory. .DESCRIPTION Creates an HTML report showing password quality for all user objects in Active Directory. This comman utilizes DSInternals PowerShell module to get the data. Then it uses PSWriteHTML to create nice looking report. .PARAMETER FilePath Path to the file where report will be saved. .PARAMETER DontShow If specified, report will not be opened in a browser. .PARAMETER Online If specified report will use CDN for JS and CSS files. If not specified, it will merge all CSS and JS files into one HTML file. This makes the file at least 3MB bigger, even if there is very small amount of data. Keep in mind that this report can be created without internet access, just that opening it in a browser with -Online switch will require internet access. .PARAMETER WeakPasswords List of weak passwords that should be checked for. Provide a list of common passwords that you want to check for, and that your users may have used. .PARAMETER 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 -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 ConfigurationDSL Parameter description .PARAMETER EmailParameters Parameters for Email. Uses Mailozaurr splatting behind the scenes, so it supports all options that Mailozaurr does. .PARAMETER OverwriteEmailProperty Property responsible for overwriting the default email field in Active Directory. Useful when the password notification has to go somewhere else than users email address. .PARAMETER UserSection Parameter description .PARAMETER ManagerSection Parameter description .PARAMETER SecuritySection Parameter description .PARAMETER AdminSection Parameter description .PARAMETER Rules Parameter description .PARAMETER TemplatePreExpiry Parameter description .PARAMETER TemplatePreExpirySubject Parameter description .PARAMETER TemplatePostExpiry Parameter description .PARAMETER TemplatePostExpirySubject Parameter description .PARAMETER TemplateManager Parameter description .PARAMETER TemplateManagerSubject Parameter description .PARAMETER TemplateSecurity Parameter description .PARAMETER TemplateSecuritySubject Parameter description .PARAMETER TemplateManagerNotCompliant Parameter description .PARAMETER TemplateManagerNotCompliantSubject Parameter description .PARAMETER TemplateAdmin Parameter description .PARAMETER TemplateAdminSubject Parameter description .PARAMETER Logging Parameter description .PARAMETER HTMLReports Parameter description .PARAMETER SearchPath Parameter description .EXAMPLE An example .NOTES General notes #> [CmdletBinding(DefaultParameterSetName = 'DSL')] param( [Parameter(ParameterSetName = 'DSL', Position = 0)][scriptblock] $ConfigurationDSL, [Parameter(Mandatory, ParameterSetName = 'Legacy')][System.Collections.IDictionary] $EmailParameters, [Parameter(ParameterSetName = 'Legacy')][string] $OverwriteEmailProperty, [Parameter(ParameterSetName = 'Legacy')][string] $OverwriteManagerProperty, [Parameter(Mandatory, ParameterSetName = 'Legacy')][System.Collections.IDictionary] $UserSection, [Parameter(Mandatory, ParameterSetName = 'Legacy')][System.Collections.IDictionary] $ManagerSection, [Parameter(Mandatory, ParameterSetName = 'Legacy')][System.Collections.IDictionary] $SecuritySection, [Parameter(Mandatory, ParameterSetName = 'Legacy')][System.Collections.IDictionary] $AdminSection, [Parameter(Mandatory, ParameterSetName = 'Legacy')][Array] $Rules, [Parameter(ParameterSetName = 'Legacy')][scriptblock] $TemplatePreExpiry, [Parameter(ParameterSetName = 'Legacy')][string] $TemplatePreExpirySubject, [Parameter(ParameterSetName = 'Legacy')][scriptblock] $TemplatePostExpiry, [Parameter(ParameterSetName = 'Legacy')][string] $TemplatePostExpirySubject, [Parameter(Mandatory, ParameterSetName = 'Legacy')][scriptblock] $TemplateManager, [Parameter(Mandatory, ParameterSetName = 'Legacy')][string] $TemplateManagerSubject, [Parameter(Mandatory, ParameterSetName = 'Legacy')][scriptblock] $TemplateSecurity, [Parameter(Mandatory, ParameterSetName = 'Legacy')][string] $TemplateSecuritySubject, [Parameter(Mandatory, ParameterSetName = 'Legacy')][scriptblock] $TemplateManagerNotCompliant, [Parameter(Mandatory, ParameterSetName = 'Legacy')][string] $TemplateManagerNotCompliantSubject, [Parameter(Mandatory, ParameterSetName = 'Legacy')][scriptblock] $TemplateAdmin, [Parameter(Mandatory, ParameterSetName = 'Legacy')][string] $TemplateAdminSubject, [Parameter(ParameterSetName = 'Legacy')][System.Collections.IDictionary] $Logging = @{}, [Parameter(ParameterSetName = 'Legacy')][Array] $HTMLReports, [Parameter(ParameterSetName = 'Legacy')][string] $SearchPath ) $TimeStart = Start-TimeLog $Script:Reporting = [ordered] @{} $Script:Reporting['Version'] = Get-GitHubVersion -Cmdlet 'Start-PasswordSolution' -RepositoryOwner 'evotecit' -RepositoryName 'PasswordSolution' Write-Color -Text '[i]', "[PasswordSolution] ", 'Version', ' [Informative] ', $Script:Reporting['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta $TodayDate = Get-Date $Today = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Set-LoggingCapabilities -LogPath $Logging.LogFile -LogMaximum $Logging.LogMaximum -ShowTime:$Logging.ShowTime -TimeFormat $Logging.TimeFormat # since the first entry didn't go to log file, this will Write-Color -Text '[i]', "[PasswordSolution] ", 'Version', ' [Informative] ', $Script:Reporting['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta -NoConsoleOutput $Summary = [ordered] @{} $Summary['Notify'] = [ordered] @{} $Summary['NotifyManager'] = [ordered] @{} $Summary['NotifySecurity'] = [ordered] @{} $Summary['Rules'] = [ordered] @{} $AllSkipped = [ordered] @{} $Locations = [ordered] @{} # lets try to get configuration, and if not provided do some defaults $SplatPasswordConfiguration = [ordered] @{ ConfigurationDSL = $ConfigurationDSL EmailParameters = $EmailParameters OverwriteEmailProperty = $OverwriteEmailProperty OverwriteManagerProperty = $OverwriteManagerProperty UserSection = $UserSection ManagerSection = $ManagerSection SecuritySection = $SecuritySection AdminSection = $AdminSection Rules = $Rules TemplatePreExpiry = $TemplatePreExpiry TemplatePreExpirySubject = $TemplatePreExpirySubject TemplatePostExpiry = $TemplatePostExpiry TemplatePostExpirySubject = $TemplatePostExpirySubject TemplateManager = $TemplateManager TemplateManagerSubject = $TemplateManagerSubject TemplateSecurity = $TemplateSecurity TemplateSecuritySubject = $TemplateSecuritySubject TemplateManagerNotCompliant = $TemplateManagerNotCompliant TemplateManagerNotCompliantSubject = $TemplateManagerNotCompliantSubject TemplateAdmin = $TemplateAdmin TemplateAdminSubject = $TemplateAdminSubject Logging = $Logging HTMLReports = $HTMLReports SearchPath = $SearchPath } $InitialVariables = Set-PasswordConfiguration @SplatPasswordConfiguration if (-not $InitialVariables) { return } $EmailParameters = $InitialVariables.EmailParameters $OverwriteEmailProperty = $InitialVariables.OverwriteEmailProperty $OverwriteManagerProperty = $InitialVariables.OverwriteManagerProperty $UserSection = $InitialVariables.UserSection $ManagerSection = $InitialVariables.ManagerSection $SecuritySection = $InitialVariables.SecuritySection $AdminSection = $InitialVariables.AdminSection $Rules = $InitialVariables.Rules $TemplatePreExpiry = $InitialVariables.TemplatePreExpiry $TemplatePreExpirySubject = $InitialVariables.TemplatePreExpirySubject $TemplatePostExpiry = $InitialVariables.TemplatePostExpiry $TemplatePostExpirySubject = $InitialVariables.TemplatePostExpirySubject $TemplateManager = $InitialVariables.TemplateManager $TemplateManagerSubject = $InitialVariables.TemplateManagerSubject $TemplateSecurity = $InitialVariables.TemplateSecurity $TemplateSecuritySubject = $InitialVariables.TemplateSecuritySubject $TemplateManagerNotCompliant = $InitialVariables.TemplateManagerNotCompliant $TemplateManagerNotCompliantSubject = $InitialVariables.TemplateManagerNotCompliantSubject $TemplateAdmin = $InitialVariables.TemplateAdmin $TemplateAdminSubject = $InitialVariables.TemplateAdminSubject $Logging = $InitialVariables.Logging $HTMLReports = $InitialVariables.HTMLReports $SearchPath = $InitialVariables.SearchPath # this is to get properties from rules to be used in building up user output [Array] $ExtendedProperties = foreach ($Rule in $Rules ) { if ($Rule.OverwriteEmailProperty) { $Rule.OverwriteEmailProperty } if ($Rule.OverwriteManagerProperty) { $Rule.OverwriteManagerProperty } } $SummarySearch = Import-SearchInformation -SearchPath $SearchPath Write-Color -Text "[i]", " Starting process to find expiring users" -Color Yellow, White, Green, White, Green, White, Green, White $CachedUsers = Find-Password -AsHashTable -OverwriteEmailProperty $OverwriteEmailProperty -RulesProperties $ExtendedProperties -OverwriteManagerProperty $OverwriteManagerProperty if ($Rules.Count -eq 0) { Write-Color -Text "[e]", " No rules found. Please add some rules to configuration" -Color Yellow, White, Red return } foreach ($Rule in $Rules) { $SplatProcessingRule = [ordered] @{ Rule = $Rule Summary = $Summary CachedUsers = $CachedUsers AllSkipped = $AllSkipped Locations = $Locations Loggin = $Logging TodayDate = $TodayDate } Invoke-PasswordRuleProcessing @SplatProcessingRule } $SplatUserNotifications = [ordered] @{ UserSection = $UserSection Summary = $Summary Logging = $Logging TemplatePreExpiry = $TemplatePreExpiry TemplatePreExpirySubject = $TemplatePreExpirySubject TemplatePostExpiry = $TemplatePostExpiry TemplatePostExpirySubject = $TemplatePostExpirySubject EmailParameter = $EmailParameters } [Array] $SummaryUsersEmails = Send-PasswordUserNofifications @SplatUserNotifications $SplatManagerNotifications = [ordered] @{ ManagerSection = $ManagerSection Summary = $Summary CachedUsers = $CachedUsers TemplateManager = $TemplateManager TemplateManagerSubject = $TemplateManagerSubject TemplateManagerNotCompliant = $TemplateManagerNotCompliant TemplateManagerNotCompliantSubject = $TemplateManagerNotCompliantSubject EmailParameters = $EmailParameters Loggin = $Logging } [Array] $SummaryManagersEmails = Send-PasswordManagerNofifications @SplatManagerNotifications $SplatSecurityNotifications = [ordered] @{ SecuritySection = $SecuritySection Summary = $Summary TemplateSecurity = $TemplateSecurity TemplateSecuritySubject = $TemplateSecuritySubject Logging = $Logging } [Array] $SummaryEscalationEmails = Send-PasswordSecurityNotifications @SplatSecurityNotifications $TimeEnd = Stop-TimeLog -Time $TimeStart -Option OneLiner Export-SearchInformation -SearchPath $SearchPath -SummarySearch $SummarySearch -Today $Today -SummaryUsersEmails $SummaryUsersEmails -SummaryManagersEmails $SummaryManagersEmails -SummaryEscalationEmails $SummaryEscalationEmails $HtmlAttachments = [System.Collections.Generic.List[string]]::new() foreach ($Report in $HTMLReports) { if ($Report.Enable) { $ReportSettings = @{ Report = $Report EmailParameters = $EmailParameters Logging = $Logging #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 } } } } $AdminSplat = [ordered] @{ AdminSection = $AdminSection TemplateAdmin = $TemplateAdmin TemplateAdminSubject = $TemplateAdminSubject TimeEnd = $TimeEnd EmailParameters = $EmailParameters HtmlAttachment = $HtmlAttachments } Send-PasswordAdminNotifications @AdminSplat } # Export functions and aliases as required Export-ModuleMember -Function @('Find-Password', 'Find-PasswordNotification', 'Find-PasswordQuality', 'New-PasswordConfigurationEmail', 'New-PasswordConfigurationOption', 'New-PasswordConfigurationReport', 'New-PasswordConfigurationRule', 'New-PasswordConfigurationRuleReminder', 'New-PasswordConfigurationTemplate', 'New-PasswordConfigurationType', 'Show-PasswordQuality', 'Start-PasswordSolution') -Alias @() # SIG # Begin signature block # MIInPgYJKoZIhvcNAQcCoIInLzCCJysCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDPLJDEa7LIuRZv # BV6toCy71J0pLRemaFdWFUD2CXfZr6CCITcwggO3MIICn6ADAgECAhAM5+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 # SIb3DQEJBDEiBCBIOiPIffZL7TxXmdUgY9eJuc1vtXcSD65NSu7+t9uDuDANBgkq # hkiG9w0BAQEFAASCAQAkL4x9R799lIzTXGGMuafrUUOYiluyY/00JLsY6UCm4Dpk # DdZepztqsVZZoilxV9SRaNHQbfRmqA5EMEuxl2R4bZE9KsSVOI3uOGIB/Zrg61+Z # VM2M04owRQr6htNgUaJnYeIWj4vdJqnjKVHaWwjQxR3qeaLNuFnwpl78aA6F/cj3 # KxdAmmZubP+QMjiQXuehXbNs8rjflzv5noWmZjT3VopJoGuLPYPy8ryMrw+asbKN # aIbnPkpFnFtqPKqAQUKk02LvviRQX3/+kvp6nxhUBbDKLy62DYd5wU1My6eCRZdd # ptV6r9FiSpeFQ8Q5pjdS4Ch/JNnUwqAX2vsrOhacoYIDIDCCAxwGCSqGSIb3DQEJ # BjGCAw0wggMJAgEBMHcwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0 # LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hB # MjU2IFRpbWVTdGFtcGluZyBDQQIQDE1pckuU+jwqSj0pB4A9WjANBglghkgBZQME # AgEFAKBpMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X # DTIzMDYwMTA5NTEzM1owLwYJKoZIhvcNAQkEMSIEIABYGRf4JTVC7+o0Vp16O5sH # FBZTwMfcvhcq238UKlUOMA0GCSqGSIb3DQEBAQUABIICAI2cE/cqfOvM3MiiOlqt # 6bNWxnH/7fS8OpWsviVe6xzjykAQbADCeoGZTmy5DfkhBIF5Y+xET/wo+qh/XgMd # Po0U83CEch0gWfMuXz0K27WQqT6iur4udr59n44ZWEOX94d3OM/65sZvtqtB3zLA # /eVWkjLcpvC4eq+EWF4JIAvEFBGeNZ4AHDxvRJF3zOs3ilJvO6than7xu/kA0hIE # Ch3pNZD+USLy2mgNOxK47uPjMSD7rFfrrXGeLPsaVqbAzBp4+UzBQ1qP8Ozfwni3 # 42YkLreQsE0OzuMW46vg4khHjtNh+QfQg9QrlclmWxIvO2WfFF3VG6IpuBgkAxNc # QUCPE9JzVJ8veVI4ggcwicZgyYk3S+Q7tO+/LyCIlyfe3WjwES/xXxcbx6kGuUC5 # feNmBu7xsF7A61Bp3npcA+PoBuqWOV864+bjVKfau520wDAVqSPU8iRaK6wWWr9W # KLYoO3VJpCj0AszPwXmNCSuaxDGcUQ6gciSWvQkrCB8PGsUihYX0rUtzSS2hXq7S # MCNhEU7tk5kyN2WZralqnlKB0LHwhsRGPtXfze/Hrwt/tKeRLkP/Qx4mWEatkxz5 # 7ucMdBzo1bhaQ0CUSeaPiQQVNX1daQyPSExXRiM+h718H7zcHvqw7+ZoY5Qvg05t # tJAa48A0Up0S6e/Xna/ASQau # SIG # End signature block |