PasswordSolution.psm1
function Convert-ADSchemaToGuid { <# .SYNOPSIS Converts name of schema properties to guids .DESCRIPTION Converts name of schema properties to guids .PARAMETER SchemaName Schema Name to convert to guid .PARAMETER All Get hashtable of all schema properties and their guids .PARAMETER Domain Domain to query. By default the current domain is used .PARAMETER RootDSE RootDSE to query. By default RootDSE is queried from the domain .PARAMETER AsString Return the guid as a string .EXAMPLE Convert-ADSchemaToGuid -SchemaName 'ms-Exch-MSO-Forward-Sync-Cookie' .EXAMPLE Convert-ADSchemaToGuid -SchemaName 'ms-Exch-MSO-Forward-Sync-Cookie' -AsString .NOTES General notes #> [CmdletBinding()] param( [string] $SchemaName, [string] $Domain, [Microsoft.ActiveDirectory.Management.ADEntity] $RootDSE, [switch] $AsString ) if (-not $Script:ADGuidMap -or -not $Script:ADGuidMapString) { if ($RootDSE) { $Script:RootDSE = $RootDSE } elseif (-not $Script:RootDSE) { if ($Domain) { $Script:RootDSE = Get-ADRootDSE -Server $Domain } else { $Script:RootDSE = Get-ADRootDSE } } $DomainCN = ConvertFrom-DistinguishedName -DistinguishedName $Script:RootDSE.defaultNamingContext -ToDomainCN $QueryServer = (Get-ADDomainController -DomainName $DomainCN -Discover -ErrorAction Stop).Hostname[0] $Script:ADGuidMap = [ordered] @{ 'All' = [System.GUID]'00000000-0000-0000-0000-000000000000' } $Script:ADGuidMapString = [ordered] @{ 'All' = '00000000-0000-0000-0000-000000000000' } Write-Verbose "Convert-ADSchemaToGuid - Querying Schema from $QueryServer" $Time = [System.Diagnostics.Stopwatch]::StartNew() if (-not $Script:StandardRights) { $Script:StandardRights = Get-ADObject -SearchBase $Script:RootDSE.schemaNamingContext -LDAPFilter "(schemaidguid=*)" -Properties name, lDAPDisplayName, schemaIDGUID -Server $QueryServer -ErrorAction Stop | Select-Object name, lDAPDisplayName, schemaIDGUID } foreach ($Guid in $Script:StandardRights) { $Script:ADGuidMapString[$Guid.lDAPDisplayName] = ([System.GUID]$Guid.schemaIDGUID).Guid $Script:ADGuidMapString[$Guid.Name] = ([System.GUID]$Guid.schemaIDGUID).Guid $Script:ADGuidMap[$Guid.lDAPDisplayName] = ([System.GUID]$Guid.schemaIDGUID) $Script:ADGuidMap[$Guid.Name] = ([System.GUID]$Guid.schemaIDGUID) } $Time.Stop() $TimeToExecute = "$($Time.Elapsed.Days) days, $($Time.Elapsed.Hours) hours, $($Time.Elapsed.Minutes) minutes, $($Time.Elapsed.Seconds) seconds, $($Time.Elapsed.Milliseconds) milliseconds" Write-Verbose "Convert-ADSchemaToGuid - Querying Schema from $QueryServer took $TimeToExecute" Write-Verbose "Convert-ADSchemaToGuid - Querying Extended Rights from $QueryServer" $Time = [System.Diagnostics.Stopwatch]::StartNew() if (-not $Script:ExtendedRightsGuids) { $Script:ExtendedRightsGuids = Get-ADObject -SearchBase $Script:RootDSE.ConfigurationNamingContext -LDAPFilter "(&(objectclass=controlAccessRight)(rightsguid=*))" -Properties name, displayName, lDAPDisplayName, rightsGuid -Server $QueryServer -ErrorAction Stop | Select-Object name, displayName, lDAPDisplayName, rightsGuid } foreach ($Guid in $Script:ExtendedRightsGuids) { $Script:ADGuidMapString[$Guid.Name] = ([System.GUID]$Guid.RightsGuid).Guid $Script:ADGuidMapString[$Guid.DisplayName] = ([System.GUID]$Guid.RightsGuid).Guid $Script:ADGuidMap[$Guid.Name] = ([System.GUID]$Guid.RightsGuid) $Script:ADGuidMap[$Guid.DisplayName] = ([System.GUID]$Guid.RightsGuid) } $Time.Stop() $TimeToExecute = "$($Time.Elapsed.Days) days, $($Time.Elapsed.Hours) hours, $($Time.Elapsed.Minutes) minutes, $($Time.Elapsed.Seconds) seconds, $($Time.Elapsed.Milliseconds) milliseconds" Write-Verbose "Convert-ADSchemaToGuid - Querying Extended Rights from $QueryServer took $TimeToExecute" } if ($SchemaName) { if ($AsString) { return $Script:ADGuidMapString[$SchemaName] } else { return $Script:ADGuidMap[$SchemaName] } } else { if ($AsString) { $Script:ADGuidMapString } else { $Script:ADGuidMap } } } function Convert-CountryCodeToCountry { <# .SYNOPSIS Converts a country code to a country name, or when used with a switch to full culture information .DESCRIPTION Converts a country code to a country name, or when used with a switch to full culture information .PARAMETER CountryCode Country code .PARAMETER All Provide full culture information rather than just the country name .EXAMPLE Convert-CountryCodeToCountry -CountryCode 'PL' .EXAMPLE Convert-CountryCodeToCountry -CountryCode 'PL' -All .EXAMPLE $Test = Convert-CountryCodeToCountry $Test['PL']['Culture'] | fl $Test['PL']['RegionInformation'] .EXAMPLE Convert-CountryCodeToCountry -CountryCode 'PL' Convert-CountryCodeToCountry -CountryCode 'POL' .NOTES General notes #> [cmdletBinding()] param( [string] $CountryCode, [switch] $All ) if ($Script:QuickSearch) { if ($PSBoundParameters.ContainsKey('CountryCode')) { if ($All) { $Script:QuickSearch[$CountryCode] } else { $Script:QuickSearch[$CountryCode].RegionInformation.EnglishName } } else { $Script:QuickSearch } } else { $Script:QuickSearch = [ordered] @{} $AllCultures = [cultureinfo]::GetCultures([System.Globalization.CultureTypes]::SpecificCultures) foreach ($Culture in $AllCultures) { $RegionInformation = [System.Globalization.RegionInfo]::new($Culture) $Script:QuickSearch[$RegionInformation.TwoLetterISORegionName] = @{ 'Culture' = $Culture 'RegionInformation' = $RegionInformation } $Script:QuickSearch[$RegionInformation.ThreeLetterISORegionName] = @{ 'Culture' = $Culture 'RegionInformation' = $RegionInformation } } if ($PSBoundParameters.ContainsKey('CountryCode')) { if ($All) { $Script:QuickSearch[$CountryCode] } else { $Script:QuickSearch[$CountryCode].RegionInformation.EnglishName } } else { $Script:QuickSearch } } } function Convert-CountryToContinent { <# .SYNOPSIS Convert country to continent .DESCRIPTION Convert country to continent or return a hashtable of countries and their corresponding continent. If the country is not found (for example empty), it will return "Unknown" .PARAMETER Country Country to convert. If country is not given it will return a hashtable of countries and their corresponding continent. .EXAMPLE Convert-CountryToContinent -Country "Poland" .EXAMPLE Convert-CountryToContinent .NOTES General notes #> [CmdletBinding()] param( [string] $Country, [switch] $ReturnHashTable ) $CountryToContinent = [ordered] @{ "Afghanistan" = "Asia" "Albania" = "Europe" "Algeria" = "Africa" "Andorra" = "Europe" "Angola" = "Africa" "Antigua and Barbuda" = "North America" "Argentina" = "South America" "Armenia" = "Asia" "Australia" = "Australia/Oceania" "Austria" = "Europe" "Azerbaijan" = "Asia" "Bahamas" = "North America" "Bahrain" = "Asia" "Bangladesh" = "Asia" "Barbados" = "North America" "Belarus" = "Europe" "Belgium" = "Europe" "Belize" = "North America" "Benin" = "Africa" "Bhutan" = "Asia" "Bolivia" = "South America" "Bosnia and Herzegovina" = "Europe" "Botswana" = "Africa" "Brazil" = "South America" "Brunei" = "Asia" "Bulgaria" = "Europe" "Burkina Faso" = "Africa" "Burundi" = "Africa" "Cabo Verde" = "Africa" "Cambodia" = "Asia" "Cameroon" = "Africa" "Canada" = "North America" "Central African Republic" = "Africa" "Chad" = "Africa" "Chile" = "South America" "China" = "Asia" "Colombia" = "South America" "Comoros" = "Africa" "Congo, Democratic Republic of the" = "Africa" "Congo, Republic of the" = "Africa" "Costa Rica" = "North America" "Cote d'Ivoire" = "Africa" "Croatia" = "Europe" "Cuba" = "North America" "Cyprus" = "Asia" "Czechia" = "Europe" "Denmark" = "Europe" "Djibouti" = "Africa" "Dominica" = "North America" "Dominican Republic" = "North America" "Ecuador" = "South America" "Egypt" = "Africa" "El Salvador" = "North America" "Equatorial Guinea" = "Africa" "Eritrea" = "Africa" "Estonia" = "Europe" "Eswatini" = "Africa" "Ethiopia" = "Africa" "Fiji" = "Australia/Oceania" "Finland" = "Europe" "France" = "Europe" "Gabon" = "Africa" "Gambia" = "Africa" "Georgia" = "Asia" "Germany" = "Europe" "Ghana" = "Africa" "Greece" = "Europe" "Grenada" = "North America" "Guatemala" = "North America" "Guinea" = "Africa" "Guinea-Bissau" = "Africa" "Guyana" = "South America" "Haiti" = "North America" "Honduras" = "North America" "Hungary" = "Europe" "Iceland" = "Europe" "India" = "Asia" "Indonesia" = "Asia" "Iran" = "Asia" "Iraq" = "Asia" "Ireland" = "Europe" "Israel" = "Asia" "Italy" = "Europe" "Jamaica" = "North America" "Japan" = "Asia" "Jordan" = "Asia" "Kazakhstan" = "Asia" "Kenya" = "Africa" "Kiribati" = "Australia/Oceania" "Kosovo" = "Europe" "Kuwait" = "Asia" "Kyrgyzstan" = "Asia" "Laos" = "Asia" "Latvia" = "Europe" "Lebanon" = "Asia" "Lesotho" = "Africa" "Liberia" = "Africa" "Libya" = "Africa" "Liechtenstein" = "Europe" "Lithuania" = "Europe" "Luxembourg" = "Europe" "Madagascar" = "Africa" "Malawi" = "Africa" "Malaysia" = "Asia" "Maldives" = "Asia" "Mali" = "Africa" "Malta" = "Europe" "Marshall Islands" = "Australia/Oceania" "Mauritania" = "Africa" "Mauritius" = "Africa" "Mexico" = "North America" "Micronesia" = "Australia/Oceania" "Moldova" = "Europe" "Monaco" = "Europe" "Mongolia" = "Asia" "Montenegro" = "Europe" "Morocco" = "Africa" "Mozambique" = "Africa" "Myanmar" = "Asia" "Namibia" = "Africa" "Nauru" = "Australia/Oceania" "Nepal" = "Asia" "Netherlands" = "Europe" "New Zealand" = "Australia/Oceania" "Nicaragua" = "North America" "Niger" = "Africa" "Nigeria" = "Africa" "North Korea" = "Asia" "North Macedonia" = "Europe" "Norway" = "Europe" "Oman" = "Asia" "Pakistan" = "Asia" "Palau" = "Australia/Oceania" "Panama" = "North America" "Papua New Guinea" = "Australia/Oceania" "Paraguay" = "South America" "Peru" = "South America" "Philippines" = "Asia" "Poland" = "Europe" "Portugal" = "Europe" "Qatar" = "Asia" "Romania" = "Europe" "Russia" = "Asia" "Rwanda" = "Africa" "Saint Kitts and Nevis" = "North America" "Saint Lucia" = "North America" "Saint Vincent and the Grenadines" = "North America" "Samoa" = "Australia/Oceania" "San Marino" = "Europe" "Sao Tome and Principe" = "Africa" "Saudi Arabia" = "Asia" "Senegal" = "Africa" "Serbia" = "Europe" "Seychelles" = "Africa" "Sierra Leone" = "Africa" "Singapore" = "Asia" "Slovakia" = "Europe" "Slovenia" = "Europe" "Solomon Islands" = "Australia/Oceania" "Somalia" = "Africa" "South Africa" = "Africa" "South Korea" = "Asia" "South Sudan" = "Africa" "Spain" = "Europe" "Sri Lanka" = "Asia" "Sudan" = "Africa" "Suriname" = "South America" "Sweden" = "Europe" "Switzerland" = "Europe" "Syria" = "Asia" "Taiwan" = "Asia" "Tajikistan" = "Asia" "Tanzania" = "Africa" "Thailand" = "Asia" "Timor-Leste" = "Asia" "Togo" = "Africa" "Tonga" = "Australia/Oceania" "Trinidad and Tobago" = "North America" "Tunisia" = "Africa" "Turkey" = "Asia" "Turkmenistan" = "Asia" "Tuvalu" = "Australia/Oceania" "Uganda" = "Africa" "Ukraine" = "Europe" "United Arab Emirates" = "Asia" "United Kingdom" = "Europe" "United States of America" = "North America" "Uruguay" = "South America" "Uzbekistan" = "Asia" "Vanuatu" = "Australia/Oceania" "Vatican City (Holy See)" = "Europe" "Venezuela" = "South America" "Vietnam" = "Asia" "Yemen" = "Asia" "Zambia" = "Africa" "Zimbabwe" = "Africa" } if ($PSBoundParameters.ContainsKey('Country')) { if ($CountryToContinent[$Country]) { $CountryToContinent[$Country] } else { "Unknown" } } else { $CountryToContinent } } function Convert-UserAccountControl { <# .SYNOPSIS Converts the UserAccountControl flags to their corresponding names. .DESCRIPTION This function takes a UserAccountControl value and converts it into a human-readable format by matching the flags to their corresponding names. .PARAMETER UserAccountControl Specifies the UserAccountControl value to be converted. .PARAMETER Separator Specifies the separator to use when joining the converted flags. If not provided, the flags will be returned as a list. .EXAMPLE Convert-UserAccountControl -UserAccountControl 66048 Outputs: "DONT_EXPIRE_PASSWORD, PASSWORD_EXPIRED" .EXAMPLE Convert-UserAccountControl -UserAccountControl 512 -Separator ', ' Outputs: "NORMAL_ACCOUNT" #> [cmdletBinding()] param( [alias('UAC')][int] $UserAccountControl, [string] $Separator ) $UserAccount = [ordered] @{ "SCRIPT" = 1 "ACCOUNTDISABLE" = 2 "HOMEDIR_REQUIRED" = 8 "LOCKOUT" = 16 "PASSWD_NOTREQD" = 32 "ENCRYPTED_TEXT_PWD_ALLOWED" = 128 "TEMP_DUPLICATE_ACCOUNT" = 256 "NORMAL_ACCOUNT" = 512 "INTERDOMAIN_TRUST_ACCOUNT" = 2048 "WORKSTATION_TRUST_ACCOUNT" = 4096 "SERVER_TRUST_ACCOUNT" = 8192 "DONT_EXPIRE_PASSWORD" = 65536 "MNS_LOGON_ACCOUNT" = 131072 "SMARTCARD_REQUIRED" = 262144 "TRUSTED_FOR_DELEGATION" = 524288 "NOT_DELEGATED" = 1048576 "USE_DES_KEY_ONLY" = 2097152 "DONT_REQ_PREAUTH" = 4194304 "PASSWORD_EXPIRED" = 8388608 "TRUSTED_TO_AUTH_FOR_DELEGATION" = 16777216 "PARTIAL_SECRETS_ACCOUNT" = 67108864 } $Output = foreach ($_ in $UserAccount.Keys) { $binaryAnd = $UserAccount[$_] -band $UserAccountControl if ($binaryAnd -ne "0") { $_ } } if ($Separator) { $Output -join $Separator } else { $Output } } function ConvertFrom-DistinguishedName { <# .SYNOPSIS Converts a Distinguished Name to CN, OU, Multiple OUs or DC .DESCRIPTION Converts a Distinguished Name to CN, OU, Multiple OUs or DC .PARAMETER DistinguishedName Distinguished Name to convert .PARAMETER ToOrganizationalUnit Converts DistinguishedName to Organizational Unit .PARAMETER ToDC Converts DistinguishedName to DC .PARAMETER ToDomainCN Converts DistinguishedName to Domain Canonical Name (CN) .PARAMETER ToCanonicalName Converts DistinguishedName to Canonical Name .EXAMPLE $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName -ToOrganizationalUnit Output: OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz .EXAMPLE $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName Output: Przemyslaw Klys .EXAMPLE ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToMultipleOrganizationalUnit -IncludeParent Output: OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz OU=Production,DC=ad,DC=evotec,DC=xyz .EXAMPLE ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToMultipleOrganizationalUnit Output: OU=Production,DC=ad,DC=evotec,DC=xyz .EXAMPLE $Con = @( 'CN=Windows Authorization Access Group,CN=Builtin,DC=ad,DC=evotec,DC=xyz' 'CN=Mmm,DC=elo,CN=nee,DC=RootDNSServers,CN=MicrosoftDNS,CN=System,DC=ad,DC=evotec,DC=xyz' 'CN=e6d5fd00-385d-4e65-b02d-9da3493ed850,CN=Operations,CN=DomainUpdates,CN=System,DC=ad,DC=evotec,DC=xyz' 'OU=Domain Controllers,DC=ad,DC=evotec,DC=pl' 'OU=Microsoft Exchange Security Groups,DC=ad,DC=evotec,DC=xyz' ) ConvertFrom-DistinguishedName -DistinguishedName $Con -ToLastName Output: Windows Authorization Access Group Mmm e6d5fd00-385d-4e65-b02d-9da3493ed850 Domain Controllers Microsoft Exchange Security Groups .EXAMPLEE ConvertFrom-DistinguishedName -DistinguishedName 'DC=ad,DC=evotec,DC=xyz' -ToCanonicalName ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToCanonicalName ConvertFrom-DistinguishedName -DistinguishedName 'CN=test,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToCanonicalName Output: ad.evotec.xyz ad.evotec.xyz\Production\Users ad.evotec.xyz\Production\Users\test .NOTES General notes #> [CmdletBinding(DefaultParameterSetName = 'Default')] param( [Parameter(ParameterSetName = 'ToOrganizationalUnit')] [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')] [Parameter(ParameterSetName = 'ToDC')] [Parameter(ParameterSetName = 'ToDomainCN')] [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'ToLastName')] [Parameter(ParameterSetName = 'ToCanonicalName')] [alias('Identity', 'DN')][Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)][string[]] $DistinguishedName, [Parameter(ParameterSetName = 'ToOrganizationalUnit')][switch] $ToOrganizationalUnit, [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')][alias('ToMultipleOU')][switch] $ToMultipleOrganizationalUnit, [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')][switch] $IncludeParent, [Parameter(ParameterSetName = 'ToDC')][switch] $ToDC, [Parameter(ParameterSetName = 'ToDomainCN')][switch] $ToDomainCN, [Parameter(ParameterSetName = 'ToLastName')][switch] $ToLastName, [Parameter(ParameterSetName = 'ToCanonicalName')][switch] $ToCanonicalName ) Process { foreach ($Distinguished in $DistinguishedName) { if ($ToDomainCN) { $DN = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1' $CN = $DN -replace ',DC=', '.' -replace "DC=" if ($CN) { $CN } } elseif ($ToOrganizationalUnit) { $Value = [Regex]::Match($Distinguished, '(?=OU=)(.*\n?)(?<=.)').Value if ($Value) { $Value } } elseif ($ToMultipleOrganizationalUnit) { if ($IncludeParent) { $Distinguished } while ($true) { $Distinguished = $Distinguished -replace '^.+?,(?=..=)' if ($Distinguished -match '^DC=') { break } $Distinguished } } elseif ($ToDC) { $Value = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1' if ($Value) { $Value } } elseif ($ToLastName) { $NewDN = $Distinguished -split ",DC=" if ($NewDN[0].Contains(",OU=")) { [Array] $ChangedDN = $NewDN[0] -split ",OU=" } elseif ($NewDN[0].Contains(",CN=")) { [Array] $ChangedDN = $NewDN[0] -split ",CN=" } else { [Array] $ChangedDN = $NewDN[0] } if ($ChangedDN[0].StartsWith('CN=')) { $ChangedDN[0] -replace 'CN=', '' } else { $ChangedDN[0] -replace 'OU=', '' } } elseif ($ToCanonicalName) { $Domain = $null $Rest = $null foreach ($O in $Distinguished -split '(?<!\\),') { if ($O -match '^DC=') { $Domain += $O.Substring(3) + '.' } else { $Rest = $O.Substring(3) + '\' + $Rest } } if ($Domain -and $Rest) { $Domain.Trim('.') + '\' + ($Rest.TrimEnd('\') -replace '\\,', ',') } elseif ($Domain) { $Domain.Trim('.') } elseif ($Rest) { $Rest.TrimEnd('\') -replace '\\,', ',' } } else { $Regex = '^CN=(?<cn>.+?)(?<!\\),(?<ou>(?:(?:OU|CN).+?(?<!\\),)+(?<dc>DC.+?))$' $Found = $Distinguished -match $Regex if ($Found) { $Matches.cn } } } } } function Get-FileInformation { <# .SYNOPSIS Get information about file such as Name, FullName and Size .DESCRIPTION Get information about file such as Name, FullName and Size .PARAMETER File File to get information about .EXAMPLE Get-FileInformation -File 'C:\Support\GitHub\PSSharedGoods\Public\FilesFolders\Get-FileInformation.ps1' #> [CmdletBinding()] param( [alias('LiteralPath', 'Path')][string] $File ) if (Test-Path -LiteralPath $File) { $Item = Get-Item -LiteralPath $File [PSCustomObject] @{ Name = $Item.Name FullName = $Item.FullName Size = Get-FileSize -Bytes $Item.Length IsReadOnly = $Item.IsReadOnly LastWriteTime = $Item.LastWriteTime } } } function Get-GitHubVersion { <# .SYNOPSIS Get the latest version of a GitHub repository and compare with local version .DESCRIPTION Get the latest version of a GitHub repository and compare with local version .PARAMETER Cmdlet Cmdlet to find module for .PARAMETER RepositoryOwner Repository owner .PARAMETER RepositoryName Repository name .EXAMPLE Get-GitHubVersion -Cmdlet 'Start-DelegationModel' -RepositoryOwner 'evotecit' -RepositoryName 'DelegationModel' .NOTES General notes #> [cmdletBinding()] param( [Parameter(Mandatory)][string] $Cmdlet, [Parameter(Mandatory)][string] $RepositoryOwner, [Parameter(Mandatory)][string] $RepositoryName ) $App = Get-Command -Name $Cmdlet -ErrorAction SilentlyContinue if ($App) { [Array] $GitHubReleases = (Get-GitHubLatestRelease -Url "https://api.github.com/repos/$RepositoryOwner/$RepositoryName/releases" -Verbose:$false) $LatestVersion = $GitHubReleases[0] if (-not $LatestVersion.Errors) { if ($App.Version -eq $LatestVersion.Version) { "Current/Latest: $($LatestVersion.Version) at $($LatestVersion.PublishDate)" } elseif ($App.Version -lt $LatestVersion.Version) { "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Update?" } elseif ($App.Version -gt $LatestVersion.Version) { "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Lucky you!" } } else { "Current: $($App.Version)" } } else { "Current: Unknown" } } function Get-WinADForestDetails { <# .SYNOPSIS Get details about Active Directory Forest, Domains and Domain Controllers in a single query .DESCRIPTION Get details about Active Directory Forest, Domains and Domain Controllers in a single query .PARAMETER Forest Target different Forest, by default current forest is used .PARAMETER ExcludeDomains Exclude domain from search, by default whole forest is scanned .PARAMETER IncludeDomains Include only specific domains, by default whole forest is scanned .PARAMETER ExcludeDomainControllers Exclude specific domain controllers, by default there are no exclusions, as long as VerifyDomainControllers switch is enabled. Otherwise this parameter is ignored. .PARAMETER IncludeDomainControllers Include only specific domain controllers, by default all domain controllers are included, as long as VerifyDomainControllers switch is enabled. Otherwise this parameter is ignored. .PARAMETER SkipRODC Skip Read-Only Domain Controllers. By default all domain controllers are included. .PARAMETER ExtendedForestInformation Ability to provide Forest Information from another command to speed up processing .PARAMETER Filter Filter for Get-ADDomainController .PARAMETER TestAvailability Check if Domain Controllers are available .PARAMETER Test Pick what to check for availability. Options are: All, Ping, WinRM, PortOpen, Ping+WinRM, Ping+PortOpen, WinRM+PortOpen. Default is All .PARAMETER Ports Ports to check for availability. Default is 135 .PARAMETER PortsTimeout Ports timeout for availability check. Default is 100 .PARAMETER PingCount How many pings to send. Default is 1 .PARAMETER PreferWritable Prefer writable domain controllers over read-only ones when returning Query Servers .PARAMETER Extended Return extended information about domains with NETBIOS names .EXAMPLE Get-WinADForestDetails | Format-Table .EXAMPLE Get-WinADForestDetails -Forest 'ad.evotec.xyz' | Format-Table .NOTES General notes #> [CmdletBinding()] param( [alias('ForestName')][string] $Forest, [string[]] $ExcludeDomains, [string[]] $ExcludeDomainControllers, [alias('Domain', 'Domains')][string[]] $IncludeDomains, [alias('DomainControllers', 'ComputerName')][string[]] $IncludeDomainControllers, [switch] $SkipRODC, [string] $Filter = '*', [switch] $TestAvailability, [ValidateSet('All', 'Ping', 'WinRM', 'PortOpen', 'Ping+WinRM', 'Ping+PortOpen', 'WinRM+PortOpen')] $Test = 'All', [int[]] $Ports = 135, [int] $PortsTimeout = 100, [int] $PingCount = 1, [switch] $PreferWritable, [switch] $Extended, [System.Collections.IDictionary] $ExtendedForestInformation ) if ($Global:ProgressPreference -ne 'SilentlyContinue') { $TemporaryProgress = $Global:ProgressPreference $Global:ProgressPreference = 'SilentlyContinue' } if (-not $ExtendedForestInformation) { $Findings = [ordered] @{ } try { if ($Forest) { $ForestInformation = Get-ADForest -ErrorAction Stop -Identity $Forest } else { $ForestInformation = Get-ADForest -ErrorAction Stop } } catch { Write-Warning "Get-WinADForestDetails - Error discovering DC for Forest - $($_.Exception.Message)" return } if (-not $ForestInformation) { return } $Findings['Forest'] = $ForestInformation $Findings['ForestDomainControllers'] = @() $Findings['QueryServers'] = @{ } $Findings['DomainDomainControllers'] = @{ } [Array] $Findings['Domains'] = foreach ($Domain in $ForestInformation.Domains) { if ($IncludeDomains) { if ($Domain -in $IncludeDomains) { $Domain.ToLower() } continue } if ($Domain -notin $ExcludeDomains) { $Domain.ToLower() } } [Array] $DomainsActive = foreach ($Domain in $Findings['Forest'].Domains) { try { $DC = Get-ADDomainController -DomainName $Domain -Discover -ErrorAction Stop -Writable:$PreferWritable.IsPresent $OrderedDC = [ordered] @{ Domain = $DC.Domain Forest = $DC.Forest HostName = [Array] $DC.HostName IPv4Address = $DC.IPv4Address IPv6Address = $DC.IPv6Address Name = $DC.Name Site = $DC.Site } } catch { Write-Warning "Get-WinADForestDetails - Error discovering DC for domain $Domain - $($_.Exception.Message)" continue } if ($Domain -eq $Findings['Forest']['Name']) { $Findings['QueryServers']['Forest'] = $OrderedDC } $Findings['QueryServers']["$Domain"] = $OrderedDC $Domain } [Array] $Findings['Domains'] = foreach ($Domain in $Findings['Domains']) { if ($Domain -notin $DomainsActive) { Write-Warning "Get-WinADForestDetails - Domain $Domain doesn't seem to be active (no DCs). Skipping." continue } $Domain } [Array] $Findings['ForestDomainControllers'] = foreach ($Domain in $Findings.Domains) { $QueryServer = $Findings['QueryServers'][$Domain]['HostName'][0] [Array] $AllDC = try { try { $DomainControllers = Get-ADDomainController -Filter $Filter -Server $QueryServer -ErrorAction Stop } catch { Write-Warning "Get-WinADForestDetails - Error listing DCs for domain $Domain - $($_.Exception.Message)" continue } foreach ($S in $DomainControllers) { if ($IncludeDomainControllers.Count -gt 0) { If (-not $IncludeDomainControllers[0].Contains('.')) { if ($S.Name -notin $IncludeDomainControllers) { continue } } else { if ($S.HostName -notin $IncludeDomainControllers) { continue } } } if ($ExcludeDomainControllers.Count -gt 0) { If (-not $ExcludeDomainControllers[0].Contains('.')) { if ($S.Name -in $ExcludeDomainControllers) { continue } } else { if ($S.HostName -in $ExcludeDomainControllers) { continue } } } $DSAGuid = (Get-ADObject -Identity $S.NTDSSettingsObjectDN -Server $QueryServer).ObjectGUID $Server = [ordered] @{ Domain = $Domain HostName = $S.HostName Name = $S.Name Forest = $ForestInformation.RootDomain Site = $S.Site IPV4Address = $S.IPV4Address IPV6Address = $S.IPV6Address IsGlobalCatalog = $S.IsGlobalCatalog IsReadOnly = $S.IsReadOnly IsSchemaMaster = ($S.OperationMasterRoles -contains 'SchemaMaster') IsDomainNamingMaster = ($S.OperationMasterRoles -contains 'DomainNamingMaster') IsPDC = ($S.OperationMasterRoles -contains 'PDCEmulator') IsRIDMaster = ($S.OperationMasterRoles -contains 'RIDMaster') IsInfrastructureMaster = ($S.OperationMasterRoles -contains 'InfrastructureMaster') OperatingSystem = $S.OperatingSystem OperatingSystemVersion = $S.OperatingSystemVersion OperatingSystemLong = ConvertTo-OperatingSystem -OperatingSystem $S.OperatingSystem -OperatingSystemVersion $S.OperatingSystemVersion LdapPort = $S.LdapPort SslPort = $S.SslPort DistinguishedName = $S.ComputerObjectDN NTDSSettingsObjectDN = $S.NTDSSettingsObjectDN DsaGuid = $DSAGuid DsaGuidName = "$DSAGuid._msdcs.$($ForestInformation.RootDomain)" Pingable = $null WinRM = $null PortOpen = $null Comment = '' } if ($TestAvailability) { if ($Test -eq 'All' -or $Test -like 'Ping*') { $Server.Pingable = Test-Connection -ComputerName $Server.IPV4Address -Quiet -Count $PingCount } if ($Test -eq 'All' -or $Test -like '*WinRM*') { $Server.WinRM = (Test-WinRM -ComputerName $Server.HostName).Status } if ($Test -eq 'All' -or '*PortOpen*') { $Server.PortOpen = (Test-ComputerPort -Server $Server.HostName -PortTCP $Ports -Timeout $PortsTimeout).Status } } [PSCustomObject] $Server } } catch { [PSCustomObject]@{ Domain = $Domain HostName = '' Name = '' Forest = $ForestInformation.RootDomain IPV4Address = '' IPV6Address = '' IsGlobalCatalog = '' IsReadOnly = '' Site = '' SchemaMaster = $false DomainNamingMasterMaster = $false PDCEmulator = $false RIDMaster = $false InfrastructureMaster = $false LdapPort = '' SslPort = '' DistinguishedName = '' NTDSSettingsObjectDN = '' DsaGuid = '' DsaGuidName = '' Pingable = $null WinRM = $null PortOpen = $null Comment = $_.Exception.Message -replace "`n", " " -replace "`r", " " } } if ($SkipRODC) { [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC | Where-Object { $_.IsReadOnly -eq $false } } else { [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC } if ($null -ne $Findings['DomainDomainControllers'][$Domain]) { [Array] $Findings['DomainDomainControllers'][$Domain] } } if ($Extended) { $Findings['DomainsExtended'] = @{ } $Findings['DomainsExtendedNetBIOS'] = @{ } foreach ($DomainEx in $Findings['Domains']) { try { $Findings['DomainsExtended'][$DomainEx] = Get-ADDomain -Server $Findings['QueryServers'][$DomainEx].HostName[0] | ForEach-Object { [ordered] @{ AllowedDNSSuffixes = $_.AllowedDNSSuffixes | ForEach-Object -Process { $_ } ChildDomains = $_.ChildDomains | ForEach-Object -Process { $_ } ComputersContainer = $_.ComputersContainer DeletedObjectsContainer = $_.DeletedObjectsContainer DistinguishedName = $_.DistinguishedName DNSRoot = $_.DNSRoot DomainControllersContainer = $_.DomainControllersContainer DomainMode = $_.DomainMode DomainSID = $_.DomainSID.Value ForeignSecurityPrincipalsContainer = $_.ForeignSecurityPrincipalsContainer Forest = $_.Forest InfrastructureMaster = $_.InfrastructureMaster LastLogonReplicationInterval = $_.LastLogonReplicationInterval LinkedGroupPolicyObjects = $_.LinkedGroupPolicyObjects | ForEach-Object -Process { $_ } LostAndFoundContainer = $_.LostAndFoundContainer ManagedBy = $_.ManagedBy Name = $_.Name NetBIOSName = $_.NetBIOSName ObjectClass = $_.ObjectClass ObjectGUID = $_.ObjectGUID ParentDomain = $_.ParentDomain PDCEmulator = $_.PDCEmulator PublicKeyRequiredPasswordRolling = $_.PublicKeyRequiredPasswordRolling | ForEach-Object -Process { $_ } QuotasContainer = $_.QuotasContainer ReadOnlyReplicaDirectoryServers = $_.ReadOnlyReplicaDirectoryServers | ForEach-Object -Process { $_ } ReplicaDirectoryServers = $_.ReplicaDirectoryServers | ForEach-Object -Process { $_ } RIDMaster = $_.RIDMaster SubordinateReferences = $_.SubordinateReferences | ForEach-Object -Process { $_ } SystemsContainer = $_.SystemsContainer UsersContainer = $_.UsersContainer } } $NetBios = $Findings['DomainsExtended'][$DomainEx]['NetBIOSName'] $Findings['DomainsExtendedNetBIOS'][$NetBios] = $Findings['DomainsExtended'][$DomainEx] } catch { Write-Warning "Get-WinADForestDetails - Error gathering Domain Information for domain $DomainEx - $($_.Exception.Message)" continue } } } if ($TemporaryProgress) { $Global:ProgressPreference = $TemporaryProgress } $Findings } else { $Findings = Copy-DictionaryManual -Dictionary $ExtendedForestInformation [Array] $Findings['Domains'] = foreach ($_ in $Findings.Domains) { if ($IncludeDomains) { if ($_ -in $IncludeDomains) { $_.ToLower() } continue } if ($_ -notin $ExcludeDomains) { $_.ToLower() } } foreach ($_ in [string[]] $Findings.DomainDomainControllers.Keys) { if ($_ -notin $Findings.Domains) { $Findings.DomainDomainControllers.Remove($_) } } foreach ($_ in [string[]] $Findings.DomainsExtended.Keys) { if ($_ -notin $Findings.Domains) { $Findings.DomainsExtended.Remove($_) $NetBiosName = $Findings.DomainsExtended.$_.'NetBIOSName' if ($NetBiosName) { $Findings.DomainsExtendedNetBIOS.Remove($NetBiosName) } } } [Array] $Findings['ForestDomainControllers'] = foreach ($Domain in $Findings.Domains) { [Array] $AllDC = foreach ($S in $Findings.DomainDomainControllers["$Domain"]) { if ($IncludeDomainControllers.Count -gt 0) { If (-not $IncludeDomainControllers[0].Contains('.')) { if ($S.Name -notin $IncludeDomainControllers) { continue } } else { if ($S.HostName -notin $IncludeDomainControllers) { continue } } } if ($ExcludeDomainControllers.Count -gt 0) { If (-not $ExcludeDomainControllers[0].Contains('.')) { if ($S.Name -in $ExcludeDomainControllers) { continue } } else { if ($S.HostName -in $ExcludeDomainControllers) { continue } } } $S } if ($SkipRODC) { [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC | Where-Object { $_.IsReadOnly -eq $false } } else { [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC } [Array] $Findings['DomainDomainControllers'][$Domain] } $Findings } } function Remove-EmptyValue { <# .SYNOPSIS Removes empty values from a hashtable recursively. .DESCRIPTION This function removes empty values from a given hashtable. It can be used to clean up a hashtable by removing keys with null, empty string, empty array, or empty dictionary values. The function supports recursive removal of empty values. .PARAMETER Hashtable The hashtable from which empty values will be removed. .PARAMETER ExcludeParameter An array of keys to exclude from the removal process. .PARAMETER Recursive Indicates whether to recursively remove empty values from nested hashtables. .PARAMETER Rerun Specifies the number of times to rerun the removal process recursively. .PARAMETER DoNotRemoveNull If specified, null values will not be removed. .PARAMETER DoNotRemoveEmpty If specified, empty string values will not be removed. .PARAMETER DoNotRemoveEmptyArray If specified, empty array values will not be removed. .PARAMETER DoNotRemoveEmptyDictionary If specified, empty dictionary values will not be removed. .EXAMPLE $hashtable = @{ 'Key1' = ''; 'Key2' = $null; 'Key3' = @(); 'Key4' = @{} } Remove-EmptyValue -Hashtable $hashtable -Recursive Description ----------- This example removes empty values from the $hashtable recursively. #> [alias('Remove-EmptyValues')] [CmdletBinding()] param( [alias('Splat', 'IDictionary')][Parameter(Mandatory)][System.Collections.IDictionary] $Hashtable, [string[]] $ExcludeParameter, [switch] $Recursive, [int] $Rerun, [switch] $DoNotRemoveNull, [switch] $DoNotRemoveEmpty, [switch] $DoNotRemoveEmptyArray, [switch] $DoNotRemoveEmptyDictionary ) foreach ($Key in [string[]] $Hashtable.Keys) { if ($Key -notin $ExcludeParameter) { if ($Recursive) { if ($Hashtable[$Key] -is [System.Collections.IDictionary]) { if ($Hashtable[$Key].Count -eq 0) { if (-not $DoNotRemoveEmptyDictionary) { $Hashtable.Remove($Key) } } else { Remove-EmptyValue -Hashtable $Hashtable[$Key] -Recursive:$Recursive } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } } if ($Rerun) { for ($i = 0; $i -lt $Rerun; $i++) { Remove-EmptyValue -Hashtable $Hashtable -Recursive:$Recursive } } } function Start-TimeLog { <# .SYNOPSIS Starts a new stopwatch for logging time. .DESCRIPTION This function starts a new stopwatch that can be used for logging time durations. .EXAMPLE Start-TimeLog Starts a new stopwatch for logging time. #> [CmdletBinding()] param() [System.Diagnostics.Stopwatch]::StartNew() } function Stop-TimeLog { <# .SYNOPSIS Stops the stopwatch and returns the elapsed time in a specified format. .DESCRIPTION The Stop-TimeLog function stops the provided stopwatch and returns the elapsed time in a specified format. The function can output the elapsed time as a single string or an array of days, hours, minutes, seconds, and milliseconds. .PARAMETER Time Specifies the stopwatch object to stop and retrieve the elapsed time from. .PARAMETER Option Specifies the format in which the elapsed time should be returned. Valid values are 'OneLiner' (default) or 'Array'. .PARAMETER Continue Indicates whether the stopwatch should continue running after retrieving the elapsed time. .EXAMPLE $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() # Perform some operations Stop-TimeLog -Time $stopwatch # Output: "0 days, 0 hours, 0 minutes, 5 seconds, 123 milliseconds" .EXAMPLE $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() # Perform some operations Stop-TimeLog -Time $stopwatch -Option Array # Output: ["0 days", "0 hours", "0 minutes", "5 seconds", "123 milliseconds"] #> [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)][System.Diagnostics.Stopwatch] $Time, [ValidateSet('OneLiner', 'Array')][string] $Option = 'OneLiner', [switch] $Continue ) Begin { } Process { if ($Option -eq 'Array') { $TimeToExecute = "$($Time.Elapsed.Days) days", "$($Time.Elapsed.Hours) hours", "$($Time.Elapsed.Minutes) minutes", "$($Time.Elapsed.Seconds) seconds", "$($Time.Elapsed.Milliseconds) milliseconds" } else { $TimeToExecute = "$($Time.Elapsed.Days) days, $($Time.Elapsed.Hours) hours, $($Time.Elapsed.Minutes) minutes, $($Time.Elapsed.Seconds) seconds, $($Time.Elapsed.Milliseconds) milliseconds" } } End { if (-not $Continue) { $Time.Stop() } return $TimeToExecute } } function Write-Color { <# .SYNOPSIS Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options. .DESCRIPTION Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options. It provides: - Easy manipulation of colors, - Logging output to file (log) - Nice formatting options out of the box. - Ability to use aliases for parameters .PARAMETER Text Text to display on screen and write to log file if specified. Accepts an array of strings. .PARAMETER Color Color of the text. Accepts an array of colors. If more than one color is specified it will loop through colors for each string. If there are more strings than colors it will start from the beginning. Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White .PARAMETER BackGroundColor Color of the background. Accepts an array of colors. If more than one color is specified it will loop through colors for each string. If there are more strings than colors it will start from the beginning. Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White .PARAMETER StartTab Number of tabs to add before text. Default is 0. .PARAMETER LinesBefore Number of empty lines before text. Default is 0. .PARAMETER LinesAfter Number of empty lines after text. Default is 0. .PARAMETER StartSpaces Number of spaces to add before text. Default is 0. .PARAMETER LogFile Path to log file. If not specified no log file will be created. .PARAMETER DateTimeFormat Custom date and time format string. Default is yyyy-MM-dd HH:mm:ss .PARAMETER LogTime If set to $true it will add time to log file. Default is $true. .PARAMETER LogRetry Number of retries to write to log file, in case it can't write to it for some reason, before skipping. Default is 2. .PARAMETER Encoding Encoding of the log file. Default is Unicode. .PARAMETER ShowTime Switch to add time to console output. Default is not set. .PARAMETER NoNewLine Switch to not add new line at the end of the output. Default is not set. .PARAMETER NoConsoleOutput Switch to not output to console. Default all output goes to console. .EXAMPLE Write-Color -Text "Red ", "Green ", "Yellow " -Color Red,Green,Yellow .EXAMPLE Write-Color -Text "This is text in Green ", "followed by red ", "and then we have Magenta... ", "isn't it fun? ", "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan .EXAMPLE Write-Color -Text "This is text in Green ", "followed by red ", "and then we have Magenta... ", "isn't it fun? ", "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan -StartTab 3 -LinesBefore 1 -LinesAfter 1 .EXAMPLE Write-Color "1. ", "Option 1" -Color Yellow, Green Write-Color "2. ", "Option 2" -Color Yellow, Green Write-Color "3. ", "Option 3" -Color Yellow, Green Write-Color "4. ", "Option 4" -Color Yellow, Green Write-Color "9. ", "Press 9 to exit" -Color Yellow, Gray -LinesBefore 1 .EXAMPLE Write-Color -LinesBefore 2 -Text "This little ","message is ", "written to log ", "file as well." ` -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" -TimeFormat "yyyy-MM-dd HH:mm:ss" Write-Color -Text "This can get ","handy if ", "want to display things, and log actions to file ", "at the same time." ` -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" .EXAMPLE Write-Color -T "My text", " is ", "all colorful" -C Yellow, Red, Green -B Green, Green, Yellow Write-Color -t "my text" -c yellow -b green Write-Color -text "my text" -c red .EXAMPLE Write-Color -Text "Testuję czy się ładnie zapisze, czy będą problemy" -Encoding unicode -LogFile 'C:\temp\testinggg.txt' -Color Red -NoConsoleOutput .NOTES Understanding Custom date and time format strings: https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings Project support: https://github.com/EvotecIT/PSWriteColor Original idea: Josh (https://stackoverflow.com/users/81769/josh) #> [alias('Write-Colour')] [CmdletBinding()] param ( [alias ('T')] [String[]]$Text, [alias ('C', 'ForegroundColor', 'FGC')] [ConsoleColor[]]$Color = [ConsoleColor]::White, [alias ('B', 'BGC')] [ConsoleColor[]]$BackGroundColor = $null, [alias ('Indent')][int] $StartTab = 0, [int] $LinesBefore = 0, [int] $LinesAfter = 0, [int] $StartSpaces = 0, [alias ('L')] [string] $LogFile = '', [Alias('DateFormat', 'TimeFormat')][string] $DateTimeFormat = 'yyyy-MM-dd HH:mm:ss', [alias ('LogTimeStamp')][bool] $LogTime = $true, [int] $LogRetry = 2, [ValidateSet('unknown', 'string', 'unicode', 'bigendianunicode', 'utf8', 'utf7', 'utf32', 'ascii', 'default', 'oem')][string]$Encoding = 'Unicode', [switch] $ShowTime, [switch] $NoNewLine, [alias('HideConsole')][switch] $NoConsoleOutput ) if (-not $NoConsoleOutput) { $DefaultColor = $Color[0] if ($null -ne $BackGroundColor -and $BackGroundColor.Count -ne $Color.Count) { Write-Error "Colors, BackGroundColors parameters count doesn't match. Terminated." return } if ($LinesBefore -ne 0) { for ($i = 0; $i -lt $LinesBefore; $i++) { Write-Host -Object "`n" -NoNewline } } if ($StartTab -ne 0) { for ($i = 0; $i -lt $StartTab; $i++) { Write-Host -Object "`t" -NoNewline } } if ($StartSpaces -ne 0) { for ($i = 0; $i -lt $StartSpaces; $i++) { Write-Host -Object ' ' -NoNewline } } if ($ShowTime) { Write-Host -Object "[$([datetime]::Now.ToString($DateTimeFormat))] " -NoNewline } if ($Text.Count -ne 0) { if ($Color.Count -ge $Text.Count) { if ($null -eq $BackGroundColor) { for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline } } else { for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline } } } else { if ($null -eq $BackGroundColor) { for ($i = 0; $i -lt $Color.Length ; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline } for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -NoNewline } } else { for ($i = 0; $i -lt $Color.Length ; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline } for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -BackgroundColor $BackGroundColor[0] -NoNewline } } } } if ($NoNewLine -eq $true) { Write-Host -NoNewline } else { Write-Host } if ($LinesAfter -ne 0) { for ($i = 0; $i -lt $LinesAfter; $i++) { Write-Host -Object "`n" -NoNewline } } } if ($Text.Count -and $LogFile) { $TextToFile = "" for ($i = 0; $i -lt $Text.Length; $i++) { $TextToFile += $Text[$i] } $Saved = $false $Retry = 0 Do { $Retry++ try { if ($LogTime) { "[$([datetime]::Now.ToString($DateTimeFormat))] $TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false } else { "$TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false } $Saved = $true } catch { if ($Saved -eq $false -and $Retry -eq $LogRetry) { Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Tried ($Retry/$LogRetry))" } else { Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Retrying... ($Retry/$LogRetry)" } } } Until ($Saved -eq $true -or $Retry -ge $LogRetry) } } function ConvertTo-OperatingSystem { <# .SYNOPSIS Allows easy conversion of OperatingSystem, Operating System Version to proper Windows 10 naming based on WMI or AD .DESCRIPTION Allows easy conversion of OperatingSystem, Operating System Version to proper Windows 10 naming based on WMI or AD .PARAMETER OperatingSystem Operating System as returned by Active Directory .PARAMETER OperatingSystemVersion Operating System Version as returned by Active Directory .EXAMPLE $Computers = Get-ADComputer -Filter * -Properties OperatingSystem, OperatingSystemVersion | ForEach-Object { $OPS = ConvertTo-OperatingSystem -OperatingSystem $_.OperatingSystem -OperatingSystemVersion $_.OperatingSystemVersion Add-Member -MemberType NoteProperty -Name 'OperatingSystemTranslated' -Value $OPS -InputObject $_ -Force $_ } $Computers | Select-Object DNS*, Name, SamAccountName, Enabled, OperatingSystem*, DistinguishedName | Format-Table .EXAMPLE $Registry = Get-PSRegistry -ComputerName 'AD1' -RegistryPath 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion' ConvertTo-OperatingSystem -OperatingSystem $Registry.ProductName -OperatingSystemVersion $Registry.CurrentBuildNumber .NOTES General notes #> [CmdletBinding()] param( [string] $OperatingSystem, [string] $OperatingSystemVersion ) if ($OperatingSystem -like 'Windows 10*' -or $OperatingSystem -like 'Windows 11*') { $Systems = @{ '10.0 (22621)' = 'Windows 11 22H2' '10.0 (22000)' = 'Windows 11 21H2' '10.0 (19045)' = 'Windows 10 22H2' '10.0 (19044)' = 'Windows 10 21H2' '10.0 (19043)' = 'Windows 10 21H1' '10.0 (19042)' = 'Windows 10 20H2' '10.0 (19041)' = 'Windows 10 2004' '10.0 (18898)' = 'Windows 10 Insider Preview' '10.0 (18363)' = "Windows 10 1909" '10.0 (18362)' = "Windows 10 1903" '10.0 (17763)' = "Windows 10 1809" '10.0 (17134)' = "Windows 10 1803" '10.0 (16299)' = "Windows 10 1709" '10.0 (15063)' = "Windows 10 1703" '10.0 (14393)' = "Windows 10 1607" '10.0 (10586)' = "Windows 10 1511" '10.0 (10240)' = "Windows 10 1507" '10.0.22621' = 'Windows 11 22H2' '10.0.22000' = 'Windows 11 21H2' '10.0.19045' = 'Windows 10 22H2' '10.0.19044' = 'Windows 10 21H2' '10.0.19043' = 'Windows 10 21H1' '10.0.19042' = 'Windows 10 20H2' '10.0.19041' = 'Windows 10 2004' '10.0.18898' = 'Windows 10 Insider Preview' '10.0.18363' = "Windows 10 1909" '10.0.18362' = "Windows 10 1903" '10.0.17763' = "Windows 10 1809" '10.0.17134' = "Windows 10 1803" '10.0.16299' = "Windows 10 1709" '10.0.15063' = "Windows 10 1703" '10.0.14393' = "Windows 10 1607" '10.0.10586' = "Windows 10 1511" '10.0.10240' = "Windows 10 1507" '22621' = 'Windows 11 22H2' '22000' = 'Windows 11 21H2' '19045' = 'Windows 10 22H2' '19044' = 'Windows 10 21H2' '19043' = 'Windows 10 21H1' '19042' = 'Windows 10 20H2' '19041' = 'Windows 10 2004' '18898' = 'Windows 10 Insider Preview' '18363' = "Windows 10 1909" '18362' = "Windows 10 1903" '17763' = "Windows 10 1809" '17134' = "Windows 10 1803" '16299' = "Windows 10 1709" '15063' = "Windows 10 1703" '14393' = "Windows 10 1607" '10586' = "Windows 10 1511" '10240' = "Windows 10 1507" } $System = $Systems[$OperatingSystemVersion] if (-not $System) { $System = $OperatingSystemVersion } } elseif ($OperatingSystem -like 'Windows Server*') { $Systems = @{ '10.0 (20348)' = 'Windows Server 2022' '10.0 (19042)' = 'Windows Server 2019 20H2' '10.0 (19041)' = 'Windows Server 2019 2004' '10.0 (18363)' = 'Windows Server 2019 1909' '10.0 (18362)' = "Windows Server 2019 1903" '10.0 (17763)' = "Windows Server 2019 1809" '10.0 (17134)' = "Windows Server 2016 1803" '10.0 (14393)' = "Windows Server 2016 1607" '6.3 (9600)' = 'Windows Server 2012 R2' '6.1 (7601)' = 'Windows Server 2008 R2' '5.2 (3790)' = 'Windows Server 2003' '10.0.20348' = 'Windows Server 2022' '10.0.19042' = 'Windows Server 2019 20H2' '10.0.19041' = 'Windows Server 2019 2004' '10.0.18363' = 'Windows Server 2019 1909' '10.0.18362' = "Windows Server 2019 1903" '10.0.17763' = "Windows Server 2019 1809" '10.0.17134' = "Windows Server 2016 1803" '10.0.14393' = "Windows Server 2016 1607" '6.3.9600' = 'Windows Server 2012 R2' '6.1.7601' = 'Windows Server 2008 R2' '5.2.3790' = 'Windows Server 2003' '20348' = 'Windows Server 2022' '19042' = 'Windows Server 2019 20H2' '19041' = 'Windows Server 2019 2004' '18363' = 'Windows Server 2019 1909' '18362' = "Windows Server 2019 1903" '17763' = "Windows Server 2019 1809" '17134' = "Windows Server 2016 1803" '14393' = "Windows Server 2016 1607" '9600' = 'Windows Server 2012 R2' '7601' = 'Windows Server 2008 R2' '3790' = 'Windows Server 2003' } $System = $Systems[$OperatingSystemVersion] if (-not $System) { $System = $OperatingSystemVersion } } else { $System = $OperatingSystem } if ($System) { $System } else { 'Unknown' } } function Copy-DictionaryManual { <# .SYNOPSIS Copies a dictionary recursively, handling nested dictionaries and lists. .DESCRIPTION This function copies a dictionary recursively, handling nested dictionaries and lists. It creates a deep copy of the input dictionary, ensuring that modifications to the copied dictionary do not affect the original dictionary. .PARAMETER Dictionary The dictionary to be copied. .EXAMPLE $originalDictionary = @{ 'Key1' = 'Value1' 'Key2' = @{ 'NestedKey1' = 'NestedValue1' } } $copiedDictionary = Copy-DictionaryManual -Dictionary $originalDictionary This example demonstrates how to copy a dictionary with nested values. #> [CmdletBinding()] param( [System.Collections.IDictionary] $Dictionary ) $clone = [ordered] @{} foreach ($Key in $Dictionary.Keys) { $value = $Dictionary.$Key $clonedValue = switch ($Dictionary.$Key) { { $null -eq $_ } { $null continue } { $_ -is [System.Collections.IDictionary] } { Copy-DictionaryManual -Dictionary $_ continue } { $type = $_.GetType() $type.IsPrimitive -or $type.IsValueType -or $_ -is [string] } { $_ continue } default { $_ | Select-Object -Property * } } if ($value -is [System.Collections.IList]) { $clone[$Key] = @($clonedValue) } else { $clone[$Key] = $clonedValue } } $clone } function Get-FileSize { <# .SYNOPSIS Get-FileSize function calculates the file size in human-readable format. .DESCRIPTION This function takes a file size in bytes and converts it into a human-readable format (e.g., KB, MB, GB, etc.). .PARAMETER Bytes Specifies the size of the file in bytes. .EXAMPLE Get-FileSize -Bytes 1024 Output: 1 KB .EXAMPLE Get-FileSize -Bytes 1048576 Output: 1 MB #> [CmdletBinding()] param( $Bytes ) $sizes = 'Bytes,KB,MB,GB,TB,PB,EB,ZB' -split ',' for ($i = 0; ($Bytes -ge 1kb) -and ($i -lt $sizes.Count); $i++) { $Bytes /= 1kb } $N = 2; if ($i -eq 0) { $N = 0 } return "{0:N$($N)} {1}" -f $Bytes, $sizes[$i] } function Get-GitHubLatestRelease { <# .SYNOPSIS Gets one or more releases from GitHub repository .DESCRIPTION Gets one or more releases from GitHub repository .PARAMETER Url Url to github repository .EXAMPLE Get-GitHubLatestRelease -Url "https://api.github.com/repos/evotecit/Testimo/releases" | Format-Table .NOTES General notes #> [CmdLetBinding()] param( [parameter(Mandatory)][alias('ReleasesUrl')][uri] $Url ) $ProgressPreference = 'SilentlyContinue' $Responds = Test-Connection -ComputerName $URl.Host -Quiet -Count 1 if ($Responds) { Try { [Array] $JsonOutput = (Invoke-WebRequest -Uri $Url -ErrorAction Stop | ConvertFrom-Json) foreach ($JsonContent in $JsonOutput) { [PSCustomObject] @{ PublishDate = [DateTime] $JsonContent.published_at CreatedDate = [DateTime] $JsonContent.created_at PreRelease = [bool] $JsonContent.prerelease Version = [version] ($JsonContent.name -replace 'v', '') Tag = $JsonContent.tag_name Branch = $JsonContent.target_commitish Errors = '' } } } catch { [PSCustomObject] @{ PublishDate = $null CreatedDate = $null PreRelease = $null Version = $null Tag = $null Branch = $null Errors = $_.Exception.Message } } } else { [PSCustomObject] @{ PublishDate = $null CreatedDate = $null PreRelease = $null Version = $null Tag = $null Branch = $null Errors = "No connection (ping) to $($Url.Host)" } } $ProgressPreference = 'Continue' } function Test-ComputerPort { <# .SYNOPSIS Tests the connectivity of a computer on specified TCP and UDP ports. .DESCRIPTION The Test-ComputerPort function tests the connectivity of a computer on specified TCP and UDP ports. It checks if the specified ports are open and reachable on the target computer. .PARAMETER ComputerName Specifies the name of the computer to test the port connectivity. .PARAMETER PortTCP Specifies an array of TCP ports to test connectivity. .PARAMETER PortUDP Specifies an array of UDP ports to test connectivity. .PARAMETER Timeout Specifies the timeout value in milliseconds for the connection test. Default is 5000 milliseconds. .EXAMPLE Test-ComputerPort -ComputerName "Server01" -PortTCP 80,443 -PortUDP 53 -Timeout 3000 Tests the connectivity of Server01 on TCP ports 80 and 443, UDP port 53 with a timeout of 3000 milliseconds. .EXAMPLE Test-ComputerPort -ComputerName "Server02" -PortTCP 3389 -PortUDP 123 Tests the connectivity of Server02 on TCP port 3389, UDP port 123 with the default timeout of 5000 milliseconds. #> [CmdletBinding()] param ( [alias('Server')][string[]] $ComputerName, [int[]] $PortTCP, [int[]] $PortUDP, [int]$Timeout = 5000 ) begin { if ($Global:ProgressPreference -ne 'SilentlyContinue') { $TemporaryProgress = $Global:ProgressPreference $Global:ProgressPreference = 'SilentlyContinue' } } process { foreach ($Computer in $ComputerName) { foreach ($P in $PortTCP) { $Output = [ordered] @{ 'ComputerName' = $Computer 'Port' = $P 'Protocol' = 'TCP' 'Status' = $null 'Summary' = $null 'Response' = $null } $TcpClient = Test-NetConnection -ComputerName $Computer -Port $P -InformationLevel Detailed -WarningAction SilentlyContinue if ($TcpClient.TcpTestSucceeded) { $Output['Status'] = $TcpClient.TcpTestSucceeded $Output['Summary'] = "TCP $P Successful" } else { $Output['Status'] = $false $Output['Summary'] = "TCP $P Failed" $Output['Response'] = $Warnings } [PSCustomObject]$Output } foreach ($P in $PortUDP) { $Output = [ordered] @{ 'ComputerName' = $Computer 'Port' = $P 'Protocol' = 'UDP' 'Status' = $null 'Summary' = $null } $UdpClient = [System.Net.Sockets.UdpClient]::new($Computer, $P) $UdpClient.Client.ReceiveTimeout = $Timeout $Encoding = [System.Text.ASCIIEncoding]::new() $byte = $Encoding.GetBytes("Evotec") [void]$UdpClient.Send($byte, $byte.length) $RemoteEndpoint = [System.Net.IPEndPoint]::new([System.Net.IPAddress]::Any, 0) try { $Bytes = $UdpClient.Receive([ref]$RemoteEndpoint) [string]$Data = $Encoding.GetString($Bytes) If ($Data) { $Output['Status'] = $true $Output['Summary'] = "UDP $P Successful" $Output['Response'] = $Data } } catch { $Output['Status'] = $false $Output['Summary'] = "UDP $P Failed" $Output['Response'] = $_.Exception.Message } $UdpClient.Close() $UdpClient.Dispose() [PSCustomObject]$Output } } } end { if ($TemporaryProgress) { $Global:ProgressPreference = $TemporaryProgress } } } function Test-WinRM { <# .SYNOPSIS Tests the WinRM connectivity on the specified computers. .DESCRIPTION The Test-WinRM function tests the WinRM connectivity on the specified computers and returns the status of the connection. .PARAMETER ComputerName Specifies the names of the computers to test WinRM connectivity on. .EXAMPLE Test-WinRM -ComputerName "Server01", "Server02" Tests the WinRM connectivity on Server01 and Server02. .EXAMPLE Test-WinRM -ComputerName "Server03" Tests the WinRM connectivity on Server03. #> [CmdletBinding()] param ( [alias('Server')][string[]] $ComputerName ) $Output = foreach ($Computer in $ComputerName) { $Test = [PSCustomObject] @{ Output = $null Status = $null ComputerName = $Computer } try { $Test.Output = Test-WSMan -ComputerName $Computer -ErrorAction Stop $Test.Status = $true } catch { $Test.Status = $false } $Test } $Output } function Add-ManagerInformation { [CmdletBinding()] param( [System.Collections.IDictionary] $SummaryDictionary, [string] $Type, [string] $ManagerType, [Object] $Key, [PSCustomObject] $User, [PSCustomObject] $Rule ) if ($Key) { if ($Key -is [string]) { $KeyDN = $Key } else { $KeyDN = $Key.DisplayName } if (-not $SummaryDictionary[$KeyDN]) { $SummaryDictionary[$KeyDN] = [ordered] @{ Manager = $Key ManagerDefault = [ordered] @{} ManagerNotCompliant = [ordered] @{} Security = [ordered] @{} } } $SummaryDictionary[$KeyDN][$Type][$User.DistinguishedName] = [ordered] @{ Manager = $User.ManagerDN User = $User Rule = $Rule ManagerOption = $Type Output = [ordered] @{} } $Default = [ordered] @{ DisplayName = $User.DisplayName Enabled = $User.Enabled SamAccountName = $User.SamAccountName Domain = $User.Domain DateExpiry = $User.DateExpiry DaysToExpire = $User.DaysToExpire PasswordLastSet = $User.PasswordLastSet PasswordExpired = $User.PasswordExpired } if ($Type -ne 'ManagerDefault') { $Extended = [ordered] @{ 'Status' = $ManagerType 'Manager' = $User.Manager 'Manager Email' = $User.ManagerEmail } $SummaryDictionary[$KeyDN][$Type][$User.DistinguishedName]['Output'] = [PSCustomObject] ( $Extended + $Default) } else { $SummaryDictionary[$KeyDN][$Type][$User.DistinguishedName]['Output'] = [PSCustomObject] $Default } } } function Add-ParametersToString { <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER String Parameter description .PARAMETER Parameter Parameter description .EXAMPLE $Test = 'this is a string $Test - and $Test2 AND $tEST3' Add-ParametersToString -String $Test -Parameter @{ Testooo = 'sdsds' Test = 'oh my god' Test2 = 'ole ole' TEST3 = '56555' } .NOTES General notes #> [CmdletBinding()] param( [string] $String, [System.Collections.IDictionary] $Parameter ) $Sorted = $Parameter.Keys | Sort-Object { $_.length } -Descending foreach ($Key in $Sorted) { $String = $String -ireplace [Regex]::Escape("`$$Key"), $Parameter[$Key] } $String } function Export-SearchInformation { [CmdletBinding()] param( [string] $SearchPath, [System.Collections.IDictionary] $SummarySearch, [string] $Today, [Array] $SummaryUsersEmails, [Array] $SummaryManagersEmails, [Array] $SummaryEscalationEmails ) if ($SearchPath) { Write-Color -Text "[i]" , " Saving Search report " -Color White, Yellow, Green if ($SummaryUsersEmails) { $SummarySearch['EmailSent'][$Today] += $SummaryUsersEmails } if ($SummaryEscalationEmails) { $SummarySearch['EmailEscalations'][$Today] += $SummaryEscalationEmails } if ($SummaryManagersEmails) { $SummarySearch['EmailManagers'][$Today] += $SummaryManagersEmails } try { $SummarySearch | Export-Clixml -LiteralPath $SearchPath -ErrorAction Stop } catch { Write-Color -Text "[e]", " Couldn't save to file $SearchPath", ". Error: ", $_.Exception.Message -Color White, Yellow, White, Yellow, White, Yellow, White } Write-Color -Text "[i]" , " Saving Search report ", "Done" -Color White, Yellow, Green } } function Import-SearchInformation { [CmdletBinding()] param( [string] $SearchPath ) if ($SearchPath) { if (Test-Path -LiteralPath $SearchPath) { try { Write-Color -Text "[i]", " Loading file ", $SearchPath -Color White, Yellow, White, Yellow, White, Yellow, White $SummarySearch = Import-Clixml -LiteralPath $SearchPath -ErrorAction Stop } catch { Write-Color -Text "[e]", " Couldn't load the file $SearchPath", ". Skipping...", $_.Exception.Message -Color White, Yellow, White, Yellow, White, Yellow, White } } } if (-not $SummarySearch) { $SummarySearch = [ordered] @{ EmailSent = [ordered] @{} EmailManagers = [ordered] @{} EmailEscalations = [ordered] @{} } } $SummarySearch } function Invoke-PasswordRuleProcessing { [CmdletBinding()] param( [System.Collections.IDictionary] $Rule, [System.Collections.IDictionary] $Summary, [System.Collections.IDictionary] $CachedUsers, [System.Collections.IDictionary] $AllSkipped, [System.Collections.IDictionary] $Locations, [System.Collections.IDictionary] $Logging, [System.Collections.IDictionary] $UsersExternalSystem, [DateTime] $TodayDate ) if ($Rule.Enable -eq $true) { Write-Color -Text "[i]", " Processing rule ", $Rule.Name, ' status: ', $Rule.Enable -Color Yellow, White, Green, White, Green, White, Green, White if (-not $Summary['Rules'][$Rule.Name] ) { $Summary['Rules'][$Rule.Name] = [ordered] @{} } $Rule.Reminders = $Rule.Reminders | ForEach-Object { $_ } foreach ($User in $CachedUsers.Values) { if ($User.Enabled -eq $false) { continue } if ($Rule.ExcludeOU.Count -gt 0) { $FoundOU = $false foreach ($OU in $Rule.ExcludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } if ($FoundOU) { continue } } if ($Rule.IncludeOU.Count -gt 0) { $FoundOU = $false foreach ($OU in $Rule.IncludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } if (-not $FoundOU) { continue } } if ($Rule.ExcludeGroup.Count -gt 0) { $FoundGroup = $false foreach ($Group in $Rule.ExcludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } if ($FoundGroup) { continue } } if ($Rule.IncludeGroup.Count -gt 0) { $FoundGroup = $false foreach ($Group in $Rule.IncludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } if (-not $FoundGroup) { continue } } if ($Rule.IncludeName.Count -gt 0) { $IncludeName = $false foreach ($Name in $Rule.IncludeName) { foreach ($Property in $Rule.IncludeNameProperties) { if ($User.$Property -like $Name) { $IncludeName = $true break } } if ($IncludeName) { break } } if (-not $IncludeName) { continue } } if ($Rule.ExcludeName.Count -gt 0) { $ExcludeName = $false foreach ($Name in $Rule.ExcludeName) { foreach ($Property in $Rule.ExcludeNameProperties) { if ($User.$Property -like $Name) { $ExcludeName = $true break } } if ($ExcludeName) { break } } if ($ExcludeName) { continue } } if ($Summary['Notify'][$User.DistinguishedName] -and $Summary['Notify'][$User.DistinguishedName].ProcessManagersOnly -ne $true) { continue } if ($Rule.IncludePasswordNeverExpires -and $Rule.IncludeExpiring) { if ($User.PasswordNeverExpires -eq $true) { $DaysToPasswordExpiry = $Rule.PasswordNeverExpiresDays - $User.PasswordDays $User.DaysToExpire = $DaysToPasswordExpiry } } elseif ($Rule.IncludeExpiring) { if ($User.PasswordNeverExpires -eq $true) { continue } } elseif ($Rule.IncludePasswordNeverExpires) { if ($User.PasswordNeverExpires -eq $true) { $DaysToPasswordExpiry = $Rule.PasswordNeverExpiresDays - $User.PasswordDays $User.DaysToExpire = $DaysToPasswordExpiry } else { continue } } else { Write-Color -Text "[i]", " Processing rule ", $Rule.Name, " doesn't include IncludePasswordNeverExpires nor IncludeExpiring so skipping." -Color Yellow, White, Green, White, Green, White, Green, White continue } if ($null -eq $User.DaysToExpire) { if ($Logging.NotifyOnUserDaysToExpireNull) { Write-Color -Text @( "[i]", " User ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire not set. ", "(", "Password Last Set: ", $User.PasswordLastSet, ")", " (Password at next logon: ", $User.PasswordAtNextLogon, ")" ) -Color Yellow, White, Yellow, White, Yellow, White, White, White, Yellow, DarkCyan, White, Yellow, DarkCyan, White } $AllSkipped[$User.DistinguishedName] = $User $Location = $User.OrganizationalUnit if (-not $Location) { $Location = 'Default' } if (-not $Locations[$Location]) { $Locations[$Location] = [PSCustomObject] @{ Location = $Location Count = 0 CountExpired = 0 Names = [System.Collections.Generic.List[string]]::new() NamesExpired = [System.Collections.Generic.List[string]]::new() } } if ($User.PasswordExpired) { $Locations[$Location].CountExpired++ $Locations[$Location].NamesExpired.Add($User.SamAccountName) } else { $Locations[$Location].Count++ $Locations[$Location].Names.Add($User.SamAccountName) } } if ($null -ne $User.DaysToExpire -and $User.DaysToExpire -in $Rule.Reminders) { if (-not $Rule.ProcessManagersOnly) { if ($Logging.NotifyOnUserMatchingRule) { Write-Color -Text "[i]", " User ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire, " " -Color Yellow, White, Yellow, White, Yellow, White, White, Blue } if ($Rule.OverwriteEmailProperty) { $NewPropertyWithEmail = $Rule.OverwriteEmailProperty if ($NewPropertyWithEmail -and $User.$NewPropertyWithEmail) { $User.EmailAddress = $User.$NewPropertyWithEmail } } if ($Rule.OverwriteEmailFromExternalUsers) { $ExternalUser = $null $ADProperty = $UsersExternalSystem.ActiveDirectoryProperty $EmailProperty = $UsersExternalSystem.EmailProperty $ExternalUser = $UsersExternalSystem['Users'][$User.$ADProperty] if ($ExternalUser -and $ExternalUser.$EmailProperty -like '*@*') { $User.EmailAddress = $ExternalUser.$EmailProperty } } $Summary['Notify'][$User.DistinguishedName] = [ordered] @{ User = $User Rule = $Rule ProcessManagersOnly = $Rule.ProcessManagersOnly } if ($Summary['Rules'][$Rule.Name][$User.DistinguishedName]) { $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleOptions.Add('User') $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleName = $Rule.Name } else { $Summary['Rules'][$Rule.Name][$User.DistinguishedName] = [ordered] @{ User = $User Rule = $Rule ProcessManagersOnly = $Rule.ProcessManagersOnly } $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleOptions.Add('User') $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleName = $Rule.Name } } } if ($Rule.OverwriteManagerProperty) { $NewPropertyWithManager = $Rule.OverwriteManagerProperty if ($NewPropertyWithManager -and $User.$NewPropertyWithManager) { $NewManager = $CachedUsers[$User.$NewPropertyWithManager] if ($NewManager -and $NewManager.Mail -like "*@*") { $User.ManagerEmail = $NewManager.Mail $User.Manager = $NewManager.DisplayName $User.ManagerSamAccountName = $NewManager.SamAccountName $User.ManagerEnabled = $NewManager.Enabled $User.ManagerLastLogon = $NewManager.LastLogonDate if ($User.ManagerLastLogon) { $User.ManagerLastLogonDays = $( - $($User.ManagerLastLogon - $Today).Days) } else { $User.ManagerLastLogonDays = $null } $User.ManagerType = $NewManager.ObjectClass $User.ManagerDN = $NewManager.DistinguishedName } } } if ($null -ne $User.DaysToExpire -and $Rule.SendToManager) { if ($Rule.SendToManager.Manager -and $Rule.SendToManager.Manager.Enable -eq $true -and $User.ManagerStatus -eq 'Enabled' -and $User.ManagerEmail -like "*@*") { $SendToManager = $true if ($Rule.SendToManager.Manager.IncludeOU.Count -gt 0) { $FoundOU = $false foreach ($OU in $Rule.SendToManager.Manager.IncludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } if (-not $FoundOU) { $SendToManager = $false } } if ($SendToManager -and $Rule.SendToManager.Manager.ExcludeOU.Count -gt 0) { $FoundOU = $false foreach ($OU in $Rule.SendToManager.Manager.ExcludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } if ($FoundOU) { $SendToManager = $false } } if ($SendToManager -and $Rule.SendToManager.Manager.ExcludeGroup.Count -gt 0) { $FoundGroup = $false foreach ($Group in $Rule.SendToManager.Manager.ExcludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } if ($FoundGroup) { $SendToManager = $false } } if ($SendToManager -and $Rule.SendToManager.Manager.IncludeGroup.Count -gt 0) { $FoundGroup = $false foreach ($Group in $Rule.SendToManager.Manager.IncludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } if (-not $FoundGroup) { $SendToManager = $false } } if ($SendToManager) { $SendToManager = $false if ($Rule.SendToManager.Manager.Reminders.Default.Enable -eq $true -and $null -eq $Rule.SendToManager.Manager.Reminders.Default.Reminder -and $User.DaysToExpire -in $Rule.Reminders) { $SendToManager = $true } elseif ($Rule.SendToManager.Manager.Reminders.Default.Enable -eq $true -and $Rule.SendToManager.Manager.Reminders.Default.Reminder -and $User.DaysToExpire -in $Rule.SendToManager.Manager.Reminders.Default.Reminder) { $SendToManager = $true } if (-not $SendToManager -and $Rule.SendToManager.Manager.Reminders.OnDay -and $Rule.SendToManager.Manager.Reminders.OnDay.Enable -eq $true) { foreach ($Day in $Rule.SendToManager.Manager.Reminders.OnDay.Days) { if ($Day -eq "$($TodayDate.DayOfWeek)") { if ($Rule.SendToManager.Manager.Reminders.OnDay.ComparisonType -eq 'lt') { if ($User.DaysToExpire -lt $Rule.SendToManager.Manager.Reminders.OnDay.Reminder) { $SendToManager = $true break } } elseif ($Rule.SendToManager.Manager.Reminders.OnDay.ComparisonType -eq 'gt') { if ($User.DaysToExpire -gt $Rule.SendToManager.Manager.Reminders.OnDay.Reminder) { $SendToManager = $true break } } elseif ($Rule.SendToManager.Manager.Reminders.OnDay.ComparisonType -eq 'eq') { if ($User.DaysToExpire -eq $Rule.SendToManager.Manager.Reminders.OnDay.Reminder) { $SendToManager = $true break } } elseif ($Rule.SendtoManager.Manager.Reminders.OnDay.ComparisonType -eq 'in') { if ($User.DaysToExpire -in $Rule.SendToManager.Manager.Reminders.OnDay.Reminder) { $SendToManager = $true break } } } } } if (-not $SendToManager -and $Rule.SendToManager.Manager.Reminders.OnDayOfMonth -and $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Enable -eq $true) { foreach ($Day in $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Days) { if ($Day -eq $TodayDate.Day) { if ($Rule.SendToManager.Manager.Reminders.OnDayOfMonth.ComparisonType -eq 'lt') { if ($User.DaysToExpire -lt $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Reminder) { $SendToManager = $true break } } elseif ($Rule.SendToManager.Manager.Reminders.OnDayOfMonth.ComparisonType -eq 'gt') { if ($User.DaysToExpire -gt $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Reminder) { $SendToManager = $true break } } elseif ($Rule.SendToManager.Manager.Reminders.OnDayOfMonth.ComparisonType -eq 'eq') { if ($User.DaysToExpire -eq $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Reminder) { $SendToManager = $true break } } elseif ($Rule.SendtoManager.Manager.Reminders.OnDayOfMonth.ComparisonType -eq 'in') { if ($User.DaysToExpire -in $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Reminder) { $SendToManager = $true break } } } } } if ($SendToManager) { if ($Logging.NotifyOnUserMatchingRuleForManager) { Write-Color -Text "[i]", " User (manager rule) ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire, " " -Color Yellow, White, Yellow, White, Yellow, White, White, Blue } if ($Summary['Rules'][$Rule.Name][$User.DistinguishedName]) { $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleOptions.Add('Manager') $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleName = $Rule.Name } else { $Summary['Rules'][$Rule.Name][$User.DistinguishedName] = [ordered] @{ User = $User Rule = $Rule ProcessManagersOnly = $Rule.ProcessManagersOnly } $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleOptions.Add('Manager') $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleName = $Rule.Name } $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifyManager'] Type = 'ManagerDefault' ManagerType = 'Ok' Key = $User.ManagerDN User = $User Rule = $Rule } Add-ManagerInformation @Splat } } } else { if ($Rule.SendToManager.Manager) { if ($Logging.NotifyOnUserMatchingRuleForManagerButNotCompliant) { Write-Color -Text "[i]", " User (manager rule) ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire, ", manager status: ", $User.ManagerStatus, ". Reason to skip: ", "No manager or manager is not enabled or manager has no email " -Color Yellow, White, Yellow, White, Yellow, White, White, Red, White, Red, White, Red } } } } if ($Rule.SendToManager -and $Rule.SendToManager.ManagerNotCompliant -and $Rule.SendToManager.ManagerNotCompliant.Enable -eq $true -and $Rule.SendToManager.ManagerNotCompliant.Manager) { if ($Rule.SendToManager.ManagerNotCompliant -and $Rule.SendToManager.ManagerNotCompliant.Enable -and $Rule.SendToManager.ManagerNotCompliant.Manager) { $ManagerNotCompliant = $true if ($Rule.SendToManager.ManagerNotCompliant.IncludeOU.Count -gt 0) { $FoundOU = $false foreach ($OU in $Rule.SendToManager.ManagerNotCompliant.IncludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } if (-not $FoundOU) { $ManagerNotCompliant = $false } } if ($ManagerNotCompliant -and $Rule.SendToManager.ManagerNotCompliant.ExcludeOU.Count -gt 0) { $FoundOU = $false foreach ($OU in $Rule.SendToManager.ManagerNotCompliant.ExcludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } if ($FoundOU) { $ManagerNotCompliant = $false } } if ($ManagerNotCompliant -and $Rule.SendToManager.ManagerNotCompliant.ExcludeGroup.Count -gt 0) { $FoundGroup = $false foreach ($Group in $Rule.SendToManager.ManagerNotCompliant.ExcludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } if ($FoundGroup) { $ManagerNotCompliant = $false } } if ($ManagerNotCompliant -and $Rule.SendToManager.ManagerNotCompliant.IncludeGroup.Count -gt 0) { $FoundGroup = $false foreach ($Group in $Rule.SendToManager.ManagerNotCompliant.IncludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } if (-not $FoundGroup) { $ManagerNotCompliant = $false } } if ($Rule.SendToManager.ManagerNotCompliant.Reminders) { $ManagerNotCompliant = $false if ($Rule.SendToManager.ManagerNotCompliant.Reminders.Default -and $Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Enable -eq $true) { $Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Reminder = $Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Reminder | ForEach-Object { $_ } if ($User.DaysToExpire -in $Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Reminder) { $ManagerNotCompliant = $true } } if ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay -and $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Enable -eq $true) { foreach ($Day in $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Days) { if ($Day -eq "$($TodayDate.DayOfWeek)") { if ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.ComparisonType -eq 'lt') { if ($User.DaysToExpire -lt $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Reminder) { $ManagerNotCompliant = $true break } } elseif ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.ComparisonType -eq 'gt') { if ($User.DaysToExpire -gt $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Reminder) { $ManagerNotCompliant = $true break } } elseif ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.ComparisonType -eq 'eq') { if ($User.DaysToExpire -eq $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Reminder) { $ManagerNotCompliant = $true break } } elseif ($Rule.SendtoManager.ManagerNotCompliant.Reminders.OnDay.ComparisonType -eq 'in') { if ($User.DaysToExpire -in $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Reminder) { $ManagerNotCompliant = $true break } } } } } if ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth -and $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Enable -eq $true) { foreach ($Day in $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Days) { if ($Day -eq $TodayDate.Day) { if ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.ComparisonType -eq 'lt') { if ($User.DaysToExpire -lt $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Reminder) { $ManagerNotCompliant = $true break } } elseif ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.ComparisonType -eq 'gt') { if ($User.DaysToExpire -gt $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Reminder) { $ManagerNotCompliant = $true break } } elseif ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.ComparisonType -eq 'eq') { if ($User.DaysToExpire -eq $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Reminder) { $ManagerNotCompliant = $true break } } elseif ($Rule.SendtoManager.ManagerNotCompliant.Reminders.OnDayOfMonth.ComparisonType -eq 'in') { if ($User.DaysToExpire -in $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Reminder) { $ManagerNotCompliant = $true break } } } } } } if ($ManagerNotCompliant -eq $true) { $ManagerNotCompliantMatched = $false if ($Rule.SendToManager.ManagerNotCompliant.MissingEmail -and $User.ManagerStatus -in 'Enabled, bad email', 'No email') { $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifyManager'] Type = 'ManagerNotCompliant' ManagerType = if ($User.ManagerStatus -eq 'Enabled, bad email') { 'Manager has bad email' } else { 'Manager has no email' } Key = $Rule.SendToManager.ManagerNotCompliant.Manager User = $User Rule = $Rule } Add-ManagerInformation @Splat $ManagerNotCompliantMatched = $true } elseif ($Rule.SendToManager.ManagerNotCompliant.Disabled -and $User.ManagerStatus -eq 'Disabled') { $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifyManager'] Type = 'ManagerNotCompliant' ManagerType = 'Manager disabled' Key = $Rule.SendToManager.ManagerNotCompliant.Manager User = $User Rule = $Rule } Add-ManagerInformation @Splat $ManagerNotCompliantMatched = $true } elseif ($Rule.SendToManager.ManagerNotCompliant.LastLogon -and $User.ManagerLastLogonDays -ge $Rule.SendToManager.ManagerNotCompliant.LastLogonDays) { $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifyManager'] Type = 'ManagerNotCompliant' ManagerType = 'Manager not logging in' Key = $Rule.SendToManager.ManagerNotCompliant.Manager User = $User Rule = $Rule } Add-ManagerInformation @Splat $ManagerNotCompliantMatched = $true } elseif ($Rule.SendToManager.ManagerNotCompliant.Missing -and $User.ManagerStatus -eq 'Missing') { $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifyManager'] Type = 'ManagerNotCompliant' ManagerType = 'Manager not set' Key = $Rule.SendToManager.ManagerNotCompliant.Manager User = $User Rule = $Rule } Add-ManagerInformation @Splat $ManagerNotCompliantMatched = $true } if ($ManagerNotCompliantMatched) { if ($Logging.NotifyOnUserMatchingRuleForManagerNotCompliant) { Write-Color -Text "[i]", " User (manager not compliant rule) ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire, " " -Color Yellow, White, Yellow, White, Yellow, White, White, Blue } if ($Summary['Rules'][$Rule.Name][$User.DistinguishedName]) { $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleOptions.Add('Manager Not Compliant') $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleName = $Rule.Name } else { $Summary['Rules'][$Rule.Name][$User.DistinguishedName] = [ordered] @{ User = $User Rule = $Rule ProcessManagersOnly = $Rule.ProcessManagersOnly } $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleOptions.Add('Manager Not Compliant') $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleName = $Rule.Name } } else { if ($User.ManagerStatus -eq 'Enabled') { } else { if ($Logging.NotifyOnUserMatchingRuleForManagerNotCompliant) { Write-Color -Text "[i]", " User (manager not compliant rule not processed) ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire, " manager status: ", $User.ManagerStatus -Color Yellow, White, Yellow, White, Yellow, White, White, Blue } } } } } } if ($null -ne $User.DaysToExpire -and $Rule.SendToManager -and $Rule.SendToManager.SecurityEscalation -and $Rule.SendToManager.SecurityEscalation.Enable -eq $true -and $Rule.SendToManager.SecurityEscalation.Manager) { $SecurityEscalation = $true if ($Rule.SendToManager.SecurityEscalation.IncludeOU.Count -gt 0) { $FoundOU = $false foreach ($OU in $Rule.SendToManager.SecurityEscalation.IncludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } if (-not $FoundOU) { $SecurityEscalation = $false } } if ($SecurityEscalation -and $Rule.SendToManager.SecurityEscalation.ExcludeOU.Count -gt 0) { $FoundOU = $false foreach ($OU in $Rule.SendToManager.SecurityEscalation.ExcludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } if ($FoundOU) { $SecurityEscalation = $false } } if ($SecurityEscalation -and $Rule.SendToManager.SecurityEscalation.ExcludeGroup.Count -gt 0) { $FoundGroup = $false foreach ($Group in $Rule.SendToManager.SecurityEscalation.ExcludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } if ($FoundGroup) { $SecurityEscalation = $false } } if ($SecurityEscalation -and $Rule.SendToManager.SecurityEscalation.IncludeGroup.Count -gt 0) { $FoundGroup = $false foreach ($Group in $Rule.SendToManager.SecurityEscalation.IncludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } if (-not $FoundGroup) { $SecurityEscalation = $false } } if ($Rule.SendToManager.SecurityEscalation.Reminders) { $SecurityEscalation = $false if ($Rule.SendToManager.SecurityEscalation.Reminders.Default -and $Rule.SendToManager.SecurityEscalation.Reminders.Default.Enable -eq $true) { $Rule.SendToManager.SecurityEscalation.Reminders.Default.Reminder = $Rule.SendToManager.SecurityEscalation.Reminders.Default.Reminder | ForEach-Object { $_ } if ($User.DaysToExpire -in $Rule.SendToManager.SecurityEscalation.Reminders.Default.Reminder) { $SecurityEscalation = $true } } if ($Rule.SendToManager.SecurityEscalation.Reminders.OnDay -and $Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Enable -eq $true) { foreach ($Day in $Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Days) { if ($Day -eq "$($TodayDate.DayOfWeek)") { if ($Rule.SendToManager.SecurityEscalation.Reminders.OnDay.ComparisonType -eq 'lt') { if ($User.DaysToExpire -lt $Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Reminder) { $SecurityEscalation = $true break } } elseif ($Rule.SendToManager.SecurityEscalation.Reminders.OnDay.ComparisonType -eq 'gt') { if ($User.DaysToExpire -gt $Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Reminder) { $SecurityEscalation = $true break } } elseif ($Rule.SendToManager.SecurityEscalation.Reminders.OnDay.ComparisonType -eq 'eq') { if ($User.DaysToExpire -eq $Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Reminder) { $SecurityEscalation = $true break } } elseif ($Rule.SendtoManager.SecurityEscalation.Reminders.OnDay.ComparisonType -eq 'in') { if ($User.DaysToExpire -in $Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Reminder) { $SecurityEscalation = $true break } } } } } if ($Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth -and $Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Enable -eq $true) { foreach ($Day in $Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Days) { if ($Day -eq $TodayDate.Day) { if ($Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.ComparisonType -eq 'lt') { if ($User.DaysToExpire -lt $Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Reminder) { $SecurityEscalation = $true break } } elseif ($Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.ComparisonType -eq 'gt') { if ($User.DaysToExpire -gt $Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Reminder) { $SecurityEscalation = $true break } } elseif ($Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.ComparisonType -eq 'eq') { if ($User.DaysToExpire -eq $Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Reminder) { $SecurityEscalation = $true break } } elseif ($Rule.SendtoManager.SecurityEscalation.Reminders.OnDayOfMonth.ComparisonType -eq 'in') { if ($User.DaysToExpire -in $Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Reminder) { $SecurityEscalation = $true break } } } } } } if ($SecurityEscalation) { if ($Logging.NotifyOnUserMatchingRuleForSecurityEscalation) { Write-Color -Text "[i]", " User (security escalation) ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire, " " -Color Yellow, White, Yellow, White, Yellow, White, White, Blue } if ($Summary['Rules'][$Rule.Name][$User.DistinguishedName]) { $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleOptions.Add('Security esclation') $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleName = $Rule.Name } else { $Summary['Rules'][$Rule.Name][$User.DistinguishedName] = [ordered] @{ User = $User Rule = $Rule ProcessManagersOnly = $Rule.ProcessManagersOnly } $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleOptions.Add('Security esclation') $Summary['Rules'][$Rule.Name][$User.DistinguishedName].User.RuleName = $Rule.Name } $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifySecurity'] Type = 'Security' ManagerType = 'Escalation' Key = $Rule.SendToManager.SecurityEscalation.Manager User = $User Rule = $Rule } Add-ManagerInformation @Splat } } } } else { if ($null -ne $Rule.Name -and $null -ne $Rule.Enable) { Write-Color -Text "[i]", " Processing rule ", $Rule.Name, ' status: ', $Rule.Enable -Color Red, White, Red, White, Red, White, Red, White } } } function New-HTMLReport { [CmdletBinding()] param( [System.Collections.IDictionary] $Report, [System.Collections.IDictionary] $EmailParameters, [System.Collections.IDictionary] $Logging, [string] $SearchPath, [Array] $Rules, [System.Collections.IDictionary] $UserSection, [System.Collections.IDictionary] $ManagerSection, [System.Collections.IDictionary] $SecuritySection, [System.Collections.IDictionary] $AdminSection, [System.Collections.IDictionary] $CachedUsers, [System.Collections.IDictionary] $Summary, [Array] $SummaryUsersEmails, [Array] $SummaryManagersEmails, [Array] $SummaryEscalationEmails, [System.Collections.IDictionary] $SummarySearch, [System.Collections.IDictionary] $Locations, [System.Collections.IDictionary] $AllSkipped, [System.Collections.IDictionary] $ExternalSystemReplacements ) $TranslateOperators = @{ 'lt' = 'Less than' 'gt' = 'Greater than' 'eq' = 'Equal to' 'ne' = 'Not equal to' 'le' = 'Less than or equal to' 'ge' = 'Greater than or equal to' 'in' = 'In' } Write-Color -Text "[i]", " Generating HTML report ", $Report.Title -Color White, Yellow, Green if ($Report.DisableWarnings -eq $true) { $WarningAction = 'SilentlyContinue' } else { $WarningAction = 'Continue' } if (-not $Report.Title) { $Report.Title = "Password Solution Report" } New-HTML { New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow New-HTMLPanelStyle -BorderRadius 0px New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ', ' -ArrayJoin New-HTMLHeader { New-HTMLSection -Invisible { New-HTMLSection { New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue } -JustifyContent flex-start -Invisible New-HTMLSection { New-HTMLText -Text "Password Solution - $($Script:Reporting['Version'])" -Color Blue } -JustifyContent flex-end -Invisible } } if ($Report.ShowConfiguration) { New-HTMLTab -Name "About" { New-HTMLTab -Name "Configuration" { New-HTMLSection -Invisible { New-HTMLSection -HeaderText "Email Configuration" { New-HTMLList { foreach ($Key in $EmailParameters.Keys) { if ($Key -eq 'Body') { } elseif ($Key -ne 'Password') { New-HTMLListItem -Text $Key, ": ", $EmailParameters[$Key] -FontWeight normal, normal, bold } else { New-HTMLListItem -Text $Key, ": ", "REDACTED" -FontWeight normal, normal, bold } } } } New-HTMLSection -HeaderText "Logging" { New-HTMLList { foreach ($Key in $Logging.Keys) { if ($Key -ne 'Password') { New-HTMLListItem -Text $Key, ": ", $Logging[$Key] -FontWeight normal, normal, bold } else { New-HTMLListItem -Text $Key, ": ", "REDACTED" -FontWeight normal, normal, bold } } } } New-HTMLSection -HeaderText "Other" { New-HTMLList { New-HTMLListItem -Text 'FilePath', ": ", $Report.FilePath -FontWeight normal, normal, bold New-HTMLListItem -Text 'SearchPath', ": ", $SearchPath -FontWeight normal, normal, bold } } } New-HTMLSection -Invisible { New-HTMLSection -HeaderText "User Section" { New-HTMLList { New-HTMLListItem -Text "Enabled: ", $UserSection.Enable -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "SendCountMaximum: ", $UserSection.SendCountMaximum -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "SendToDefaultEmail: ", $UserSection.SendToDefaultEmail -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "DefaultEmail: ", ($UserSection.DefaultEmail -join ", ") -FontWeight normal, bold -TextDecoration underline, none } } New-HTMLSection -HeaderText "Manager Section" { New-HTMLList { New-HTMLListItem -Text "Enabled: ", $ManagerSection.Enable -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "SendCountMaximum: ", $ManagerSection.SendCountMaximum -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "DefaultEmail: ", ($ManagerSection.DefaultEmail -join ", ") -FontWeight normal, bold -TextDecoration underline, none } } New-HTMLSection -HeaderText "Security Section" { New-HTMLList { New-HTMLListItem -Text "Enabled: ", $SecuritySection.Enable -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "SendCountMaximum: ", $SecuritySection.SendCountMaximum -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "SendToDefaultEmail: ", $SecuritySection.SendToDefaultEmail -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "DefaultEmail: ", ($SecuritySection.DefaultEmail -join ", ") -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "Attach CSV: ", ($SecuritySection.AttachCSV -join ",") -FontWeight normal, bold -TextDecoration underline, none } } New-HTMLSection -HeaderText "Admin Section" { New-HTMLList { New-HTMLListItem -Text "Enabled: ", $AdminSection.Enable -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "Subject: ", $AdminSection.Subject -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "Manager: ", $AdminSection.Manager.DisplayName -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "Manager Email: ", ($AdminSection.Manager.EmailAddress -join ", ") -FontWeight normal, bold -TextDecoration underline, none } } } } New-HTMLTab -Name 'Rules Configuration' { New-HTMLText -Text "There are ", $Rules.Count, " rules defined in the Password Solution. ", "Please keep in mind that order of the rules matter." -FontWeight normal, bold, normal -Color None, Blue, None foreach ($Rule in $Rules) { if ($Rule.Enable) { $SectionColor = 'SpringGreen' } else { $SectionColor = 'Coral' } New-HTMLSection -HeaderText "Rule $($Rule.Name)" -CanCollapse -HeaderBackGroundColor $SectionColor { New-HTMLList { if ($Rule.Enable) { New-HTMLListItem -Text "Rule ", $Rule.Name, " is ", "enabled" -FontWeight normal, bold, normal, bold, normal, normal -Color None, None, None, Green } else { New-HTMLListItem -Text "Rule ", $Rule.Name, " is ", "disabled" -FontWeight normal, bold, normal, bold, normal, normal -Color None, None, None, Red } New-HTMLList { New-HTMLListItem -Text "Notify till expiry on ", $($Rule.Reminders -join ","), " day " -FontWeight normal, bold, normal if ($Rule.IncludeExpiring) { New-HTMLListItem -Text "Include expiring accounts is ", "enabled" -FontWeight bold, bold -Color None, Green } else { New-HTMLListItem -Text "Include expiring accounts is ", "disabled" -FontWeight bold, bold -Color None, Red } if ($Rule.IncludePasswordNeverExpires) { New-HTMLListItem -Text "Include passwords never expiring with ", $Rule.PasswordNeverExpiresDays, " days rule" -FontWeight bold -Color Amethyst } else { New-HTMLListItem -Text "Do not include passwords that never expire." -FontWeight bold -Color Blue } if ($Rule.IncludeName.Count -gt 0 -and $Rule.IncludeNameProperties.Count -gt 0) { New-HTMLListItem -Text "Apply naming rule to require that account contains of of names ", $($Rule.IncludeName -join ", "), " in at least one property ", ($Rule.IncludeNameProperties -join ", ") -FontWeight normal, bold, normal, bold, normal -Color None, Blue, None, Blue } else { New-HTMLListItem -Text "Do not apply special name rules" -Color Blue -FontWeight bold } if ($Rule.IncludeOU) { New-HTMLListItem -Text "Apply Organizational Unit inclusion on ", ($Rule.IncludeOU -join ", ") -FontWeight normal, bold -Color None, Blue } else { New-HTMLListItem -Text "Do not apply Organizational Unit limit" -Color Blue -FontWeight bold } if ($Rule.ExcludeOU) { New-HTMLListItem -Text "Apply Organizational Unit exclusion on ", $Rule.ExcludeOU -FontWeight normal, bold -Color None, Green } else { New-HTMLListItem -Text "Do not exclude any Organizational Unit" -Color Blue -FontWeight bold } if ($Rule.IncludeGroup) { New-HTMLListItem -Text "Appply Group Membership inclusion (direct only) ", ($Rule.IncludeGroup -join ", ") } else { New-HTMLListItem -Text "Do not apply Group Membership limit" } if ($Rule.ExcludeGroup) { New-HTMLListItem -Text "Apply Group Membership exclusion (direct only): ", ($Rule.ExcludeGroup -join ", ") } else { New-HTMLListItem -Text "Do not apply Group Membership exclusion" } New-HTMLListItem -Text "Send to manager" -NestedListItems { New-HTMLList { if ($Rule.SendToManager.Manager.Enable) { New-HTMLListItem -Text "Manager ", " is ", 'enabled' -FontWeight bold, normal, bold -Color None, None, Green { New-HTMLList { New-HTMLListItem -Text "Rules: " { New-HTMLList { if ($Rule.SendToManager.Manager.Reminders.Default.Enable) { if ($Rule.SendToManager.Manager.Reminders.Default.Reminder) { New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $($Rule.SendToManager.Manager.Reminders.Default.Reminder -join ", "), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green } else { New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $($Rule.Reminders -join ", "), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green } } else { New-HTMLListItem -Text "Default rule is ", "disabled" -FontWeight bold, bold -Color None, Red } if ($Rule.SendToManager.Manager.Reminders.OnDay.Enable) { New-HTMLListItem -Text @( "On day of the week ", "is ", "enabled" " on days: ", ($Rule.SendToManager.Manager.Reminders.OnDay.Days -join ", "), " with comparison ", $TranslateOperators[$Rule.SendToManager.Manager.Reminders.OnDay.ComparisonType], ' value ', $Rule.SendToManager.Manager.Reminders.OnDay.Reminder ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green } else { New-HTMLListItem -Text "On day of week rule is ", "disabled" -FontWeight bold, bold -Color None, Red } if ($Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Enable) { New-HTMLListItem -Text @( "On day of the month rule ", "is", " enabled", " on days ", ($Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Days -join ","), " with comparison ", $TranslateOperators[$Rule.SendToManager.Manager.Reminders.OnDayOfMonth.ComparisonType], ' value ', $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Reminder ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green } else { New-HTMLListItem -Text "On day of month rule is ", "disabled" -FontWeight bold, bold -Color None, Red } } } } } } else { New-HTMLListItem -Text "Manager ", " is ", 'disabled' -FontWeight bold, normal, bold -Color None, None, Red } if ($Rule.SendToManager.ManagerNotCompliant.Enable) { New-HTMLListItem -Text "Manager Escalation", " is ", 'enabled' -FontWeight bold, normal, bold -Color None, None, Green { New-HTMLList { New-HTMLListItem -Text "Manager Name: ", $Rule.SendToManager.ManagerNotCompliant.Manager.DisplayName -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "Manager Email Address: ", $Rule.SendToManager.ManagerNotCompliant.Manager.EmailAddress -FontWeight normal, bold -TextDecoration underline, none } New-HTMLList { New-HTMLListItem -Text "Rules: " { New-HTMLList { if ($Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Enable) { if ($Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Reminder) { New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $($Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Reminder -join ", "), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green } else { New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $($Rule.Reminders -join ", "), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green } } else { New-HTMLListItem -Text "Default rule is ", "disabled" -FontWeight bold, bold -Color None, Red } if ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Enable) { New-HTMLListItem -Text @( "On day of the week ", "is ", "enabled" " on days: ", ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Days -join ", "), " with comparison ", $TranslateOperators[$Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.ComparisonType], ' value ', $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Reminder ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green } else { New-HTMLListItem -Text "On day of week rule is ", "disabled" -FontWeight bold, bold -Color None, Red } if ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Enable) { New-HTMLListItem -Text @( "On day of the month rule ", "is", " enabled", " on days ", ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Days -join ", "), " with comparison ", $TranslateOperators[$Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.ComparisonType], ' value ', $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Reminder ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green } else { New-HTMLListItem -Text "On day of month rule is ", "disabled" -FontWeight bold, bold -Color None, Red } } } } } } else { New-HTMLListItem -Text "Manager Escalation", " is ", "disabled" -FontWeight bold, normal, bold -Color None, None, Red } if ($Rule.SendToManager.SecurityEscalation.Enable) { New-HTMLListItem -Text "Security Escalation ", "is", " enabled" -FontWeight bold, normal, bold -Color None, None, Green { New-HTMLList { New-HTMLListItem -Text "Manager Name: ", $Rule.SendToManager.SecurityEscalation.Manager.DisplayName -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "Manager Email Address: ", $Rule.SendToManager.SecurityEscalation.Manager.EmailAddress -FontWeight normal, bold -TextDecoration underline, none } New-HTMLList { New-HTMLListItem -Text "Rules: " { New-HTMLList { if ($Rule.SendToManager.SecurityEscalation.Reminders.Default.Enable) { if ($Rule.SendToManager.SecurityEscalation.Reminders.Default.Reminder) { New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $($Rule.SendToManager.SecurityEscalation.Reminders.Default.Reminder -join ", "), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green } else { New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $($Rule.Reminders -join ", "), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green } } else { New-HTMLListItem -Text "Default rule is ", "disabled" -FontWeight bold, bold -Color None, Red } if ($Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Enable) { New-HTMLListItem -Text @( "On day of the week ", "is ", "enabled" " on days: ", ($Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Days -join ", "), " with comparison ", $TranslateOperators[$Rule.SendToManager.SecurityEscalation.Reminders.OnDay.ComparisonType], ' value ', $Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Reminder ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green } else { New-HTMLListItem -Text "On day of week rule is ", "disabled" -FontWeight bold, bold -Color None, Red } if ($Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Enable) { New-HTMLListItem -Text @( "On day of the month rule ", "is", " enabled", " on days ", ($Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Days -join ", "), " with comparison ", $TranslateOperators[$Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.ComparisonType], ' value ', $Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Reminder ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green } else { New-HTMLListItem -Text "On day of month rule is ", "disabled" -FontWeight bold, bold -Color None, Red } } } } } } else { New-HTMLListItem -Text "Security Escalation", " is ", "disabled" -FontWeight bold, normal, bold -Color None, None, Red } } } } } } } } } } if ($Report.ShowAllUsers) { $AllUsers = foreach ($User in $CachedUsers.Values) { if ($User.Type -eq 'Contact') { continue } $User } New-HTMLTab -Name 'All Users' { New-HTMLTable -DataTable $AllUsers -Filtering { New-TableCondition -Name 'Enabled' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq New-TableCondition -Name 'HasMailbox' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor BlueSmoke -Value $true -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Missing', 'Disabled' -BackgroundColor Salmon -Operator in New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Enabled' -BackgroundColor LawnGreen New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Not available' -BackgroundColor BlueSmoke } } } if ($Report.ShowRules) { if ($Report.NestedRules) { if ($Summary['Rules'].Keys.Count -gt 0) { New-HTMLTab -Name 'Rules Information' { foreach ($Rule in $Summary['Rules'].Keys) { if ((Measure-Object -InputObject $Summary['Rules'][$Rule].Values.User).Count -gt 0) { $Color = 'LawnGreen' $IconSolid = 'Star' } else { $Color = 'Salmon' $IconSolid = 'Stop' } New-HTMLTab -Name $Rule -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $Summary['Rules'][$Rule].Values.User -Filtering { New-TableCondition -Name 'Enabled' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string New-TableCondition -Name 'HasMailbox' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor BlueSmoke -Value $true -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Missing', 'Disabled' -BackgroundColor Salmon -Operator in New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Enabled' -BackgroundColor LawnGreen New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Not available' -BackgroundColor BlueSmoke } } } } } } else { foreach ($Rule in $Summary['Rules'].Keys) { if ((Measure-Object -InputObject $Summary['Rules'][$Rule].Values.User).Count -gt 0) { $Color = 'LawnGreen' $IconSolid = 'Star' } else { $Color = 'Salmon' $IconSolid = 'Stop' } New-HTMLTab -Name $Rule -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $Summary['Rules'][$Rule].Values.User -Filtering { New-TableCondition -Name 'Enabled' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string New-TableCondition -Name 'HasMailbox' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor BlueSmoke -Value $true -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Missing', 'Disabled' -BackgroundColor Salmon -Operator in New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Enabled' -BackgroundColor LawnGreen New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Not available' -BackgroundColor BlueSmoke } } } } } if ($Report.ShowUsersSent) { if ((Measure-Object -InputObject $SummaryUsersEmails).Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'Email sent to users' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $SummaryUsersEmails { New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string } -Filtering } } if ($Report.ShowManagersSent) { if ((Measure-Object -InputObject $SummaryManagersEmails).Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'Email sent to manager' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $SummaryManagersEmails { New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' } -Filtering } } if ($Report.ShowEscalationSent) { if ((Measure-Object -InputObject $SummaryEscalationEmails).Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'Email sent to Security' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $SummaryEscalationEmails { New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' } -Filtering } } if ($Report.ShowExternalSystemReplacementsUsers) { if ($ExternalSystemReplacements.Users.Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'External System Users' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $ExternalSystemReplacements.Users { } -Filtering } } if ($Report.ShowExternalSystemReplacementsManagers) { if ($ExternalSystemReplacements.Managers.Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'External System Managers' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $ExternalSystemReplacements.Managers { } -Filtering } } if ($Report.ShowSearchUsers) { [Array] $UsersSent = $SummarySearch['EmailSent'].Values if ($UsersSent.Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'History Emails To Users' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $UsersSent { New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string } -Filtering } } if ($Report.ShowSearchManagers) { [Array] $ShowSearchManagers = $SummarySearch['EmailManagers'].Values if ($ShowSearchManagers.Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'History Emails To Managers' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $ShowSearchManagers { New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' } -Filtering } } if ($Report.ShowSearchEscalations) { [Array] $ShowSearchEscalations = $SummarySearch['EmailEscalations'].Values if ($ShowSearchEscalations.Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'History Email To Security' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $ShowSearchEscalations { New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' } -Filtering } } if ($Report.ShowSkippedUsers) { New-HTMLTab -Name 'Skipped Users' -IconSolid users { $SkippedUsers = foreach ($User in $AllSkipped.Values) { if ($User.Type -ne 'Contact') { $User } } New-HTMLPanel -AlignContentText center { New-HTMLText -FontSize 15pt -Text "Those users have no password date set. This means account running expiration checks doesn't have permissions or acccout never had password set or account is set to change password on logon. " } -Invisible New-HTMLTable -DataTable $SkippedUsers -Filtering { New-TableCondition -Name 'Enabled' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq New-TableCondition -Name 'HasMailbox' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor BlueSmoke -Value $true -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Missing', 'Disabled' -BackgroundColor Salmon -Operator in New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Enabled' -BackgroundColor LawnGreen New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Not available' -BackgroundColor BlueSmoke } } } if ($Report.ShowSkippedLocations) { New-HTMLTab -Name 'Skipped Locations' -IconSolid building { New-HTMLPanel -AlignContentText center { New-HTMLText -FontSize 15pt -Text "Users in those Organizational Units have no password date set. This means account running expiration checks doesn't have permissions or acccout never had password set or account is set to change password on logon. " } -Invisible New-HTMLTable -DataTable $Locations.Values -Filtering { New-TableHeader -ResponsiveOperations none -Names 'Names', 'NamesExpired' } } } } -ShowHTML:$Report.ShowHTML -FilePath $Report.FilePath -Online:$Report.Online -WarningAction $WarningAction -TitleText $Report.Title Write-Color -Text "[i]" , " Generating HTML report ", $Report.Title, ". Done" -Color White, Yellow, Green } function Send-PasswordAdminNotifications { [CmdletBinding()] param( $AdminSection, $TemplateAdmin, $TemplateAdminSubject, $TimeEnd, $EmailParameters, $HtmlAttachments, $Logging ) if ($AdminSection.Enable) { Write-Color -Text "[i] Sending summary information " -Color White, Yellow, White, Yellow, White, Yellow, White $CountSecurity = 0 [Array] $SummaryEmail = @( $CountSecurity++ $ManagerUser = $AdminSection.Manager $EmailSplat = [ordered] @{} $EmailSplat.Template = $TemplateAdmin $EmailSplat.Subject = $TemplateAdminSubject $EmailSplat.User = $ManagerUser $EmailSplat.SummaryUsersEmails = $SummaryUsersEmails $EmailSplat.SummaryManagersEmails = $SummaryManagersEmails $EmailSplat.SummaryEscalationEmails = $SummaryEscalationEmails $EmailSplat.TimeToProcess = $TimeEnd $EmailSplat.EmailParameters = $EmailParameters $EmailSplat.EmailParameters.To = $AdminSection.Manager.EmailAddress $EmailSplat.EmailDateFormat = $Logging.EmailDateFormat $EmailSplat.EmailDateFormatUTCConversion = $Logging.EmailDateFormatUTCConversion if ($HtmlAttachments.Count -gt 0) { $EmailSplat.Attachments = $HtmlAttachments } Write-Color -Text "[i] Sending summary information ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow $EmailResult = Send-PasswordEmail @EmailSplat Write-Color -Text "[r] Sending summary information ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ") (status: ", $EmailResult.Status, " sent to: ", $EmailResult.SentTo, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow if ($EmailResult.Error) { Write-Color -Text "[r] Error: ", $EmailResult.Error -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } [PSCustomObject] @{ DisplayName = $ManagerUser.DisplayName SamAccountName = $ManagerUser.SamAccountName Domain = $ManagerUser.Domain Status = $EmailResult.Status StatusWhen = Get-Date -Format "yyyy-MM-dd HH:mm:ss" SentTo = $EmailResult.SentTo StatusError = $EmailResult.Error Template = 'Unknown' } ) Write-Color -Text "[i] Sending summary information (sent: ", $SummaryEmail.Count, ")" -Color White, Yellow, White, Yellow, White, Yellow, White } else { Write-Color -Text "[i] Sending summary information is ", "disabled!" -Color White, Yellow, DarkMagenta } } function Send-PasswordEmail { [CmdletBinding()] param( [scriptblock] $Template, [PSCustomObject] $User, [Array] $ManagedUsers, [Array] $ManagedUsersManagerNotCompliant, [Array] $SummaryUsersEmails, [Array] $SummaryManagersEmails, [Array] $SummaryEscalationEmails, [string] $TimeToProcess, [Array] $Attachments, [System.Collections.IDictionary] $EmailParameters, [string] $Subject, [string] $EmailDateFormat, [switch] $EmailDateFormatUTCConversion ) if ($Template) { if ($User.PasswordLastSet) { if ($EmailDateFormat) { if ($EmailDateFormatUTCConversion) { $PasswordLastSet = $User.PasswordLastSet.ToUniversalTime().ToString($EmailDateFormat) } else { $PasswordLastSet = $User.PasswordLastSet.ToString($EmailDateFormat) } } else { if ($EmailDateFormatUTCConversion) { $PasswordLastSet = $User.PasswordLastSet.ToUniversalTime() } else { $PasswordLastSet = $User.PasswordLastSet } } } else { $PasswordLastSet = $User.PasswordLastSet } if ($User.DateExpiry) { if ($EmailDateFormat) { if ($EmailDateFormatUTCConversion) { $ExpiryDate = $User.DateExpiry.ToUniversalTime().ToString($EmailDateFormat) } else { $ExpiryDate = $User.DateExpiry.ToString($EmailDateFormat) } } else { if ($EmailDateFormatUTCConversion) { $ExpiryDate = $User.DateExpiry.ToUniversalTime() } else { $ExpiryDate = $User.DateExpiry } } } else { $ExpiryDate = $User.DateExpiry } $SourceParameters = [ordered] @{ ManagerDisplayName = $User.DisplayName ManagerUsersTable = $ManagedUsers ManagerUsersTableManagerNotCompliant = $ManagedUsersManagerNotCompliant SummaryEscalationEmails = $SummaryEscalationEmails SummaryManagersEmails = $SummaryManagersEmails SummaryUsersEmails = $SummaryUsersEmails TimeToProcess = $TimeToProcess UserPrincipalName = $User.UserPrincipalName SamAccountName = $User.SamAccountName Domain = $User.Domain Enabled = $User.Enabled EmailAddress = $User.EmailAddress DateExpiry = $ExpiryDate DaysToExpire = $User.DaysToExpire PasswordExpired = $User.PasswordExpired PasswordLastSet = $PasswordLastSet PasswordNotRequired = $User.PasswordNotRequired PasswordNeverExpires = $User.PasswordNeverExpires ManagerSamAccountName = $User.ManagerSamAccountName ManagerEmail = $User.ManagerEmail ManagerStatus = $User.ManagerStatus ManagerLastLogonDays = $User.ManagerLastLogonDays Manager = $User.Manager DisplayName = $User.DisplayName GivenName = $User.GivenName Surname = $User.Surname OrganizationalUnit = $User.OrganizationalUnit MemberOf = $User.MemberOf DistinguishedName = $User.DistinguishedName ManagerDN = $User.ManagerDN } $Body = EmailBody -EmailBody $Template -Parameter $SourceParameters $EmailParameters.Subject = Add-ParametersToString -String $Subject -Parameter $SourceParameters $EmailParameters.Body = $Body if ($Attachments) { $EmailParameters.Attachment = $Attachments } else { $EmailParameters.Attachment = @() } try { Send-EmailMessage @EmailParameters -ErrorAction Stop -WarningAction SilentlyContinue } catch { if ($_.Exception.Message -like "*Credential*") { Write-Color -Text "[e] " , "Failed to send email to $($EmailParameters.EmailParameters) because error: $($_.Exception.Message)" -Color Yellow, White, Red Write-Color -Text "[i] " , "Please make sure you have valid credentials in your configuration file (graph encryption issue?)" -Color Yellow, White, Red } else { Write-Color -Text "[e] " , "Failed to send email to $($EmailParameters.EmailParameters) because error: $($_.Exception.Message)" -Color Yellow, White, Red } } } } function Send-PasswordManagerNofifications { [CmdletBinding()] param( [System.Collections.IDictionary] $ManagerSection, [System.Collections.IDictionary] $Summary, [System.Collections.IDictionary] $CachedUsers, [ScriptBlock] $TemplateManager, [string] $TemplateManagerSubject, [ScriptBlock] $TemplateManagerNotCompliant, [string] $TemplateManagerNotCompliantSubject, [System.Collections.IDictionary] $EmailParameters, [System.Collections.IDictionary] $Logging, [System.Collections.IDictionary] $GlobalManagersCache ) if ($ManagerSection.Enable) { Write-Color -Text "[i] Sending notifications to managers " -Color White, Yellow, White, Yellow, White, Yellow, White $CountManagers = 0 [Array] $SummaryManagersEmails = foreach ($Manager in $Summary['NotifyManager'].Keys) { $CountManagers++ if ($CachedUsers[$Manager]) { $ManagerUser = $CachedUsers[$Manager] } elseif ($GlobalManagersCache[$Manager]) { $ManagerUser = $GlobalManagersCache[$Manager] } else { $ManagerUser = $Summary['NotifyManager'][$Manager]['Manager'] } [Array] $ManagedUsers = $Summary['NotifyManager'][$Manager]['ManagerDefault'].Values.Output [Array] $ManagedUsersManagerNotCompliant = $Summary['NotifyManager'][$Manager]['ManagerNotCompliant'].Values.Output $EmailSplat = [ordered] @{} if ($Summary['NotifyManager'][$Manager].ManagerDefault.Count -gt 0) { if ($TemplateManager) { $EmailSplat.Template = $TemplateManager } else { $EmailSplat.Template = { } } if ($TemplateManagerSubject) { $EmailSplat.Subject = $TemplateManagerSubject } else { $EmailSplat.Subject = "[Password Expiring] Dear Manager - Your accounts are expiring!" } } elseif ($Summary['NotifyManager'][$Manager].ManagerNotCompliant.Count -gt 0) { if ($TemplateManagerNotCompliant) { $EmailSplat.Template = $TemplateManagerNotCompliant } else { $EmailSplat.Template = { } } if ($TemplateManagerNotCompliantSubject) { $EmailSplat.Subject = $TemplateManagerNotCompliantSubject } else { $EmailSplat.Subject = "[Password Escalation] Accounts are expiring with non-compliant manager" } } $EmailSplat.User = $ManagerUser $EmailSplat.ManagedUsers = $ManagedUsers $EmailSplat.ManagedUsersManagerNotCompliant = $ManagedUsersManagerNotCompliant $EmailSplat.EmailParameters = $EmailParameters $EmailSplat.EmailDateFormat = $Logging.EmailDateFormat $EmailSplat.EmailDateFormatUTCConversion = $Logging.EmailDateFormatUTCConversion if ($ManagerSection.SendToDefaultEmail -ne $true) { $EmailSplat.EmailParameters.To = $ManagerUser.EmailAddress } else { $EmailSplat.EmailParameters.To = $ManagerSection.DefaultEmail } if ($Logging.NotifyOnManagerSend) { Write-Color -Text "[i] Sending notifications to managers ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } $EmailResult = Send-PasswordEmail @EmailSplat if ($Logging.NotifyOnManagerSend) { if ($EmailResult.SentTo) { Write-Color -Text "[r] Sending notifications to managers ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ") (status: ", $EmailResult.Status, " sent to: ", $EmailResult.SentTo, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } else { Write-Color -Text "[r] Sending notifications to managers ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ") (status: ", $EmailResult.Status -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } } [PSCustomObject] @{ DisplayName = $ManagerUser.DisplayName SamAccountName = $ManagerUser.SamAccountName Domain = $ManagerUser.Domain Status = $EmailResult.Status StatusWhen = Get-Date -Format "yyyy-MM-dd HH:mm:ss" SentTo = $EmailResult.SentTo StatusError = $EmailResult.Error Accounts = $ManagedUsers.SamAccountName AccountsCount = $ManagedUsers.Count Template = 'Unknown' ManagerNotCompliant = $ManagedUsersManagerNotCompliant.SamAccountName ManagerNotCompliantCount = $ManagedUsersManagerNotCompliant.Count } if ($ManagerSection.SendCountMaximum -gt 0) { if ($ManagerSection.SendCountMaximum -le $CountManagers) { Write-Color -Text "[i]", " Send count maximum reached. There may be more managers that match the rule." -Color Red, DarkRed break } } } Write-Color -Text "[i] Sending notifications to managers (sent: ", $SummaryManagersEmails.Count, " out of ", $Summary['NotifyManager'].Values.Count, ")" -Color White, Yellow, White, Yellow, White, Yellow, White $SummaryManagersEmails } else { Write-Color -Text "[i] Sending notifications to managers is ", "disabled!" -Color White, Yellow, DarkRed } } function Send-PasswordSecurityNotifications { [CmdletBinding()] param( [System.Collections.IDictionary] $SecuritySection, [System.Collections.IDictionary] $Summary, [ScriptBlock] $TemplateSecurity, [string] $TemplateSecuritySubject, [System.Collections.IDictionary] $Logging ) if ($SecuritySection.Enable) { Write-Color -Text "[i] Sending notifications to security " -Color White, Yellow, White, Yellow, White, Yellow, White $CountSecurity = 0 [Array] $SummaryEscalationEmails = foreach ($Manager in $Summary['NotifySecurity'].Keys) { $CountSecurity++ $ManagerUser = $Summary['NotifySecurity'][$Manager]['Manager'] [Array] $ManagedUsers = $Summary['NotifySecurity'][$Manager]['Security'].Values.Output $EmailSplat = [ordered] @{} if ($Summary['NotifySecurity'][$Manager].Security.Count -gt 0) { $EmailSplat.Template = $TemplateSecurity if ($TemplateSecuritySubject) { $EmailSplat.Subject = $TemplateSecuritySubject } else { $EmailSplat.Subject = "[Password Expiring] Dear Security - Accounts expired" } } else { continue } if ($SecuritySection.AttachCSV -and $ManagedUsers.Count -gt 0) { $ManagedUsers | Export-Csv -LiteralPath $Env:TEMP\ManagedUsersSecurity.csv -NoTypeInformation -Force -Encoding UTF8 -ErrorAction Stop $EmailSplat.Attachments = @( if (Test-Path -LiteralPath "$Env:TEMP\ManagedUsersSecurity.csv") { "$Env:TEMP\ManagedUsersSecurity.csv" } ) } $EmailSplat.User = $ManagerUser $EmailSplat.ManagedUsers = $ManagedUsers | Select-Object -Property 'Status', 'DisplayName', 'Enabled', 'SamAccountName', 'Domain', 'DateExpiry', 'DaysToExpire', 'PasswordLastSet', 'PasswordExpired' $EmailSplat.EmailParameters = $EmailParameters $EmailSplat.EmailDateFormat = $Logging.EmailDateFormat $EmailSplat.EmailDateFormatUTCConversion = $Logging.EmailDateFormatUTCConversion if ($SecuritySection.SendToDefaultEmail -ne $true) { $EmailSplat.EmailParameters.To = $ManagerUser.EmailAddress } else { $EmailSplat.EmailParameters.To = $SecuritySection.DefaultEmail } if ($Logging.NotifyOnSecuritySend) { Write-Color -Text "[i] Sending notifications to security ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } $EmailResult = Send-PasswordEmail @EmailSplat if ($Logging.NotifyOnSecuritySend) { Write-Color -Text "[r] Sending notifications to security ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ") (status: ", $EmailResult.Status, " sent to: ", $EmailResult.SentTo, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } [PSCustomObject] @{ DisplayName = $ManagerUser.DisplayName SamAccountName = $ManagerUser.SamAccountName Domain = $ManagerUser.Domain Status = $EmailResult.Status StatusWhen = Get-Date -Format "yyyy-MM-dd HH:mm:ss" SentTo = $EmailResult.SentTo StatusError = $EmailResult.Error Accounts = $ManagedUsers.SamAccountName AccountsCount = $ManagedUsers.Count Template = 'Unknown' } if ($SecuritySection.SendCountMaximum -gt 0) { if ($SecuritySection.SendCountMaximum -le $CountSecurity) { Write-Color -Text "[i]", " Send count maximum reached. There may be more managers that match the rule." -Color Red, DarkRed break } } } Write-Color -Text "[i] Sending notifications to security (sent: ", $SummaryEscalationEmails.Count, " out of ", $Summary['NotifySecurity'].Values.Count, ")" -Color White, Yellow, White, Yellow, White, Yellow, White $SummaryEscalationEmails } else { Write-Color -Text "[i] Sending notifications to security is ", "disabled!" -Color White, Yellow, DarkRed } } function Send-PasswordUserNofifications { [CmdletBinding()] param( [System.Collections.IDictionary] $UserSection, [System.Collections.IDictionary] $Summary, [System.Collections.IDictionary] $Logging, [ScriptBlock] $TemplatePreExpiry, [string] $TemplatePreExpirySubject, [scriptBlock] $TemplatePostExpiry, [string] $TemplatePostExpirySubject, [System.Collections.IDictionary] $EmailParameters ) if ($UserSection.Enable) { Write-Color -Text "[i] Sending notifications to users " -Color White, Yellow, White, Yellow, White, Yellow, White $CountUsers = 0 [Array] $SummaryUsersEmails = foreach ($Notify in $Summary['Notify'].Values) { $CountUsers++ $User = $Notify.User $Rule = $Notify.Rule if ($Notify.ProcessManagersOnly -eq $true) { if ($Logging.NotifyOnSkipUserManagerOnly) { Write-Color -Text "[i]", " Skipping User (Manager Only - $($Rule.Name)) ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire -Color Yellow, White, Magenta, White, Magenta, White, White, Blue } continue } $EmailSplat = [ordered] @{} if ($Notify.User.DaysToExpire -ge 0) { if ($Notify.Rule.TemplatePreExpiry) { $EmailSplat.Template = $Notify.Rule.TemplatePreExpiry } elseif ($TemplatePreExpiry) { $EmailSplat.Template = $TemplatePreExpiry } else { $EmailSplat.Template = { } } if ($Notify.Rule.TemplatePreExpirySubject) { $EmailSplat.Subject = $Notify.Rule.TemplatePreExpirySubject } elseif ($TemplatePreExpirySubject) { $EmailSplat.Subject = $TemplatePreExpirySubject } else { $EmailSplat.Subject = '[Password] Your password will expire on $DateExpiry ($DaysToExpire days)' } } else { if ($Notify.Rule.TemplatePostExpiry) { $EmailSplat.Template = $Notify.Rule.TemplatePostExpiry } elseif ($TemplatePostExpiry) { $EmailSplat.Template = $TemplatePostExpiry } else { $EmailSplat.Template = { } } if ($Notify.Rule.TemplatePostExpirySubject) { $EmailSplat.Subject = $Notify.Rule.TemplatePostExpirySubject } elseif ($TemplatePostExpirySubject) { $EmailSplat.Subject = $TemplatePostExpirySubject } else { $EmailSplat.Subject = '[Password] Your password expired on $DateExpiry ($DaysToExpire days ago)' } } $EmailSplat.User = $Notify.User $EmailSplat.EmailParameters = $EmailParameters $EmailSplat.EmailDateFormat = $Logging.EmailDateFormat $EmailSplat.EmailDateFormatUTCConversion = $Logging.EmailDateFormatUTCConversion if ($UserSection.SendToDefaultEmail -ne $true) { $EmailSplat.EmailParameters.To = $Notify.User.EmailAddress } else { $EmailSplat.EmailParameters.To = $UserSection.DefaultEmail } if ($Notify.User.EmailAddress -like "*@*") { $EmailResult = Send-PasswordEmail @EmailSplat [PSCustomObject] @{ UserPrincipalName = $EmailSplat.User.UserPrincipalName SamAccountName = $EmailSplat.User.SamAccountName Domain = $EmailSplat.User.Domain Rule = $Notify.Rule.Name Status = $EmailResult.Status StatusWhen = Get-Date -Format "yyyy-MM-dd HH:mm:ss" StatusError = $EmailResult.Error SentTo = $EmailResult.SentTo DateExpiry = $EmailSplat.User.DateExpiry DaysToExpire = $EmailSplat.User.DaysToExpire PasswordExpired = $EmailSplat.User.PasswordExpired PasswordNeverExpires = $EmailSplat.User.PasswordNeverExpires PasswordLastSet = $EmailSplat.User.PasswordLastSet EmailFrom = $EmailSplat.User.EmailFrom } } else { $EmailResult = @{ Status = $false Error = 'No email address for user' SentTo = '' } [PSCustomObject] @{ UserPrincipalName = $EmailSplat.User.UserPrincipalName SamAccountName = $EmailSplat.User.SamAccountName Domain = $EmailSplat.User.Domain Rule = $Notify.Rule.Name Status = $EmailResult.Status StatusWhen = Get-Date -Format "yyyy-MM-dd HH:mm:ss" StatusError = $EmailResult.Error SentTo = $EmailResult.SentTo DateExpiry = $EmailSplat.User.DateExpiry DaysToExpire = $EmailSplat.User.DaysToExpire PasswordExpired = $EmailSplat.User.PasswordExpired PasswordNeverExpires = $EmailSplat.User.PasswordNeverExpires PasswordLastSet = $EmailSplat.User.PasswordLastSet EmailFrom = $EmailSplat.User.EmailFrom } } if ($Logging.NotifyOnUserSend) { if ($EmailResult.SentTo) { Write-Color -Text "[i]", " Sending notifications to user ", $Notify.User.DisplayName, " (", $Notify.User.EmailAddress, ")", " status: ", $EmailResult.Status, " sent to: ", $EmailResult.SentTo, ", details: ", $EmailResult.Error -Color Yellow, White, Yellow, White, Yellow, White, White, Blue, White, Blue } else { Write-Color -Text "[i]", " Skipping notifications to user ", $Notify.User.DisplayName, " (", $Notify.User.EmailAddress, ")", " status: ", $EmailResult.Status, " details: ", $EmailResult.Error -Color Yellow, White, Yellow, White, Yellow, White, White, Blue, White, Blue } } if ($UserSection.SendCountMaximum -gt 0) { if ($UserSection.SendCountMaximum -le $CountUsers) { Write-Color -Text "[i]", " Send count maximum reached. There may be more accounts that match the rule." -Color Red, DarkRed break } } } Write-Color -Text "[i] Sending notifications to users (sent: ", $SummaryUsersEmails.Count, " out of ", $Summary['Notify'].Values.Count, ")" -Color White, Yellow, White, Yellow, White, Yellow, White $SummaryUsersEmails } else { Write-Color -Text "[i] Sending notifications to users is ", "disabled!" -Color White, Yellow, DarkRed } } function Set-LoggingCapabilities { [CmdletBinding()] param( [string] $LogPath, [int] $LogMaximum, [switch] $ShowTime, [string] $TimeFormat ) $Script:PSDefaultParameterValues = @{ "Write-Color:LogFile" = $LogPath "Write-Color:ShowTime" = if ($PSBoundParameters.ContainsKey('ShowTime')) { $ShowTime.IsPresent } else { $null } "Write-Color:TimeFormat" = $TimeFormat } Remove-EmptyValue -Hashtable $Script:PSDefaultParameterValues if ($LogPath) { $FolderPath = [io.path]::GetDirectoryName($LogPath) if (-not (Test-Path -LiteralPath $FolderPath)) { $null = New-Item -Path $FolderPath -ItemType Directory -Force -WhatIf:$false } if ($LogMaximum -gt 0) { $CurrentLogs = Get-ChildItem -LiteralPath $FolderPath | Sort-Object -Property CreationTime -Descending | Select-Object -Skip $LogMaximum if ($CurrentLogs) { Write-Color -Text '[i] ', "Logs directory has more than ", $LogMaximum, " log files. Cleanup required..." -Color Yellow, DarkCyan, Red, DarkCyan foreach ($Log in $CurrentLogs) { try { Remove-Item -LiteralPath $Log.FullName -Confirm:$false -WhatIf:$false Write-Color -Text '[+] ', "Deleted ", "$($Log.FullName)" -Color Yellow, White, Green } catch { Write-Color -Text '[-] ', "Couldn't delete log file $($Log.FullName). Error: ', "$($_.Exception.Message) -Color Yellow, White, Red } } } } else { Write-Color -Text '[i] ', "LogMaximum is set to 0 (Unlimited). No log files will be deleted." -Color Yellow, DarkCyan } } } function Set-PasswordConfiguration { [CmdletBinding()] param( [System.Collections.IDictionary] $Logging, [scriptblock] $ConfigurationDSL, [scriptblock] $TemplatePreExpiry, [string] $TemplatePreExpirySubject, [scriptblock] $TemplatePostExpiry, [string] $TemplatePostExpirySubject, [scriptblock] $TemplateManager, [string] $TemplateManagerSubject, [scriptblock] $TemplateSecurity, [string] $TemplateSecuritySubject, [scriptblock] $TemplateManagerNotCompliant, [string] $TemplateManagerNotCompliantSubject, [scriptblock] $TemplateAdmin, [string] $TemplateAdminSubject, [System.Collections.IDictionary] $EmailParameters, [System.Collections.IDictionary] $UserSection, [System.Collections.IDictionary] $ManagerSection, [System.Collections.IDictionary] $SecuritySection, [System.Collections.IDictionary] $AdminSection, [System.Collections.IDictionary] $UsersExternalSystem, [Array] $HTMLReports, [Array] $Rules, [string] $SearchPath, [string] $OverwriteEmailProperty, [string] $OverwriteManagerProperty, [string[]] $FilterOrganizationalUnit ) if (-not $Rules) { $Rules = @() } if (-not $HTMLReports) { $HTMLReports = @() } if ($ConfigurationDSL) { try { $ConfigurationExecuted = & $ConfigurationDSL foreach ($Configuration in $ConfigurationExecuted) { if ($Configuration.Type -eq 'PasswordConfigurationOption') { if ($Configuration.Settings.SearchPath) { $SearchPath = $Configuration.Settings.SearchPath } if ($Configuration.Settings.OverwriteEmailProperty) { $OverwriteEmailProperty = $Configuration.Settings.OverwriteEmailProperty } if ($Configuration.Settings.OverwriteManagerProperty) { $OverwriteManagerProperty = $Configuration.Settings.OverwriteManagerProperty } if ($Configuration.Settings.FilterOrganizationalUnit) { $FilterOrganizationalUnit = $Configuration.Settings.FilterOrganizationalUnit } foreach ($Setting in $Configuration.Settings.Keys) { if ($Setting -notin 'SearchPath', 'OverwriteEmailProperty', 'OverwriteManagerProperty', 'FilterOrganizationalUnit') { $Logging[$Setting] = $Configuration.Settings[$Setting] } } } elseif ($Configuration.Type -eq 'PasswordConfigurationEmail') { $EmailParameters = $Configuration.Settings } elseif ($Configuration.Type -eq 'PasswordConfigurationTypeUser') { $UserSection = $Configuration.Settings } elseif ($Configuration.Type -eq 'PasswordConfigurationTypeManager') { $ManagerSection = $Configuration.Settings } elseif ($Configuration.Type -eq 'PasswordConfigurationTypeSecurity') { $SecuritySection = $Configuration.Settings } elseif ($Configuration.Type -eq 'PasswordConfigurationTypeAdmin') { $AdminSection = $Configuration.Settings } elseif ($Configuration.Type -eq 'PasswordConfigurationReport') { $HTMLReports += $Configuration.Settings } elseif ($Configuration.Type -eq 'PasswordConfigurationRule') { if ($Configuration.Error) { return } $Rules += $Configuration.Settings } elseif ($Configuration.Type -eq "PasswordConfigurationTemplatePreExpiry") { $TemplatePreExpiry = $Configuration.Settings.Template $TemplatePreExpirySubject = $Configuration.Settings.Subject } elseif ($Configuration.Type -eq "PasswordConfigurationTemplatePostExpiry") { $TemplatePostExpiry = $Configuration.Settings.Template $TemplatePostExpirySubject = $Configuration.Settings.Subject } elseif ($Configuration.Type -eq "PasswordConfigurationTemplateManager") { $TemplateManager = $Configuration.Settings.Template $TemplateManagerSubject = $Configuration.Settings.Subject } elseif ($Configuration.Type -eq "PasswordConfigurationTemplateSecurity") { $TemplateSecurity = $Configuration.Settings.Template $TemplateSecuritySubject = $Configuration.Settings.Subject } elseif ($Configuration.Type -eq "PasswordConfigurationTemplateManagerNotCompliant") { $TemplateManagerNotCompliant = $Configuration.Settings.Template $TemplateManagerNotCompliantSubject = $Configuration.Settings.Subject } elseif ($Configuration.Type -eq "PasswordConfigurationTemplateAdmin") { $TemplateAdmin = $Configuration.Settings.Template $TemplateAdminSubject = $Configuration.Settings.Subject } elseif ($Configuration.Type -eq 'ExternalUsers') { $UsersExternalSystem = $Configuration } } } catch { Write-Color -Text "[e]", " Processing configuration failed because of error in line ", $_.InvocationInfo.ScriptLineNumber, " in ", $_.InvocationInfo.InvocationName, " with message: ", $_.Exception.Message -Color Yellow, White, Red return } } if (-not $TemplatePreExpiry) { Write-Color -Text "[i]", " TemplatePreExpiry not defined. Using default template (built-in)" -Color Yellow, Red $TemplatePreExpiry = { EmailText -LineBreak EmailText -Text "Dear ", "$DisplayName," -LineBreak EmailText -Text "Your password will expire in $DaysToExpire days and if you do not change it, you will not be able to connect to the Network and IT services. " EmailText -Text "Depending on your situation, please follow one of the methods below to change your password." -LineBreak EmailText -Text "If you are connected to the Internal Network (either directly or through VPN):" EmailList { EmailListItem -Text "Press CTRL+ALT+DEL" EmailListItem -Text "Choose Change password" EmailListItem -Text "Type in your old password and then type the new one according to the password policy (twice)" EmailListItem -Text "After the change is complete you will be prompted with information that the password has been changed" } EmailText -Text "If you are not connected to the Internal Network:" EmailList { EmailListItem -Text "Open [Password Change Link](https://account.activedirectory.windowsazure.com/ChangePassword.aspx) using your web browser" EmailListItem -Text "Login using your current credentials" EmailListItem -Text "On the change password form, type your old password and the new password that you want to set (twice)" EmailListItem -Text "Click Submit" } EmailText -Text "Please also remember to modify your password on the email configuration of your Smartphone or Tablet." -LineBreak EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } } if (-not $TemplatePreExpirySubject) { Write-Color -Text "[i]", " TemplatePreExpirySubject not defined. Using default template (built-in)" -Color Yellow, Red $TemplatePreExpirySubject = '[Password Expiring] Your password will expire on $DateExpiry ($DaysToExpire days)' } if (-not $TemplatePostExpiry) { Write-Color -Text "[i]", " TemplatePostExpiry not defined. Using default template (built-in)" -Color Yellow, Red $TemplatePostExpiry = { EmailText -LineBreak EmailText -Text "Dear ", "$DisplayName," -LineBreak EmailText -Text "Your password already expired on $PasswordLastSet. If you do not change it, you will not be able to connect to the Network and IT services. " EmailText -Text "Depending on your situation, please follow one of the methods below to change your password." -LineBreak EmailText -Text "If you are connected to the Network (either directly or through VPN):" EmailList { EmailListItem -Text "Press CTRL+ALT+DEL" EmailListItem -Text "Choose Change password" EmailListItem -Text "Type in your old password and then type the new one according to the password policy (twice)" EmailListItem -Text "After the change is complete you will be prompted with information that the password has been changed" } EmailText -Text "If you are not connected to the Internal Network:" EmailList { EmailListItem -Text "Open [Password Change Link](https://account.activedirectory.windowsazure.com/ChangePassword.aspx) using your web browser" EmailListItem -Text "Login using your current credentials" EmailListItem -Text "On the change password form, type your old password and the new password that you want to set (twice)" EmailListItem -Text "Click Submit" } EmailText -Text "Please also remember to modify your password on the email configuration of your Smartphone or Tablet." -LineBreak EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } } if (-not $TemplatePostExpirySubject) { Write-Color -Text "[i]", " TemplatePostExpirySubject not defined. Using default template (built-in)" -Color Yellow, Red $TemplatePostExpirySubject = '[Password Expired] Your password expired on $DateExpiry ($DaysToExpire days ago)' } if (-not $TemplateSecurity) { Write-Color -Text "[i]", " TemplateSecurity not defined. Using default template (built-in)" -Color Yellow, Red $TemplateSecurity = { EmailText -LineBreak EmailText -Text "Hello ", "$ManagerDisplayName", "," -LineBreak -FontWeight normal, bold, normal EmailText -Text @( "Below is a summary of ", "all service accounts", " where the passwords have exceeded the time limit stipulated in the password policy. These accounts are all in violation of the policy and immediate action/escalation should take place." ) -LineBreak -FontWeight normal, bold, normal EmailText -Text "It has been agreed that the ", "password never expires", " flag has been set to ", "true", " to avoid business disruption/loss of service. As a result we require your escalation to the managers of the account to take immediate action to change the password ASAP." -LineBreak -FontWeight normal, bold, normal, bold, normal EmailText -Text "Numerous automated reminders have been sent to the Manager, but no response/action has been taken yet." -LineBreak EmailText -Text "Please reach out directly to the manager/site to ensure that these passwords are changed immediately." -LineBreak EmailText -Text "If there is still lack of responses/action taken, it will be in your (IT Security) discretion to disable the account(s) question and take any appropriate action." -LineBreak -FontWeight bold EmailTable -DataTable $ManagerUsersTable -HideFooter EmailText -LineBreak EmailText -Text "Many thanks in advance." -LineBreak EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } } if (-not $TemplateSecuritySubject) { Write-Color -Text "[i]", " TemplateSecuritySubject not defined. Using default template (built-in)" -Color Yellow, Red $TemplateSecuritySubject = "[Passsword Expired] Following accounts are expired!" } if (-not $TemplateManager) { Write-Color -Text "[i]", " TemplateManager not defined. Using default template (built-in)" -Color Yellow, Red $TemplateManager = { EmailText -LineBreak EmailText -Text "Hello $ManagerDisplayName," -LineBreak EmailText -Text "Below is a summary of accounts where the password is due to expire soon. These accounts are either:" EmailList { EmailListItem -Text 'Managed by you' EmailListItem -Text 'You are the manager of the owner of these accounts.' } EmailText -Text "Where you are the owner, please action the password change on each account outlined below, according to the rules specified by Password Policy." -LineBreak EmailTable -DataTable $ManagerUsersTable -HideFooter EmailText -LineBreak EmailText -Text @( "Please note that for Service Accounts, even though the ", "'password never expires' " "flag remains set to " "'true' " ", the password MUST be changed before the expiry date specified in the above table. " "It is the responsibility of the manager of the account to ensure that this takes place. " ) -FontWeight normal, bold, normal, bold, normal, normal -LineBreak EmailText -Text @( "Please make an effort " "to change password yourself using known methods rather than asking the Service Desk to change the password for you. " "If password is changed by Service Desk agent, there are at least 2 people knowing the password - Service Desk Agent and You! " "Do you really want the Service Desk agent to know the password to critical system you manage/own? " "Be responsible!" ) -FontWeight bold, normal, normal, normal, bold -LineBreak -Color None, None, None, None, Red EmailText -Text "One of the ways to change the password is: " -FontWeight bold EmailList { EmailListItem -Text "Press CTRL+ALT+DEL" EmailListItem -Text "Choose Change password" EmailListItem -Text "In the account name - change it to the account you want to change password for." -FontWeight bold EmailListItem -Text "Type in current password for the account and then type the new one according to the rules specified in the password policy." EmailListItem -Text "After the change is complete you will be provided with information that the password has been changed" } EmailText -Text "Failure to take action could result in loss of service/escalation to the IT Security team." -LineBreak -FontWeight bold EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } } if (-not $TemplateManagerSubject) { Write-Color -Text "[i]", " TemplateManagerSubject not defined. Using default template (built-in)" -Color Yellow, Red $TemplateManagerSubject = "[Passsword Expiring] Accounts you manage/own are expiring or already expired" } if (-not $TemplateManagerNotCompliant) { $TemplateManagerNotCompliant = { EmailText -LineBreak EmailText -Text "Hello $ManagerDisplayName," -LineBreak EmailText -Text "Below is a summary of accounts where there is missing 'critical' information. These accounts are either:" EmailList { EmailListItem -Text "Missing a Manager in the AD - please add an active manager" EmailListItem -Text "The Manager in AD is Disabled - please add an active manager" EmailListItem -Text "Manager Last logon >90 days - please confirm if the manager is still an employee/change the manager to an active manager" EmailListItem -Text "Manager is missing email - add manager email" } EmailText -Text "Please contact the respective local IT Service Desk (outlined in the below table) to update this Manager's attributes in the AD directly. The suggested action to take can be found in the below table." -LineBreak EmailTable -DataTable $ManagerUsersTableManagerNotCompliant -HideFooter EmailText -LineBreak EmailText -Text "Kind regards," -LineBreak EmailText -Text "IT Service Desk" -LineBreak } } if (-not $TemplateManagerNotCompliantSubject) { $TemplateManagerNotCompliantSubject = "[Password Escalation] Accounts are expiring with non-compliant manager" } if (-not $TemplateAdmin) { $TemplateAdmin = { EmailText -LineBreak EmailText -Text "Hello $ManagerDisplayName," -LineBreak EmailText -Text "Here's the summary of password notifications:" EmailList { EmailListItem -Text "Found users matching rule to send emails: ", $SummaryUsersEmails.Count EmailListItem -Text "Sent emails to users: ", ($SummaryUsersEmails | Where-Object { $_.Status -eq $true }).Count EmailListItem -Text "Couldn't send emails because of no email: ", ($SummaryUsersEmails | Where-Object { $_.Status -eq $false -and $_.StatusError -eq 'No email address for user' }).Count EmailListItem -Text "Couldn't send emails because other reasons: ", ($SummaryUsersEmails | Where-Object { $_.Status -eq $false -and $_.StatusError -ne 'No email address for user' }).Count EmailListItem -Text "Sent emails to managers: ", $SummaryManagersEmails.Count EmailListItem -Text "Sent emails to security: ", $SummaryEscalationEmails.Count } EmailText -Text "It took ", $TimeToProcess , " seconds to process the template." -LineBreak EmailText -Text "Hope everything works correctly! " -LineBreak EmailText -Text "Kind regards," -LineBreak EmailText -Text "IT Service Desk" -LineBreak } if (-not $TemplateAdminSubject) { $TemplateAdminSubject = '[Password Summary] Passwords summary' } } $OutputInformation = [ordered] @{ EmailParameters = $EmailParameters UserSection = $UserSection ManagerSection = $ManagerSection SecuritySection = $SecuritySection AdminSection = $AdminSection HTMLReports = $HTMLReports Rules = $Rules SearchPath = $SearchPath OverwriteEmailProperty = $OverwriteEmailProperty OverwriteManagerProperty = $OverwriteManagerProperty Logging = $Logging TemplatePreExpiry = $TemplatePreExpiry TemplatePreExpirySubject = $TemplatePreExpirySubject TemplatePostExpiry = $TemplatePostExpiry TemplatePostExpirySubject = $TemplatePostExpirySubject TemplateManager = $TemplateManager TemplateManagerSubject = $TemplateManagerSubject TemplateSecurity = $TemplateSecurity TemplateSecuritySubject = $TemplateSecuritySubject TemplateManagerNotCompliant = $TemplateManagerNotCompliant TemplateManagerNotCompliantSubject = $TemplateManagerNotCompliantSubject TemplateAdmin = $TemplateAdmin TemplateAdminSubject = $TemplateAdminSubject UsersExternalSystem = $UsersExternalSystem FilterOrganizationalUnit = $FilterOrganizationalUnit } $OutputInformation } function Find-Password { <# .SYNOPSIS Scan Active Directory forest for all users and their password expiration date .DESCRIPTION Scan Active Directory forest for all users and their password expiration date .PARAMETER Forest Target different Forest, by default current forest is used .PARAMETER ExcludeDomains Exclude domain from search, by default whole forest is scanned .PARAMETER IncludeDomains Include only specific domains, by default whole forest is scanned .PARAMETER ExtendedForestInformation Ability to provide Forest Information from another command to speed up processing .PARAMETER OverwriteEmailProperty Overwrite EmailAddress property with different property name .PARAMETER OverwriteManagerProperty Overwrite Manager property with different property name. Can use DistinguishedName or SamAccountName .PARAMETER RulesProperties Add additional properties to be returned from rules .EXAMPLE Find-Password | ft .NOTES General notes #> [CmdletBinding()] param( [alias('ForestName')][string] $Forest, [string[]] $ExcludeDomains, [alias('Domain', 'Domains')][string[]] $IncludeDomains, [System.Collections.IDictionary] $ExtendedForestInformation, [string] $OverwriteEmailProperty, [Parameter(DontShow)][switch] $AsHashTable, [Parameter(DontShow)][string] $HashtableField = 'DistinguishedName', [ValidateSet('Users', 'Contacts')][string[]] $ReturnObjectsType = @('Users', 'Contacts'), [Parameter(DontShow)][switch] $AsHashTableObject, [Parameter(DontShow)][string[]] $AddEmptyProperties = @(), [Parameter(DontShow)][string[]] $RulesProperties, [string] $OverwriteManagerProperty, [Parameter(DontShow)][System.Collections.IDictionary] $UsersExternalSystem, [Parameter(DontShow)][System.Collections.IDictionary] $ExternalSystemReplacements = [ordered] @{ Managers = [System.Collections.Generic.List[PSCustomObject]]::new() Users = [System.Collections.Generic.List[PSCustomObject]]::new() }, [string[]] $FilterOrganizationalUnit, [System.Collections.IDictionary] $Cache = [ordered] @{}, [System.Collections.IDictionary] $CacheManager = [ordered] @{} ) $ExternalSystemManagers = [ordered]@{} if ($UsersExternalSystem.Name) { Write-Color -Text '[i] ', "Using external system ", $UsersExternalSystem.Name, " for EMAIL replacement functionality" -Color Yellow, White, Yellow, White Write-Color -Text '[i] ', "There are ", $UsersExternalSystem.Users.Count, " users in the external system" -Color Yellow, White, Yellow, White } if (-not $ExternalSystemReplacements.Users) { $ExternalSystemReplacements.Users = [System.Collections.Generic.List[PSCustomObject]]::new() } if (-not $ExternalSystemReplacements.Managers) { $ExternalSystemReplacements.Managers = [System.Collections.Generic.List[PSCustomObject]]::new() } $Today = Get-Date $GuidForExchange = Convert-ADSchemaToGuid -SchemaName 'msExchMailboxGuid' if ($GuidForExchange) { $ExchangeProperty = 'msExchMailboxGuid' } $Properties = @( 'Manager', 'DisplayName', 'GivenName', 'Surname', 'SamAccountName', 'EmailAddress', 'msDS-UserPasswordExpiryTimeComputed', 'PasswordExpired', 'PasswordLastSet', 'PasswordNotRequired', 'Enabled', 'PasswordNeverExpires', 'Mail', 'MemberOf', 'LastLogonDate', 'Name' 'userAccountControl' 'pwdLastSet', 'ObjectClass' 'LastLogonDate' 'Country' if ($UsersExternalSystem -and $UsersExternalSystem.Type -eq 'ExternalUsers') { $UsersExternalSystem.ActiveDirectoryProperty } if ($ExchangeProperty) { $ExchangeProperty } if ($OverwriteEmailProperty) { $OverwriteEmailProperty } if ($OverwriteManagerProperty) { $OverwriteManagerProperty } foreach ($Rule in $RulesProperties) { $Rule } ) $Properties = $Properties | Sort-Object -Unique [Array] $ExtendedProperties = foreach ($Rule in $RulesProperties) { $Rule } [Array] $ExtendedProperties = $ExtendedProperties | Sort-Object -Unique $PropertiesContacts = @( 'SamAccountName', 'CanonicalName', 'WhenChanged', 'WhenChanged', 'DisplayName', 'DistinguishedName', 'Name', 'Mail', 'TargetAddress', 'ObjectClass' ) if (-not $Cache) { $Cache = [ordered] @{ } } if (-not $CachedUsers) { $CachedUsers = [ordered] @{ } } Write-Color -Text '[i] ', "Discovering forest information" -Color Yellow, White $ForestInformation = Get-WinADForestDetails -PreferWritable -Extended -Forest $Forest -ExcludeDomains $ExcludeDomains -IncludeDomains $IncludeDomains -ExtendedForestInformation $ExtendedForestInformation $DNSNetBios = @{ } foreach ($NETBIOS in $ForestInformation.DomainsExtendedNetBIOS.Keys) { $DNSNetBios[$ForestInformation.DomainsExtendedNetBIOS[$NETBIOS].DnsRoot] = $NETBIOS } [Array] $Users = foreach ($Domain in $ForestInformation.Domains) { Write-Color -Text "[i] ", "Discovering DC for domain ", "$($Domain)", " in forest ", $ForestInformation.Name -Color Yellow, White, Yellow, White $Server = $ForestInformation['QueryServers'][$Domain]['HostName'][0] Write-Color -Text "[i] ", "Getting users from ", "$($Domain)", " using ", $Server -Color Yellow, White, Yellow, White try { Get-ADUser -Server $Server -Filter '*' -Properties $Properties -ErrorAction Stop } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Color '[e] Error: ', $ErrorMessage -Color White, Red } } Write-Color -Text "[i] ", "Caching users for easy access" -Color Yellow, White foreach ($User in $Users) { $Cache[$User.DistinguishedName] = $User $Cache[$User.SamAccountName] = $User } if ($ReturnObjectsType -contains 'Contacts') { [Array] $Contacts = foreach ($Domain in $ForestInformation.Domains) { Write-Color -Text "[i] ", "Discovering DC for domain ", "$($Domain)", " in forest ", $ForestInformation.Name -Color Yellow, White, Yellow, White $Server = $ForestInformation['QueryServers'][$Domain]['HostName'][0] Write-Color -Text "[i] ", "Getting contacts from ", "$($Domain)", " using ", $Server -Color Yellow, White, Yellow, White try { Get-ADObject -LDAPFilter "objectClass=Contact" -Server $Server -Properties $PropertiesContacts -ErrorAction Stop } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Color '[e] Error: ', $ErrorMessage -Color White, Red } } foreach ($Contact in $Contacts) { $Cache[$Contact.DistinguishedName] = $Contact } } Write-Color -Text "[i] ", "Preparing users ", $Users.Count, " for password expirations in forest ", $Forest.Name -Color Yellow, White, Yellow, White, Yellow, White foreach ($OU in $FilterOrganizationalUnit) { Write-Color -Text "[i] ", "Filtering users by Organizational Unit ", $OU -Color Yellow, White, Yellow, White } $CountUsers = 0 foreach ($User in $Users) { $CountUsers++ Write-Verbose -Message "Processing $($User.DisplayName) / $($User.DistinguishedName) - $($CountUsers)/$($Users.Count)" $SkipUser = $false $DateExpiry = $null $DaysToExpire = $null $PasswordDays = $null $PasswordNeverExpires = $null $PasswordAtNextLogon = $null $HasMailbox = $null $OUPath = ConvertFrom-DistinguishedName -DistinguishedName $User.DistinguishedName -ToOrganizationalUnit foreach ($OU in $FilterOrganizationalUnit) { if ($null -ne $OUPath -and $OUPath -like "$OU") { $SkipUser = $false break } else { $SkipUser = $true } } if ($SkipUser) { continue } if ($OverwriteManagerProperty) { $ManagerTemp = $User.$OverwriteManagerProperty if ($ManagerTemp) { $ManagerSpecial = $Cache[$ManagerTemp] } else { $ManagerSpecial = $null } } else { $ManagerSpecial = $null } if ($ManagerSpecial) { $ManagerDN = $ManagerSpecial.DistinguishedName $Manager = $ManagerSpecial.DisplayName $ManagerSamAccountName = $ManagerSpecial.SamAccountName $ManagerDisplayName = $ManagerSpecial.DisplayName $ManagerEmail = $ManagerSpecial.Mail if ($ManagerSamAccountName -and $UsersExternalSystem -and $UsersExternalSystem.Global -eq $true) { $ADProperty = $UsersExternalSystem.ActiveDirectoryProperty if ($ADProperty -eq 'SamAccountName') { $EmailProperty = $UsersExternalSystem.EmailProperty $ExternalUser = $UsersExternalSystem['Users'][$ManagerSamAccountName] if ($ExternalUser -and $ExternalUser.$EmailProperty -like '*@*' -and $ExternalUser.$EmailProperty -ne $ManagerEmail) { $ReplacedManagerEmail = $ManagerEmail $ManagerEmail = $ExternalUser.$EmailProperty if (-not $ExternalSystemManagers[$ManagerSamAccountName]) { $ExternalSystemManagers[$ManagerSamAccountName] = $ManagerSamAccountName $ExternalSystemReplacements.Managers.Add( [PSCustomObject]@{ ManagerSamAccountName = $ManagerSamAccountName ExternalEmail = $ManagerEmail ADEmailAddress = $ReplacedManagerEmail ExternalSystem = $UsersExternalSystem.Name } ) } } } } $ManagerEnabled = $ManagerSpecial.Enabled $ManagerLastLogon = $ManagerSpecial.LastLogonDate if ($ManagerLastLogon) { $ManagerLastLogonDays = $( - $($ManagerLastLogon - $Today).Days) } else { $ManagerLastLogonDays = $null } $ManagerType = $ManagerSpecial.ObjectClass } elseif ($User.Manager) { $ManagerDN = $Cache[$User.Manager].DistinguishedName $Manager = $Cache[$User.Manager].DisplayName $ManagerSamAccountName = $Cache[$User.Manager].SamAccountName $ManagerDisplayName = $Cache[$User.Manager].DisplayName $ManagerEmail = $Cache[$User.Manager].Mail if ($ManagerSamAccountName -and $UsersExternalSystem -and $UsersExternalSystem.Global -eq $true) { $ADProperty = $UsersExternalSystem.ActiveDirectoryProperty if ($ADProperty -eq 'SamAccountName') { $EmailProperty = $UsersExternalSystem.EmailProperty $ExternalUser = $UsersExternalSystem['Users'][$ManagerSamAccountName] if ($ExternalUser -and $ExternalUser.$EmailProperty -like '*@*' -and $ExternalUser.$EmailProperty -ne $ManagerEmail) { $ReplacedManagerEmail = $ManagerEmail $ManagerEmail = $ExternalUser.$EmailProperty if (-not $ExternalSystemManagers[$ManagerSamAccountName]) { $ExternalSystemManagers[$ManagerSamAccountName] = $ManagerSamAccountName $ExternalSystemReplacements.Managers.Add( [PSCustomObject]@{ ManagerSamAccountName = $ManagerSamAccountName ExternalEmail = $ManagerEmail ADEmailAddress = $ReplacedManagerEmail ExternalSystem = $UsersExternalSystem.Name } ) } } } } $ManagerEnabled = $Cache[$User.Manager].Enabled $ManagerLastLogon = $Cache[$User.Manager].LastLogonDate if ($ManagerLastLogon) { $ManagerLastLogonDays = $( - $($ManagerLastLogon - $Today).Days) } else { $ManagerLastLogonDays = $null } $ManagerType = $Cache[$User.Manager].ObjectClass } else { if ($User.ObjectClass -eq 'user') { $ManagerStatus = 'Missing' } else { $ManagerStatus = 'Not available' } $ManagerDN = $null $Manager = $null $ManagerSamAccountName = $null $ManagerDisplayName = $null $ManagerEmail = $null $ManagerEnabled = $null $ManagerLastLogon = $null $ManagerLastLogonDays = $null $ManagerType = $null } if ($ManagerDN -and -not $CacheManager[$ManagerDN]) { $CacheManager[$ManagerDN] = [PSCustomObject] @{ DistinguishedName = $ManagerDN Domain = ConvertFrom-DistinguishedName -DistinguishedName $ManagerDN -ToDomainCN DisplayName = $ManagerDisplayName SamAccountName = $ManagerSamAccountName EmailAddress = $ManagerEmail Enabled = $ManagerEnabled LastLogonDate = $ManagerLastLogon LastLogonDays = $ManagerLastLogonDays Type = $ManagerType } } if ($OverwriteEmailProperty) { $EmailTemp = $User.$OverwriteEmailProperty if ($EmailTemp -like '*@*') { $EmailAddress = $EmailTemp } else { $EmailAddress = $User.EmailAddress } if ($Cache["$($User.Manager)"]) { if ($Cache["$($User.Manager)"].$OverwriteEmailProperty -like '*@*') { $ManagerEmail = $Cache["$($User.Manager)"].$OverwriteEmailProperty } } } else { $EmailAddress = $User.EmailAddress } if ($UsersExternalSystem -and $UsersExternalSystem.Global -eq $true) { if ($UsersExternalSystem.Type -eq 'ExternalUsers') { $ADProperty = $UsersExternalSystem.ActiveDirectoryProperty $EmailProperty = $UsersExternalSystem.EmailProperty $ExternalUser = $UsersExternalSystem['Users'][$User.$ADProperty] $EmailFrom = 'AD' if ($ExternalUser -and $ExternalUser.$EmailProperty -like '*@*' -and $EmailAddress -ne $ExternalUser.$EmailProperty) { $EmailFrom = 'ILM' $EmailAddress = $ExternalUser.$EmailProperty $ExternalSystemReplacements.Users.Add( [PSCustomObject]@{ UserSamAccountName = $User.SamAccountName ExternalEmail = $EmailAddress ADEmailAddress = $User.EmailAddress ExternalSystem = $UsersExternalSystem.Name } ) } } else { Write-Color -Text '[-] ', "External system type not supported. Please use only type as provided using 'New-PasswordConfigurationExternalUsers'." -Color Yellow, White, Red return } } else { $EmailFrom = 'AD' } if ($User.PasswordLastSet) { $PasswordDays = (New-TimeSpan -Start ($User.PasswordLastSet) -End ($Today)).Days } else { $PasswordDays = $null } if ($User.Manager) { if ($ManagerEnabled -and $ManagerEmail) { if ((Test-EmailAddress -EmailAddress $ManagerEmail).IsValid -eq $true) { $ManagerStatus = 'Enabled' } else { $ManagerStatus = 'Enabled, bad email' } } elseif ($ManagerEnabled) { $ManagerStatus = 'No email' } elseif ($Cache[$User.Manager].ObjectClass -eq 'Contact') { $ManagerStatus = 'Enabled' } else { $ManagerStatus = 'Disabled' } } if ($User."msDS-UserPasswordExpiryTimeComputed" -ne 9223372036854775807) { try { $DateExpiry = ([datetime]::FromFileTime($User."msDS-UserPasswordExpiryTimeComputed")) } catch { $DateExpiry = $User."msDS-UserPasswordExpiryTimeComputed" } try { $DaysToExpire = (New-TimeSpan -Start ($Today) -End ([datetime]::FromFileTime($User."msDS-UserPasswordExpiryTimeComputed"))).Days } catch { $DaysToExpire = $null } $PasswordNeverExpires = $User.PasswordNeverExpires } else { $PasswordNeverExpires = $true } if ($User.pwdLastSet -eq 0 -and $DateExpiry.Year -eq 1601) { $PasswordAtNextLogon = $true } else { $PasswordAtNextLogon = $false } if ($PasswordNeverExpires -or $null -eq $User.PasswordLastSet) { $DateExpiry = $null $DaysToExpire = $null } $UserAccountControl = Convert-UserAccountControl -UserAccountControl $User.UserAccountControl if ($UserAccountControl -contains 'INTERDOMAIN_TRUST_ACCOUNT') { continue } if ($ExchangeProperty) { if ($User.'msExchMailboxGuid') { $HasMailbox = 'Yes' } else { $HasMailbox = 'No' } } else { $HasMailbox = 'Unknown' } if ($User.LastLogonDate) { $LastLogonDays = $( - $($User.LastLogonDate - $Today).Days) } else { $LastLogonDays = $null } if ($User.Country) { $Country = Convert-CountryCodeToCountry -CountryCode $User.Country $CountryCode = $User.Country } else { $Country = 'Unknown' $CountryCode = 'Unknown' } if ($AddEmptyProperties.Count -gt 0) { $StartUser = [ordered] @{ UserPrincipalName = $User.UserPrincipalName SamAccountName = $User.SamAccountName Domain = ConvertFrom-DistinguishedName -DistinguishedName $User.DistinguishedName -ToDomainCN RuleName = '' RuleOptions = [System.Collections.Generic.List[string]]::new() Enabled = $User.Enabled HasMailbox = $HasMailbox EmailAddress = $EmailAddress SystemEmailAddress = $User.EmailAddress DateExpiry = $DateExpiry DaysToExpire = $DaysToExpire PasswordExpired = $User.PasswordExpired PasswordDays = $PasswordDays PasswordAtNextLogon = $PasswordAtNextLogon PasswordLastSet = $User.PasswordLastSet PasswordNotRequired = $User.PasswordNotRequired PasswordNeverExpires = $PasswordNeverExpires LastLogonDate = $User.LastLogonDate LastLogonDays = $LastLogonDays } foreach ($Property in $AddEmptyProperties) { $StartUser.$Property = $null } $EndUser = [ordered] @{ Manager = $Manager ManagerDisplayName = $ManagerDisplayName ManagerSamAccountName = $ManagerSamAccountName ManagerEmail = $ManagerEmail ManagerStatus = $ManagerStatus ManagerLastLogonDays = $ManagerLastLogonDays ManagerType = $ManagerType DisplayName = $User.DisplayName Name = $User.Name GivenName = $User.GivenName Surname = $User.Surname OrganizationalUnit = $OUPath MemberOf = $User.MemberOf DistinguishedName = $User.DistinguishedName ManagerDN = $User.Manager Country = $Country CountryCode = $CountryCode Type = 'User' EmailFrom = $EmailFrom } $MyUser = $StartUser + $EndUser } else { $MyUser = [ordered] @{ UserPrincipalName = $User.UserPrincipalName SamAccountName = $User.SamAccountName Domain = ConvertFrom-DistinguishedName -DistinguishedName $User.DistinguishedName -ToDomainCN RuleName = '' RuleOptions = [System.Collections.Generic.List[string]]::new() Enabled = $User.Enabled HasMailbox = $HasMailbox EmailAddress = $EmailAddress SystemEmailAddress = $User.EmailAddress DateExpiry = $DateExpiry DaysToExpire = $DaysToExpire PasswordExpired = $User.PasswordExpired PasswordDays = $PasswordDays PasswordAtNextLogon = $PasswordAtNextLogon PasswordLastSet = $User.PasswordLastSet PasswordNotRequired = $User.PasswordNotRequired PasswordNeverExpires = $PasswordNeverExpires LastLogonDate = $User.LastLogonDate LastLogonDays = $LastLogonDays Manager = $Manager ManagerDisplayName = $ManagerDisplayName ManagerSamAccountName = $ManagerSamAccountName ManagerEmail = $ManagerEmail ManagerStatus = $ManagerStatus ManagerLastLogonDays = $ManagerLastLogonDays ManagerType = $ManagerType DisplayName = $User.DisplayName Name = $User.Name GivenName = $User.GivenName Surname = $User.Surname OrganizationalUnit = ConvertFrom-DistinguishedName -DistinguishedName $User.DistinguishedName -ToOrganizationalUnit MemberOf = $User.MemberOf DistinguishedName = $User.DistinguishedName ManagerDN = $User.Manager Country = $Country CountryCode = $CountryCode Type = 'User' EmailFrom = $EmailFrom } } foreach ($Property in $ConditionProperties) { $MyUser["$Property"] = $User.$Property } foreach ($E in $ExtendedProperties) { $MyUser[$E] = $User.$E } if ($HashtableField -eq 'NetBiosSamAccountName') { $HashField = $DNSNetBios[$MyUser.Domain] + '\' + $MyUser.SamAccountName if ($AsHashTableObject) { $CachedUsers["$HashField"] = $MyUser } else { $CachedUsers["$HashField"] = [PSCustomObject] $MyUser } } else { if ($AsHashTableObject) { $CachedUsers["$($User.$HashtableField)"] = $MyUser } else { $CachedUsers["$($User.$HashtableField)"] = [PSCustomObject] $MyUser } } } if ($ReturnObjectsType -contains 'Contacts') { $CountContacts = 0 foreach ($Contact in $Contacts) { $CountContacts++ $OUPath = ConvertFrom-DistinguishedName -DistinguishedName $Contact.DistinguishedName -ToOrganizationalUnit foreach ($OU in $FilterOrganizationalUnit) { if ($null -eq $OUPath) { $SkipUser = $true break } elseif ($OUPath -notlike "$OU") { $SkipUser = $true break } } if ($SkipUser) { continue } Write-Verbose -Message "Processing $($Contact.DisplayName) - $($CountContacts)/$($Contacts.Count)" $MyUser = [ordered] @{ UserPrincipalName = $null SamAccountName = $null Domain = ConvertFrom-DistinguishedName -DistinguishedName $Contact.DistinguishedName -ToDomainCN RuleName = '' RuleOptions = [System.Collections.Generic.List[string]]::new() Enabled = $true HasMailbox = $null EmailAddress = $Contact.Mail SystemEmailAddress = $Contact.Mail DateExpiry = $null DaysToExpire = $null PasswordExpired = $null PasswordDays = $null PasswordAtNextLogon = $null PasswordLastSet = $null PasswordNotRequired = $null PasswordNeverExpires = $null LastLogonDate = $null LastLogonDays = $null Manager = $null ManagerDisplayName = $null ManagerSamAccountName = $null ManagerEmail = $null ManagerStatus = $null ManagerLastLogonDays = $null ManagerType = $null DisplayName = $Contact.DisplayName Name = $Contact.Name GivenName = $null Surname = $null OrganizationalUnit = $OUPath MemberOf = $Contact.MemberOf DistinguishedName = $Contact.DistinguishedName ManagerDN = $null Country = $null CountryCode = $null Type = 'Contact' EmailFrom = $EmailFrom } foreach ($E in $ExtendedProperties) { $MyUser[$E] = $User.$E } if ($HashtableField -eq 'NetBiosSamAccountName') { continue } else { if ($AsHashTableObject) { $CachedUsers["$($Contact.$HashtableField)"] = $MyUser } else { $CachedUsers["$($Contact.$HashtableField)"] = [PSCustomObject] $MyUser } } } } if ($AsHashTable) { $CachedUsers } else { $CachedUsers.Values } } function Find-PasswordNotification { <# .SYNOPSIS Searches thru XML logs created by Password Solution .DESCRIPTION Searches thru XML logs created by Password Solution .PARAMETER SearchPath Path to file where the XML log is located .PARAMETER Manager Search thru manager escalations .EXAMPLE Find-PasswordNotification -SearchPath $PSScriptRoot\Search\SearchLog.xml | Format-Table .EXAMPLE Find-PasswordNotification -SearchPath "$PSScriptRoot\Search\SearchLog_2021-06.xml" -Manager | Format-Table .NOTES General notes #> [CmdletBinding()] param( [Parameter(Mandatory)][string] $SearchPath, [switch] $Manager ) if ($SearchPath) { if (Test-Path -LiteralPath $SearchPath) { try { $SummarySearch = Import-Clixml -LiteralPath $SearchPath -ErrorAction Stop } catch { Write-Color -Text "[e]", " Couldn't load the file $SearchPath", ". Skipping...", $_.Exception.Message -Color White, Yellow, White, Yellow, White, Yellow, White } if ($SummarySearch -and $Manager) { $SummarySearch.EmailEscalations.Values } elseif ($SummarySearch -and $Manager -eq $false) { $SummarySearch.EmailSent.Values } } } } function Find-PasswordQuality { <# .SYNOPSIS Scan Active Directory forest for asses password quality of users .DESCRIPTION Scan Active Directory forest for asses password quality of users including weak passwords, duplicate groups and more. .PARAMETER WeakPasswords List of weak passwords to check against .PARAMETER WeakPasswordsFilePath Path to a file that contains weak passwords, one password per line. .PARAMETER WeakPasswordsHashesFile Path to a file that contains NT hashes of weak passwords, one hash in HEX format per line. For performance reasons, the -WeakPasswordHashesSortedFile parameter should be used instead. .PARAMETER WeakPasswordsHashesSortedFile Path to a file that contains NT hashes of weak passwords, one hash in HEX format per line. The hashes must be sorted alphabetically, because a binary search is performed. This parameter is typically used with a list of leaked password hashes from HaveIBeenPwned. .PARAMETER IncludeStatistics Include statistics in output .PARAMETER Forest Target different Forest, by default current forest is used .PARAMETER ExcludeDomains Exclude domain from search, by default whole forest is scanned .PARAMETER IncludeDomains Include only specific domains, by default whole forest is scanned .PARAMETER ExtendedForestInformation Ability to provide Forest Information from another command to speed up processing .EXAMPLE Find-PasswordQuality -WeakPasswords "Test1", "Test2", "Test3" .EXAMPLE Find-PasswordQuality -WeakPasswords "Test1", "Test2", "Test3" -IncludeStatistics .NOTES General notes #> [CmdletBinding()] param( [string[]] $WeakPasswords, [string] $WeakPasswordsFilePath, [string] $WeakPasswordsHashesFile, [string] $WeakPasswordsHashesSortedFile, [switch] $IncludeStatistics, [alias('ForestName')][string] $Forest, [string[]] $ExcludeDomains, [alias('Domain', 'Domains')][string[]] $IncludeDomains, [System.Collections.IDictionary] $ExtendedForestInformation ) $PropertiesToAdd = @( 'ClearTextPassword' 'LMHash' 'EmptyPassword' 'WeakPassword' 'AESKeysMissing' 'PreAuthNotRequired' 'DESEncryptionOnly' 'Kerberoastable' 'DelegatableAdmins' 'SmartCardUsersWithPassword' 'DuplicatePasswordGroups' ) if ($WeakPasswordsHashesFile) { if (Test-Path -LiteralPath $WeakPasswordsHashesFile) { Write-Color -Text "[i] ", "Weak password hashes available to read from ", $WeakPasswordsHashesFile -Color Yellow, Gray, White, Yellow, White, Yellow, White $WeakPasswordHashesStats = Get-FileInformation -File $WeakPasswordsHashesFile } else { Write-Color -Text "[e] ", "Weak password hashes file not found at ", $WeakPasswordsHashesFile -Color Red, Yellow, White, Yellow, Red return } } if ($WeakPasswordsHashesSortedFile) { if (Test-Path -LiteralPath $WeakPasswordsHashesSortedFile) { Write-Color -Text "[i] ", "Weak passwords hashes (sorted) available to read from ", $WeakPasswordsHashesSortedFile -Color Yellow, Gray, White, Yellow, White, Yellow, White $WeakPasswordHashesSortedStats = Get-FileInformation -File $WeakPasswordsHashesSortedFile } else { Write-Color -Text "[e] ", "Weak passwords hashes (sorted) file not found at ", $WeakPasswordsHashesSortedFile -Color Red, Yellow, White, Yellow, Red return } } if ($WeakPasswordsFilePath) { if (Test-Path -LiteralPath $WeakPasswordsFilePath) { Write-Color -Text "[i] ", "Weak passwords available to read from ", $WeakPasswordsFilePath -Color Yellow, Gray, White, Yellow, White, Yellow, White $WeakPasswordsStats = Get-FileInformation -File $WeakPasswordsFilePath } else { Write-Color -Text "[e] ", "Weak passwords file not found at ", $WeakPasswordsFilePath -Color Red, Yellow, White, Yellow, Red return } } $ModuleExists = Get-Command -Module DSInternals -ErrorAction SilentlyContinue if (-not $ModuleExists) { Write-Color -Text "[e] ", "DSInternals module is not installed. Please install it using Install-Module DSInternals -Verbose" -Color Yellow, Red return } $AllUsers = Find-Password -AsHashTable -HashtableField NetBiosSamAccountName -ReturnObjectsType Users -AsHashTableObject -AddEmptyProperties $PropertiesToAdd -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains Write-Color -Text "[i] ", "Discovering forest information" -Color Yellow, Gray, White, Yellow, White, Yellow, White $ForestInformation = Get-WinADForestDetails -PreferWritable -Forest $Forest -ExcludeDomains $ExcludeDomains -IncludeDomains $IncludeDomains -ExtendedForestInformation $ExtendedForestInformation $PasswordsInHash = [ordered] @{} $PasswordQuality = foreach ($Domain in $ForestInformation.Domains) { Write-Color -Text "[i] ", "Discovering DC for domain ", "$($Domain)", " in forest ", $ForestInformation.Name -Color Yellow, Gray, White, Yellow, White, Yellow, White $Server = $ForestInformation['QueryServers'][$Domain]['HostName'][0] Write-Color -Text "[i] ", "Getting replication data from ", "$($Domain)", " using ", $Server -Color Yellow, Gray, White, Yellow, White, Yellow, White $testPasswordQualitySplat = @{ WeakPasswords = $WeakPasswords WeakPasswordsFile = $WeakPasswordsFilePath WeakPasswordHashesFile = $WeakPasswordsHashesFile WeakPasswordHashesSortedFile = $WeakPasswordsHashesSortedFile IncludeDisabledAccounts = $true } Remove-EmptyValue -Hashtable $testPasswordQualitySplat try { Get-ADReplAccount -All -Server $Server -ErrorAction Stop } catch { Write-Color -Text "[e] ", "Unable to get replication data from ", "$($Domain)", " using ", $Server, ". Error: ", $_.Exception.Message -Color Red, Yellow, White, Yellow, Red, Red } } Write-Color -Text "[i] Testing password quality" -Color Yellow, Gray, White, Yellow, White, Yellow, White $Quality = $PasswordQuality | Test-PasswordQuality @testPasswordQualitySplat Write-Color -Text "[i] Processing results, merging data from DSInternals" -Color Yellow, Gray, White, Yellow, White, Yellow, White foreach ($Property in $Quality.PSObject.Properties.Name) { $PasswordsInHash[$Property] = $Quality.$Property } $PasswordGroupsUsers = [ordered] @{} $Count = 0 foreach ($Group in $PasswordsInHash.DuplicatePasswordGroups) { $Count++ foreach ($User in $Group) { $PasswordGroupsUsers[$User] = "Group $Count" } } $QualityStatistics = [ordered] @{ AESKeysMissing = $PasswordsInHash.AESKeysMissing.Count AESKeysMissingEnabledOnly = 0 AESKeysMissingDisabledOnly = 0 DESEncryptionOnly = $PasswordsInHash.DESEncryptionOnly.Count DESEncryptionOnlyEnabledOnly = 0 DESEncryptionOnlyDisabledOnly = 0 DelegatableAdmins = $PasswordsInHash.DelegatableAdmins.Count DelegatableAdminsEnabledOnly = 0 DelegatableAdminsDisabledOnly = 0 DuplicatePasswordGroups = $PasswordsInHash.DuplicatePasswordGroups.Count DuplicatePasswordUsers = $PasswordGroupsUsers.Keys.Count DuplicatePasswordUsersEnabledOnly = 0 DuplicatePasswordUsersDisabledOnly = 0 ClearTextPassword = $PasswordsInHash.ClearTextPassword.Count ClearTextPasswordEnabledOnly = 0 ClearTextPasswordDisabledOnly = 0 LMHash = $PasswordsInHash.LMHash.Count LMHashEnabledOnly = 0 LMHashDisabledOnly = 0 EmptyPassword = $PasswordsInHash.EmptyPassword.Count EmptyPasswordEnabledOnly = 0 EmptyPasswordDisabledOnly = 0 WeakPassword = $PasswordsInHash.WeakPassword.Count WeakPasswordEnabledOnly = 0 WeakPasswordDisabledOnly = 0 PasswordNotRequired = 0 PasswordNotRequiredEnabledOnly = 0 PasswordNotRequiredDisabledOnly = 0 PasswordNeverExpires = 0 PasswordNeverExpiresEnabledOnly = 0 PasswordNeverExpiresDisabledOnly = 0 PreAuthNotRequired = $PasswordsInHash.PreAuthNotRequired.Count PreAuthNotRequiredEnabledOnly = 0 PreAuthNotRequiredDisabledOnly = 0 Kerberoastable = $PasswordsInHash.Kerberoastable.Count KerberoastableEnabledOnly = 0 KerberoastableDisabledOnly = 0 SmartCardUsersWithPassword = $PasswordsInHash.SmartCardUsersWithPassword.Count SmartCardUsersWithPasswordEnabledOnly = 0 SmartCardUsersWithPasswordDisabledOnly = 0 } $CountryStatistics = [ordered] @{ DuplicatePasswordUsers = [ordered] @{} WeakPassword = [ordered] @{} } $ContinentStatistics = [ordered] @{ DuplicatePasswordUsers = [ordered] @{} WeakPassword = [ordered] @{} } $CountryCodeStatistics = [ordered] @{ DuplicatePasswordUsers = [ordered] @{} WeakPassword = [ordered] @{} } $CountryToContinent = Convert-CountryToContinent $OutputUsers = foreach ($User in $AllUsers.Keys) { if ($AllUsers[$User].Country) { $Continent = $CountryToContinent[$AllUsers[$User].Country] if (-not $Continent) { $Continent = 'Unknown' } } else { $Continent = 'Unknown' } if ($AllUsers[$User].PasswordNotRequired) { $QualityStatistics.PasswordNotRequired++ if ($AllUsers[$User].Enabled -eq $true) { $QualityStatistics.PasswordNotRequiredEnabledOnly++ } else { $QualityStatistics.PasswordNotRequiredDisabledOnly++ } } if ($AllUsers[$User].PasswordNeverExpires) { $QualityStatistics.PasswordNeverExpires++ if ($AllUsers[$User].Enabled -eq $true) { $QualityStatistics.PasswordNeverExpiresEnabledOnly++ } else { $QualityStatistics.PasswordNeverExpiresDisabledOnly++ } } foreach ($Property in $PasswordsInHash.Keys) { if ($Property -eq 'DuplicatePasswordGroups') { if ($PasswordGroupsUsers[$User]) { $AllUsers[$User][$Property] = $PasswordGroupsUsers[$User] if ($AllUsers[$User].Enabled -eq $true) { $QualityStatistics["$($Property)EnabledOnly"]++ $QualityStatistics.DuplicatePasswordUsersEnabledOnly++ } else { $QualityStatistics["$($Property)DisabledOnly"]++ $QualityStatistics.DuplicatePasswordUsersDisabledOnly++ } $CountryStatistics['DuplicatePasswordUsers'][$AllUsers[$User].Country]++ $ContinentStatistics['DuplicatePasswordUsers'][$Continent]++ $CountryCodeStatistics['DuplicatePasswordUsers'][$AllUsers[$User].CountryCode]++ } else { $AllUsers[$User][$Property] = '' } } elseif ($Property -in $PropertiesToAdd) { if ($PasswordsInHash[$Property] -contains $User) { $AllUsers[$User][$Property] = $true if ($AllUsers[$User].Enabled -eq $true) { $QualityStatistics["$($Property)EnabledOnly"]++ } else { $QualityStatistics["$($Property)DisabledOnly"]++ } if ($Property -eq 'WeakPassword') { $CountryStatistics[$Property][$AllUsers[$User].Country]++ $ContinentStatistics[$Property][$Continent]++ $CountryCodeStatistics[$Property][$AllUsers[$User].CountryCode]++ } } else { $AllUsers[$User][$Property] = $false } } } [PSCustomObject] $AllUsers[$User] } if ($IncludeStatistics) { [ordered] @{ Forest = $ForestInformation.Forest Domains = $ForestInformation.Domains Statistics = $QualityStatistics StatisticsCountry = $CountryStatistics StatisticsCountryCode = $CountryCodeStatistics StatisticsContinents = $ContinentStatistics Users = $OutputUsers WeakPasswordsFileInformation = [ordered] @{ WeakPasswordHashesStats = $WeakPasswordHashesStats WeakPasswordHashesSortedStats = $WeakPasswordHashesSortedStats WeakPasswordsStats = $WeakPasswordsStats } } } else { $OutputUsers } } function New-PasswordConfigurationEmail { [cmdletBinding(DefaultParameterSetName = 'Compatibility', SupportsShouldProcess)] param( [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [alias('SmtpServer')][string] $Server, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [int] $Port, [Parameter(Mandatory, ParameterSetName = 'SecureString')] [Parameter(Mandatory, ParameterSetName = 'oAuth')] [Parameter(Mandatory, ParameterSetName = 'Graph')] [Parameter(Mandatory, ParameterSetName = 'MgGraphRequest')] [Parameter(Mandatory, ParameterSetName = 'Compatibility')] [Parameter(Mandatory, ParameterSetName = 'SendGrid')] [object] $From, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [Parameter(ParameterSetName = 'Compatibility')] [Parameter(ParameterSetName = 'SendGrid')] [string] $ReplyTo, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [Parameter(ParameterSetName = 'Compatibility')] [Parameter(ParameterSetName = 'SendGrid')] [alias('Importance')][ValidateSet('Low', 'Normal', 'High')][string] $Priority, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [ValidateSet('None', 'OnSuccess', 'OnFailure', 'Delay', 'Never')][string[]] $DeliveryNotificationOption, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [MailKit.Net.Smtp.DeliveryStatusNotificationType] $DeliveryStatusNotificationType, [Parameter(ParameterSetName = 'oAuth')] [Parameter(Mandatory, ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'Compatibility')] [Parameter(Mandatory, ParameterSetName = 'SendGrid')] [pscredential] $Credential, [Parameter(ParameterSetName = 'SecureString')] [string] $Username, [Parameter(ParameterSetName = 'SecureString')] [string] $Password, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [MailKit.Security.SecureSocketOptions] $SecureSocketOptions, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [switch] $UseSsl, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [switch] $SkipCertificateRevocation, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [alias('SkipCertificateValidatation')][switch] $SkipCertificateValidation, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [int] $Timeout, [Parameter(ParameterSetName = 'oAuth')] [alias('oAuth')][switch] $oAuth2, [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [switch] $RequestReadReceipt, [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [switch] $RequestDeliveryReceipt, [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [switch] $Graph, [Parameter(ParameterSetName = 'MgGraphRequest')] [switch] $MgGraphRequest, [Parameter(ParameterSetName = 'SecureString')] [switch] $AsSecureString, [Parameter(ParameterSetName = 'SendGrid')] [switch] $SendGrid, [Parameter(ParameterSetName = 'SendGrid')] [switch] $SeparateTo, [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [switch] $DoNotSaveToSentItems, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [string] $LocalDomain ) $Output = [ordered] @{ Type = 'PasswordConfigurationEmail' Settings = [ordered] @{ Server = if ($PSBoundParameters.ContainsKey('Server')) { $Server } else { $null } Port = if ($PSBoundParameters.ContainsKey('Port')) { $Port } else { $null } From = if ($PSBoundParameters.ContainsKey('From')) { $From } else { $null } ReplyTo = if ($PSBoundParameters.ContainsKey('ReplyTo')) { $ReplyTo } else { $null } Priority = if ($PSBoundParameters.ContainsKey('Priority')) { $Priority } else { $null } DeliveryNotificationOption = if ($PSBoundParameters.ContainsKey('DeliveryNotificationOption')) { $DeliveryNotificationOption } else { $null } DeliveryStatusNotificationType = if ($PSBoundParameters.ContainsKey('DeliveryStatusNotificationType')) { $DeliveryStatusNotificationType } else { $null } Credential = if ($PSBoundParameters.ContainsKey('Credential')) { $Credential } else { $null } Username = if ($PSBoundParameters.ContainsKey('Username')) { $Username } else { $null } Password = if ($PSBoundParameters.ContainsKey('Password')) { $Password } else { $null } SecureSocketOptions = if ($PSBoundParameters.ContainsKey('SecureSocketOptions')) { $SecureSocketOptions } else { $null } UseSsl = if ($PSBoundParameters.ContainsKey('UseSsl')) { $UseSsl } else { $null } SkipCertificateRevocation = if ($PSBoundParameters.ContainsKey('SkipCertificateRevocation')) { $SkipCertificateRevocation } else { $null } SkipCertificateValidation = if ($PSBoundParameters.ContainsKey('SkipCertificateValidatation')) { $SkipCertificateValidation } else { $null } Timeout = if ($PSBoundParameters.ContainsKey('Timeout')) { $Timeout } else { $null } oAuth2 = if ($PSBoundParameters.ContainsKey('oAuth2')) { $oAuth2 } else { $null } RequestReadReceipt = if ($PSBoundParameters.ContainsKey('RequestReadReceipt')) { $RequestReadReceipt } else { $null } RequestDeliveryReceipt = if ($PSBoundParameters.ContainsKey('RequestDeliveryReceipt')) { $RequestDeliveryReceipt } else { $null } Graph = if ($PSBoundParameters.ContainsKey('Graph')) { $Graph } else { $null } MgGraphRequest = if ($PSBoundParameters.ContainsKey('MgGraphRequest')) { $MgGraphRequest } else { $null } AsSecureString = if ($PSBoundParameters.ContainsKey('AsSecureString')) { $AsSecureString } else { $null } SendGrid = if ($PSBoundParameters.ContainsKey('SendGrid')) { $SendGrid } else { $null } SeparateTo = if ($PSBoundParameters.ContainsKey('SeparateTo')) { $SeparateTo } else { $null } DoNotSaveToSentItems = if ($PSBoundParameters.ContainsKey('DoNotSaveToSentItems')) { $DoNotSaveToSentItems } else { $null } WhatIf = $WhatIfPreference } } Remove-EmptyValue -Hashtable $Output.Settings $Output } function New-PasswordConfigurationExternalUsers { <# .SYNOPSIS This function caches users from external systems to be used in the password configuration. .DESCRIPTION This function caches users from external systems to be used in the password configuration. It provides ability to find user by some property and get another property of the user. .PARAMETER Users Parameter description .PARAMETER ActiveDirectoryProperty Property in Active Directory to search for when comparing against SearchProperty. .PARAMETER SearchProperty Property to cache on the user object. .PARAMETER EmailProperty How the email property is called in the user object. .PARAMETER Global Tells the solution to globally overwrite email addresses for all users. .PARAMETER Name Name of the configuration. Visible in HTML reports. .EXAMPLE New-PasswordConfigurationExternalUsers -Users $ExportDataFromHrSystem -SearchProperty '<property in the HR system>' -EmailProperty '<email property in HR system>' -ActiveDirectoryProperty 'SamAccountName' .NOTES General notes #> [CmdletBinding()] param( [parameter(Mandatory)][string] $Name, [parameter(Mandatory)][Array] $Users, [parameter(Mandatory)][string] $ActiveDirectoryProperty, [parameter(Mandatory)][string] $SearchProperty, [parameter(Mandatory)][string] $EmailProperty, [switch] $Global ) $CachedUsers = [ordered] @{} if ($Users.Count -gt 0 -and $Users[0].$SearchProperty -and $Users[0].$EmailProperty) { Write-Color -Text '[+] ', "Caching users for '$Name'" -Color Green, White } else { Write-Color -Text '[-] ', "Couldn't cache users as either users not provided or email/search property are invalid. Please fix 'New-PasswordConfigurationExternalUsers'" -Color Yellow, White return } try { foreach ($User in $Users) { if ($User.$SearchProperty) { $CachedUsers[$User.$SearchProperty] = $User | Select-Object -Property $EmailProperty } } } catch { Write-Color -Text '[-] ', "Couldn't cache users. Please fix 'New-PasswordConfigurationExternalUsers'. Error: ", "$($_.Exception.Message)" -Color Yellow, White, Red return } [ordered] @{ Type = 'ExternalUsers' ActiveDirectoryProperty = $ActiveDirectoryProperty SearchProperty = $SearchProperty EmailProperty = $EmailProperty Users = $CachedUsers Global = $Global.IsPresent Name = $Name } } function New-PasswordConfigurationOption { <# .SYNOPSIS Provides a way to create a PasswordConfigurationOption object. .DESCRIPTION This function provides a way to create a PasswordConfigurationOption object. The object is used to store configuration options for the Password Solution module. .PARAMETER ShowTime Show time in the console output. If not provided, time will not be shown. Time in the log file is always shown. .PARAMETER LogFile File path to the log file. If not provided, there will be no logging to file .PARAMETER TimeFormat Time format used in the logging functionality. .PARAMETER LogMaximum Maximum number of log files to keep. Default is 0 (unlimited). Once the number of log files exceeds the limit, the oldest log files will be deleted. .PARAMETER NotifyOnSkipUserManagerOnly Provides a way to control output to screen for SkipUserManagerOnly. .PARAMETER NotifyOnSecuritySend Provides a way to control output to screen for SecuritySend. .PARAMETER NotifyOnManagerSend Provides a way to control output to screen for ManagerSend. .PARAMETER NotifyOnUserSend Provides a way to control output to screen for UserSend. .PARAMETER NotifyOnUserMatchingRule Provides a way to control output to screen for UserMatchingRule. .PARAMETER NotifyOnUserDaysToExpireNull Provides a way to control output to screen for UserDaysToExpireNull. .PARAMETER NotifyOnUserMatchingRuleForManager Provides a way to control output to screen for UserMatchingRuleForManager. .PARAMETER NotifyOnUserMatchingRuleForManagerButNotCompliant Provides a way to control output to screen for UserMatchingRuleForManagerButNotCompliant. .PARAMETER SearchPath Path to XML file that will be used for storing search results. .PARAMETER EmailDateFormat Parameter description .PARAMETER EmailDateFormatUTCConversion Parameter description .PARAMETER OverwriteEmailProperty Parameter description .PARAMETER OverwriteManagerProperty Parameter description .PARAMETER FilterOrganizationalUnit Provides a way to filter users by Organizational Unit limiting the scope of the search. The search is performed using 'like' operator, so you can use wildcards if needed. .EXAMPLE $Options = @{ # Logging to file and to screen ShowTime = $true LogFile = "$PSScriptRoot\Logs\PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).log" TimeFormat = "yyyy-MM-dd HH:mm:ss" LogMaximum = 365 NotifyOnSkipUserManagerOnly = $false NotifyOnSecuritySend = $true NotifyOnManagerSend = $true NotifyOnUserSend = $true NotifyOnUserMatchingRule = $false NotifyOnUserDaysToExpireNull = $false SearchPath = "$PSScriptRoot\Search\SearchLog_$((Get-Date).ToString('yyyy-MM')).xml" EmailDateFormat = "yyyy-MM-dd" EmailDateFormatUTCConversion = $true FilterOrganizationalUnit = @( "*OU=Accounts,OU=Administration,DC=ad,DC=evotec,DC=xyz" "*OU=Administration,DC=ad,DC=evotec,DC=xyz" ) } New-PasswordConfigurationOption @Options .NOTES General notes #> [CmdletBinding()] param( [switch] $ShowTime , #= $true [string] $LogFile , #= "$PSScriptRoot\Logs\PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).log" [string] $TimeFormat , #= "yyyy-MM-dd HH:mm:ss" [int] $LogMaximum , #= 365 [switch] $NotifyOnSkipUserManagerOnly , #= $false [switch] $NotifyOnSecuritySend , #= $true [switch] $NotifyOnManagerSend , #= $true [switch] $NotifyOnUserSend , #= $true [switch] $NotifyOnUserMatchingRule , #= $true [switch] $NotifyOnUserDaysToExpireNull , #= $true [switch] $NotifyOnUserMatchingRuleForManager, [switch] $NotifyOnUserMatchingRuleForManagerButNotCompliant, [string] $SearchPath, [string] $EmailDateFormat, [switch] $EmailDateFormatUTCConversion, [string] $OverwriteEmailProperty, [string] $OverwriteManagerProperty, [string[]] $FilterOrganizationalUnit ) $Output = [ordered] @{ Type = "PasswordConfigurationOption" Settings = [ordered] @{ ShowTime = $ShowTime.IsPresent LogFile = $LogFile TimeFormat = $TimeFormat LogMaximum = $LogMaximum NotifyOnSkipUserManagerOnly = $NotifyOnSkipUserManagerOnly.IsPresent NotifyOnSecuritySend = $NotifyOnSecuritySend.IsPresent NotifyOnManagerSend = $NotifyOnManagerSend.IsPresent NotifyOnUserSend = $NotifyOnUserSend.IsPresent NotifyOnUserMatchingRule = $NotifyOnUserMatchingRule.IsPresent NotifyOnUserDaysToExpireNull = $NotifyOnUserDaysToExpireNull.IsPresent NotifyOnUserMatchingRuleForManager = $NotifyOnUserMatchingRuleForManager.IsPresent NotifyOnUserMatchingRuleForManagerButNotCompliant = $NotifyOnUserMatchingRuleForManagerButNotCompliant.IsPresent SearchPath = $SearchPath EmailDateFormat = $EmailDateFormat EmailDateFormatUTCConversion = $EmailDateFormatUTCConversion.IsPresent OverwriteEmailProperty = $OverwriteEmailProperty OverwriteManagerProperty = $OverwriteManagerProperty FilterOrganizationalUnit = $FilterOrganizationalUnit } } Remove-EmptyValue -Hashtable $Output.Settings $Output } function New-PasswordConfigurationReport { <# .SYNOPSIS Provides HTML report configuration for Password Notifications in Password Solution. .DESCRIPTION Provides HTML report configuration for Password Notifications in Password Solution. The New-PasswordConfigurationReport function generates configuration for HTML report. .PARAMETER Enable Specifies whether to enable the report generation. The default value is $false. .PARAMETER ShowHTML Specifies whether to display the report in HTML format right after it's generated in default browser. The default value is $false. .PARAMETER Title Specifies the title of the report. The default value is "Password Solution Summary". .PARAMETER Online Specifies whether to generate the report using CDN for CSS and JS scripts, or use it locally. It doesn't require internet connectivity during generation. Makes the final output 3MB smaller. The default value is $false. .PARAMETER DisableWarnings Specifies whether to disable warning messages during report generation. The default value is $false. .PARAMETER ShowConfiguration Specifies whether to display the current Password Solution configuration settings. The default value is $false. .PARAMETER ShowAllUsers Specifies whether to display information about all user accounts. The default value is $false. .PARAMETER ShowRules Specifies whether to display information from the rules. The default value is $false. .PARAMETER ShowUsersSent Specifies whether to display information about users who have received (or not) password expiry notifications. The default value is $false. .PARAMETER ShowManagersSent Specifies whether to display information about managers who have received password expiry notifications. The default value is $false. .PARAMETER ShowEscalationSent Specifies whether to display information about escalation contacts who have received password expiry notifications. The default value is $false. .PARAMETER ShowSkippedUsers Specifies whether to display information about users who were during password expiry notifications because of inability to asses their expiration date. The default value is $false. .PARAMETER ShowSkippedLocations Specifies whether to display information about locations where skipped users are located. The default value is $false. .PARAMETER ShowSearchUsers Specifies whether to display information for searching who got password expiry notifications. The default value is $false. .PARAMETER ShowSearchManagers Specifies whether to display information for searching who got password expiry notifications and for which accounts from managers. The default value is $false. .PARAMETER ShowSearchEscalations Specifies whether to display information for searching who got password escalation notifications and what's the status of that message. The default value is $false. .PARAMETER ShowExternalSystemReplacementsUsers Specifies whether to display information about users who's email address was replaced by an external system. The default value is $false. .PARAMETER ShowExternalSystemReplacementsManagers Specifies whether to display information about managers who's email address was replaced by an external system. The default value is $false. .PARAMETER FilePath Specifies the file path for the report .PARAMETER AttachToEmail Specifies whether to attach the report to an administrative email. The default value is $false. .PARAMETER NestedRules Specifies whether to display nested password rules. Each rule has it's own tab with output. Having many rules and all other settings enabled can result in a very long list of tabs that's hard to navigate. This setting forces separate tab for all rules. The default value is $false. .OUTPUTS The function returns an ordered dictionary that contains the report settings. .EXAMPLE New-PasswordConfigurationReport -ShowHTML -Title "Password Configuration Report" -FilePath "C:\Reports\PasswordReport.html" .EXAMPLE $Date = Get-Date $Report = [ordered] @{ Enable = $true ShowHTML = $true Title = "Password Solution Summary" Online = $true DisableWarnings = $true ShowConfiguration = $true ShowAllUsers = $true ShowRules = $true ShowUsersSent = $true ShowManagersSent = $true ShowEscalationSent = $true ShowSkippedUsers = $true ShowSkippedLocations = $true ShowSearchUsers = $true ShowSearchManagers = $true ShowSearchEscalations = $true NestedRules = $false FilePath = "$PSScriptRoot\Reporting\PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).html" AttachToEmail = $true } New-PasswordConfigurationReport @Report #> [CmdletBinding()] param( [switch] $Enable, [switch] $ShowHTML, [string] $Title, [switch] $Online, [switch] $DisableWarnings, [switch] $ShowConfiguration, [switch] $ShowAllUsers, [switch] $ShowRules, [switch] $ShowUsersSent, [switch] $ShowManagersSent, [switch] $ShowEscalationSent, [switch] $ShowSkippedUsers, [switch] $ShowSkippedLocations, [switch] $ShowSearchUsers, [switch] $ShowSearchManagers, [switch] $ShowSearchEscalations , [string] $FilePath, [switch] $AttachToEmail, [switch] $NestedRules, [switch] $ShowExternalSystemReplacementsUsers, [switch] $ShowExternalSystemReplacementsManagers ) $Output = [ordered] @{ Type = "PasswordConfigurationReport" Settings = [ordered] @{ Enable = $Enable.IsPresent ShowHTML = $ShowHTML.IsPresent Title = $Title Online = $Online.IsPresent DisableWarnings = $DisableWarnings.IsPresent ShowConfiguration = $ShowConfiguration.IsPresent ShowAllUsers = $ShowAllUsers.IsPresent ShowRules = $ShowRules.IsPresent ShowUsersSent = $ShowUsersSent.IsPresent ShowManagersSent = $ShowManagersSent.IsPresent ShowEscalationSent = $ShowEscalationSent.IsPresent ShowSkippedUsers = $ShowSkippedUsers.IsPresent ShowSkippedLocations = $ShowSkippedLocations.IsPresent ShowSearchUsers = $ShowSearchUsers.IsPresent ShowSearchManagers = $ShowSearchManagers.IsPresent ShowSearchEscalations = $ShowSearchEscalations.IsPresent FilePath = $FilePath AttachToEmail = $AttachToEmail.IsPresent NestedRules = $NestedRules.IsPresent ShowExternalSystemReplacementsUsers = $ShowExternalSystemReplacementsUsers.IsPresent ShowExternalSystemReplacementsManagers = $ShowExternalSystemReplacementsManagers.IsPresent } } $Output } function New-PasswordConfigurationRule { <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER ReminderConfiguration Parameter description .PARAMETER Name Parameter description .PARAMETER Enable Parameter description .PARAMETER IncludeExpiring Parameter description .PARAMETER IncludePasswordNeverExpires Parameter description .PARAMETER PasswordNeverExpiresDays Parameter description .PARAMETER IncludeName Include user in rule if any of the properties match the value of Name in the properties defined in IncludeNameProperties .PARAMETER IncludeNameProperties Include user in rule if any of the properties match the value as defined in IncludeName .PARAMETER ExcludeName Exclude user from rule if any of the properties match the value of Name in the properties defined in ExcludeNameProperties .PARAMETER ExcludeNameProperties Exclude user from rule if any of the properties match the value as defined in ExcludeName .PARAMETER IncludeOU Parameter description .PARAMETER ExcludeOU Parameter description .PARAMETER IncludeGroup Parameter description .PARAMETER ExcludeGroup Parameter description .PARAMETER ReminderDays Days before expiration to send reminder. If not set and ProcessManagersOnly is not set, the rule will be throw an error. .PARAMETER ManagerReminder Parameter description .PARAMETER ManagerNotCompliant Parameter description .PARAMETER ManagerNotCompliantDisplayName Parameter description .PARAMETER ManagerNotCompliantEmailAddress Parameter description .PARAMETER ManagerNotCompliantDisabled Parameter description .PARAMETER ManagerNotCompliantMissing Parameter description .PARAMETER ManagerNotCompliantMissingEmail Parameter description .PARAMETER ManagerNotCompliantLastLogonDays Parameter description .PARAMETER SecurityEscalation Parameter description .PARAMETER SecurityEscalationDisplayName Parameter description .PARAMETER SecurityEscalationEmailAddress Parameter description .PARAMETER OverwriteEmailProperty Parameter description .PARAMETER OverwriteManagerProperty Parameter description .PARAMETER OverwriteEmailFromExternalUsers Allow to overwrite email from external users for specific rule .PARAMETER ProcessManagersOnly This parameters is used to process users, but only managers will be notified. Sending emails to users within the rule will be skipped completly. This is useful if users would have email addresses, that would normally trigger an email to them. .EXAMPLE An example .NOTES General notes #> [CmdletBinding()] param( [scriptblock] $ReminderConfiguration, [parameter(Mandatory)][string] $Name, [switch] $Enable, [switch] $IncludeExpiring, [switch] $IncludePasswordNeverExpires, [nullable[int]]$PasswordNeverExpiresDays, [string[]] $IncludeNameProperties, [string[]] $IncludeName, [string[]] $ExcludeNameProperties, [string[]] $ExcludeName, [string[]] $IncludeOU, [string[]] $ExcludeOU, [string[]] $IncludeGroup, [string[]] $ExcludeGroup, [alias('ExpirationDays', 'Days')][Array] $ReminderDays, [switch] $ManagerReminder, [switch] $ManagerNotCompliant, [string] $ManagerNotCompliantDisplayName, [string] $ManagerNotCompliantEmailAddress, [switch] $ManagerNotCompliantDisabled, [switch] $ManagerNotCompliantMissing, [switch]$ManagerNotCompliantMissingEmail, [nullable[int]] $ManagerNotCompliantLastLogonDays, [switch] $SecurityEscalation, [string] $SecurityEscalationDisplayName, [string] $SecurityEscalationEmailAddress, [string] $OverwriteEmailProperty, [string] $OverwriteManagerProperty, [switch] $ProcessManagersOnly, [switch] $OverwriteEmailFromExternalUsers ) if (-not $ProcessManagersOnly) { if ($null -eq $ReminderDays) { $ErrorMessage = "'ReminderDays' is required for rule '$Name', unless 'ProcessManagersOnly' is set. This is to make sure the rule is not skipped completly." Write-Color -Text "[e]", " Processing rule ", $Name, " failed because of error: ", $ErrorMessage -Color Yellow, White, Red return [ordered] @{ Type = 'PasswordConfigurationRule' Error = $ErrorMessage } } } $Output = [ordered] @{ Name = $Name Enable = $Enable.IsPresent IncludeExpiring = $IncludeExpiring.IsPresent IncludePasswordNeverExpires = $IncludePasswordNeverExpires.IsPresent Reminders = $ReminderDays PasswordNeverExpiresDays = $PasswordNeverExpiresDays IncludeNameProperties = $IncludeNameProperties IncludeName = $IncludeName IncludeOU = $IncludeOU ExcludeOU = $ExcludeOU SendToManager = [ordered] @{} ProcessManagersOnly = $ProcessManagersOnly.IsPresent OverwriteEmailProperty = $OverwriteEmailProperty OverwriteManagerProperty = $OverwriteManagerProperty OverwriteEmailFromExternalUsers = $OverwriteEmailFromExternalUsers.IsPresent } $Output.SendToManager['Manager'] = [ordered] @{ Enable = $false Reminders = [ordered] @{} } $Output.SendToManager['ManagerNotCompliant'] = [ordered] @{ Enable = $false Manager = [ordered] @{ DisplayName = $ManagerNotCompliantDisplayName EmailAddress = $ManagerNotCompliantEmailAddress } Disabled = $ManagerNotCompliantDisabled Missing = $ManagerNotCompliantMissing MissingEmail = $ManagerNotCompliantMissingEmail LastLogon = if ($PSBoundParameters.ContainsKey('ManagerNotCompliantLastLogonDays')) { $true } else { $false } LastLogonDays = $ManagerNotCompliantLastLogonDays Reminders = [ordered] @{ } } $Output.SendToManager['SecurityEscalation'] = [ordered] @{ Enable = $false Manager = [ordered] @{ DisplayName = $SecurityEscalationDisplayName EmailAddress = $SecurityEscalationEmailAddress } Reminders = [ordered] @{} } if ($ManagerReminder) { $Output.SendToManager['Manager'].Enable = $true } if ($ManagerNotCompliant) { $Output.SendToManager['ManagerNotCompliant'].Enable = $true } if ($SecurityEscalation) { $Output.SendToManager['SecurityEscalation'].Enable = $true } if ($ReminderConfiguration) { try { $RemindersExecution = & $ReminderConfiguration } catch { Write-Color -Text "[e]", " Processing rule ", $Output.Name, " failed because of error: ", $_.Exception.Message -Color Yellow, White, Red return [ordered] @{ Type = 'PasswordConfigurationRule' Error = $_.Exception.Message } } foreach ($Reminder in $RemindersExecution) { if ($Reminder.Type -eq 'Manager') { foreach ($ReminderReminders in $Reminder.Reminders) { $Output.SendToManager['Manager'].Reminders += $ReminderReminders } } elseif ($Reminder.Type -eq 'ManagerNotCompliant') { foreach ($ReminderReminders in $Reminder.Reminders) { $Output.SendToManager['ManagerNotCompliant'].Reminders += $ReminderReminders } } elseif ($Reminder.Type -eq 'Security') { foreach ($ReminderReminders in $Reminder.Reminders) { $Output.SendToManager['SecurityEscalation'].Reminders += $ReminderReminders } } else { throw "Invalid reminder type: $($Reminder.Type)" } } } Remove-EmptyValue -Hashtable $Output -Recursive -Rerun 2 $Configuration = [ordered] @{ Type = 'PasswordConfigurationRule' Settings = $Output } $Configuration } function New-PasswordConfigurationRuleReminder { [CmdletBinding(DefaultParameterSetName = 'Daily')] param( [Parameter(Mandatory, ParameterSetName = 'Daily')] [Parameter(Mandatory, ParameterSetName = 'DayOfWeek')] [Parameter(Mandatory, ParameterSetName = 'DayOfMonth')] [ValidateSet('Manager', 'ManagerNotCompliant', 'Security')][string] $Type, [Parameter(Mandatory, ParameterSetName = 'Daily')] [Parameter(Mandatory, ParameterSetName = 'DayOfWeek')] [Parameter(Mandatory, ParameterSetName = 'DayOfMonth')] [alias('ConditionDays', 'Days')][Array] $ExpirationDays, [Parameter(Mandatory, ParameterSetName = 'DayOfWeek')] [ValidateSet( 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' )][Array] $DayOfWeek, [Parameter(Mandatory, ParameterSetName = 'DayOfMonth')] [Array] $DayOfMonth, [Parameter(ParameterSetName = 'Daily')] [Parameter(ParameterSetName = 'DayOfWeek')] [Parameter(ParameterSetName = 'DayOfMonth')] [ValidateSet('lt', 'gt', 'eq', 'in')][string] $ComparisonType = 'eq' ) if ($ComparisonType -in 'eq', 'lt', 'gt') { if ($ExpirationDays.Count -gt 1) { throw "Only one number for 'ExpirationDays' can be specified when using comparison types 'eq', 'lt', and 'gt'." } else { $ExpirationDaysToUse = $ExpirationDays[0] } } else { $ExpirationDaysToUse = $ExpirationDays } if ($PSCmdlet.ParameterSetName -eq 'Daily') { $Reminders = [ordered] @{ Type = $Type Reminders = @{ Default = [ordered] @{ Enable = $true } } } } elseif ($PSCmdlet.ParameterSetName -eq 'DayOfWeek') { $Reminders = [ordered] @{ Type = $Type Reminders = @{ OnDay = [ordered] @{ Enable = $true Reminder = $ExpirationDaysToUse ComparisonType = $ComparisonType Days = $DayOfWeek } } } } elseif ($PSCmdlet.ParameterSetName -eq 'DayOfMonth') { $Reminders = [ordered] @{ Type = $Type Reminders = @{ OnDayOfMonth = [ordered] @{ Enable = $true Reminder = $ExpirationDaysToUse ComparisonType = $ComparisonType Days = $DayOfMonth } } } } $Reminders } function New-PasswordConfigurationTemplate { [CmdletBinding()] param( [parameter(Mandatory)][ScriptBlock] $Template, [parameter(Mandatory)][string] $Subject, [parameter(Mandatory)][ValidateSet('PreExpiry', 'PostExpiry', 'Manager', 'ManagerNotCompliant', 'Security', 'Admin')] $Type ) $Output = [ordered] @{ Type = "PasswordConfigurationTemplate$Type" Settings = [ordered] @{ Template = $Template Subject = $Subject } } $Output } function New-PasswordConfigurationType { [CmdletBinding()] param( [Parameter(Mandatory)][ValidateSet('User', 'Manager', 'Security', 'Admin')][string] $Type, [switch] $Enable, [int] $SendCountMaximum, [string] $DefaultEmail, [switch] $AttachCSV ) $Output = [ordered] @{ Type = "PasswordConfigurationType$Type" Settings = @{ Enable = $Enable.IsPresent SendCountMaximum = $SendCountMaximum SendToDefaultEmail = if ($DefaultEmail) { $true } else { $false } DefaultEmail = $DefaultEmail OverwriteEmailProperty = $OverwriteEmailProperty AttachCSV = $AttachCSV.IsPresent } } $Output } function Show-PasswordQuality { <# .SYNOPSIS Creates an HTML report showing password quality for all user objects in Active Directory. .DESCRIPTION Creates an HTML report showing password quality for all user objects in Active Directory. This comman utilizes DSInternals PowerShell module to get the data. Then it uses PSWriteHTML to create nice looking report. .PARAMETER FilePath Path to the file where report will be saved. .PARAMETER DontShow If specified, report will not be opened in a browser. .PARAMETER Online If specified report will use CDN for JS and CSS files. If not specified, it will merge all CSS and JS files into one HTML file. This makes the file at least 3MB bigger, even if there is very small amount of data. Keep in mind that this report can be created without internet access, just that opening it in a browser with -Online switch will require internet access. .PARAMETER WeakPasswords List of weak passwords that should be checked for. Provide a list of common passwords that you want to check for, and that your users may have used. .PARAMETER WeakPasswordsFilePath Path to a file that contains weak passwords, one password per line. .PARAMETER WeakPasswordsHashesFile Path to a file that contains NT hashes of weak passwords, one hash in HEX format per line. For performance reasons, the -WeakPasswordHashesSortedFile parameter should be used instead. .PARAMETER WeakPasswordsHashesSortedFile Path to a file that contains NT hashes of weak passwords, one hash in HEX format per line. The hashes must be sorted alphabetically, because a binary search is performed. This parameter is typically used with a list of leaked password hashes from HaveIBeenPwned. .PARAMETER SeparateDuplicateGroups If specified, report will show duplicate groups separately, one group per tab. .EXAMPLE Show-PasswordQuality -FilePath $PSScriptRoot\Reporting\PasswordQuality.html -Online -WeakPasswords "Test1", "Test2", "Test3" -Verbose .EXAMPLE Show-PasswordQuality -FilePath "C:\Support\GitHub\TheDashboard\Ignore\Reports\CustomReports\PasswordQuality_$(Get-Date -f yyyy-MM-dd_HHmmss).html" -WeakPasswords "Test1", "Test2", "Test3" #-Verbose .NOTES General notes #> [CmdletBinding()] param( [alias('ForestName')][string] $Forest, [string[]] $ExcludeDomains, [alias('Domain', 'Domains')][string[]] $IncludeDomains, [System.Collections.IDictionary] $ExtendedForestInformation, [string] $FilePath, [switch] $DontShow, [switch] $Online, [alias('KnownPasswords')][string[]] $WeakPasswords, [alias('KnownPasswordsFilePath')][string] $WeakPasswordsFilePath, [alias('KnownPasswordsHashesFile')][string] $WeakPasswordsHashesFile, [alias('KnownPasswordsHashesSortedFile')][string] $WeakPasswordsHashesSortedFile, [switch] $SeparateDuplicateGroups, [switch] $PassThru, [switch] $AddWorldMap, [alias('LogFile')][string] $LogPath, [int] $LogMaximum, [switch] $LogShowTime, [string] $LogTimeFormat = "yyyy-MM-dd HH:mm:ss" ) $TimeStart = Start-TimeLog $Script:Reporting = [ordered] @{} $Script:Reporting['Version'] = Get-GitHubVersion -Cmdlet 'Show-PasswordQuality' -RepositoryOwner 'evotecit' -RepositoryName 'PasswordSolution' Write-Color -Text '[i]', "[PasswordSolution] ", 'Version', ' [Informative] ', $Script:Reporting['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta Set-LoggingCapabilities -LogPath $LogPath -LogMaximum $LogMaximum -ShowTime:$LogShowTime.IsPresent -TimeFormat $TimeFormat Write-Color -Text '[i]', "[PasswordSolution] ", 'Version', ' [Informative] ', $Script:Reporting['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta -NoConsoleOutput Write-Color '[i]', ' Gathering passwords data' -Color Yellow, DarkGray, Yellow, DarkGray, Magenta Write-Color '[i]', ' Using provided ', $WeakPasswords.Count, " weak passwords to verify against." -Color Yellow, DarkGray, Yellow, DarkGray, Magenta $TimeStartPasswords = Start-TimeLog $findPasswordQualitySplat = @{ IncludeStatistics = $true WeakPasswords = $WeakPasswords WeakPasswordsFilePath = $WeakPasswordsFilePath WeakPasswordsHashesFile = $WeakPasswordsHashesFile WeakPasswordsHashesSortedFile = $WeakPasswordsHashesSortedFile Forest = $Forest ExcludeDomains = $ExcludeDomains IncludeDomains = $IncludeDomains ExtendedForestInformation = $ExtendedForestInformation } $PasswordQuality = Find-PasswordQuality @findPasswordQualitySplat if (-not $PasswordQuality) { return } $Users = $PasswordQuality.Users $Statistics = $PasswordQuality.Statistics $Countries = $PasswordQuality.StatisticsCountry $CountriesCodes = $PasswordQuality.StatisticsCountryCode $Continents = $PasswordQuality.StatisticsContinents $EndLogPasswords = Stop-TimeLog -Time $TimeStartPasswords -Option OneLiner Write-Color '[i]', ' Time to gather passwords data ', $EndLogPasswords -Color Yellow, DarkGray, Yellow, DarkGray, Magenta $TimeStartHTML = Start-TimeLog Write-Color -Text '[i] ', 'Generating HTML report...' -Color Yellow, DarkGray New-HTML { New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow New-HTMLPanelStyle -BorderRadius 0px New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ', ' -ArrayJoin New-HTMLHeader { New-HTMLSection -Invisible { New-HTMLSection { New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue } -JustifyContent flex-start -Invisible New-HTMLSection { New-HTMLText -Text "Password Solution - $($Script:Reporting['Version'])" -Color Blue } -JustifyContent flex-end -Invisible } } Write-Color -Text '[i] ', 'Generating summary statistics' -Color Yellow, DarkGray New-HTMLSection { New-HTMLSection -Invisible { New-HTMLPanel -Invisible { New-HTMLText -Text @( "This report shows current status of an Active Directory forest $($PasswordQuality.Forest)." "It focuses on the password quality of users in the following domains: " ) -FontSize 12px New-HTMLList { foreach ($Domain in $PasswordQuality.Domains) { New-HTMLListItem -Text $Domain -Color Blue } } -FontSize 12px $WeakPasswordsFileInformation = $PasswordQuality.WeakPasswordsFileInformation if ($WeakPasswords.Count -gt 0 -or $WeakPasswordsFileInformation.WeakPasswordsStats -or $WeakPasswordsFileInformation.WeakPasswordHashesStats -or $WeakPasswordsFileInformation.WeakPasswordHashesSortedStats) { New-HTMLText -Text @( "The report uses following weak password features: " ) -FontSize 12px New-HTMLList { if ($WeakPasswords.Count -gt 0) { New-HTMLListItem -Text @( "This report uses ", $WeakPasswords.Count, " weak passwords to check for, as provided during runtime." ) -FontSize 12px -Color None, Red, None -FontWeight normal, bold, normal } if ($WeakPasswordsFileInformation.WeakPasswordsStats) { New-HTMLListItem -Text @( "This report uses weak passwords from ", $WeakPasswordsFileInformation.WeakPasswordsStats.FullName, " to check for, as provided during runtime, size ", $WeakPasswordsFileInformation.WeakPasswordsStats.Size, ", last write time ", $WeakPasswordsFileInformation.WeakPasswordsStats.LastWriteTime, "." ) -FontSize 12px -Color None, Red, None, Blue, None, Blue, None -FontWeight normal, bold, normal, bold, normal, bold, normal } if ($WeakPasswordsFileInformation.WeakPasswordHashesStats) { New-HTMLListItem -Text @( "This report uses weak passwords hashes from ", $WeakPasswordsFileInformation.WeakPasswordHashesStats.FullName, " to check for, as provided during runtime, size ", $WeakPasswordsFileInformation.WeakPasswordHashesStats.Size, ", last write time ", $WeakPasswordsFileInformation.WeakPasswordHashesStats.LastWriteTime, "." ) -FontSize 12px -Color None, Red, None, Blue, None, Blue, None -FontWeight normal, bold, normal, bold, normal, bold, normal } if ($WeakPasswordsFileInformation.WeakPasswordHashesSortedStats) { New-HTMLListItem -Text @( "This report uses weak passwords hashes from ", $WeakPasswordsFileInformation.WeakPasswordHashesSortedStats.FullName, " to check for, as provided during runtime, size ", $WeakPasswordsFileInformation.WeakPasswordHashesSortedStats.Size, ", last write time ", $WeakPasswordsFileInformation.WeakPasswordHashesSortedStats.LastWriteTime, "." ) -FontSize 12px -Color None, Red, None, Blue, None, Blue, None -FontWeight normal, bold, normal, bold, normal, bold, normal } } } New-HTMLText -Text "Here's a short overview of what this report shows:" -Color None -FontSize 12px New-HTMLList { foreach ($Statistic in $Statistics.Keys | Where-Object { $_ -notlike '*EnabledOnly' -and $_ -notlike '*DisabledOnly' } ) { $ValueTotal = $Statistics[$Statistic] if ($Statistic -eq "DuplicatePasswordGroups") { $ValueEnabled = $Statistics['DuplicatePasswordUsersEnabledOnly'] $ValueDisabled = $Statistics['DuplicatePasswordUsersDisabledOnly'] New-HTMLListItem -Text @( "$($Statistic)", " property shows there are " "$ValueTotal" " groups of people with duplicate passwords." ) -Color Blue, None, Salmon, None, LightSkyBlue, None -FontWeight bold, normal, bold, normal, bold, normal } elseif ($Statistic -eq 'DuplicatePasswordUsers') { $ValueEnabled = $Statistics['DuplicatePasswordUsersEnabledOnly'] $ValueDisabled = $Statistics['DuplicatePasswordUsersDisabledOnly'] New-HTMLListItem -Text @( "$($Statistic)", " property shows there are " "$ValueEnabled" "enabled accounts, and " $ValueDisabled " disabled accounts having duplicate passwords with other accounts." ) -Color Blue, None, Salmon, None, LightSkyBlue, None -FontWeight bold, normal, bold, normal, bold, normal } else { $ValueEnabled = $Statistics[$Statistic + 'EnabledOnly'] $ValueDisabled = $Statistics[$Statistic + 'DisabledOnly'] New-HTMLListItem -Text @( "$($Statistic)", " property shows there are " "$ValueEnabled " "enabled accounts, and " "$ValueDisabled " "that are disabled." ) -Color Blue, None, Salmon, None, LightSkyBlue, None -FontWeight bold, normal, bold, normal, bold, normal } } } -Type Unordered -FontSize 12px New-HTMLText -Text "Please review the report and make sure that you're happy with findings!" -Color Blue -FontSize 12px } } New-HTMLSection -Invisible { New-HTMLChart { New-ChartBarOptions -Type barStacked New-ChartAxisY -LabelMaxWidth 250 -Show -LabelAlign left New-ChartLegend -LegendPosition bottom -HorizontalAlign center -Color Alizarin, LightSkyBlue -Names 'Enabled', 'Disabled' foreach ($Statistic in $Statistics.Keys | Where-Object { $_ -notlike '*EnabledOnly' -and $_ -notlike '*DisabledOnly' } ) { if ($Statistic -eq "DuplicatePasswordGroups") { $ValueTotal = $Statistics[$Statistic] New-ChartBar -Name $Statistic -Value @($ValueTotal, 0) } else { $ValueEnabled = $Statistics[$Statistic + 'EnabledOnly'] $ValueDisabled = $Statistics[$Statistic + 'DisabledOnly'] New-ChartBar -Name $Statistic -Value @($ValueEnabled, $ValueDisabled) } } } } } $PropertiesHighlight = @( 'ClearTextPassword' 'LMHash' 'EmptyPassword' 'WeakPassword' 'AESKeysMissing' 'PreAuthNotRequired' 'DESEncryptionOnly' 'Kerberoastable' 'DelegatableAdmins' 'SmartCardUsersWithPassword' ) Write-Color -Text '[i] ', 'Generating users table with all information' -Color Yellow, DarkGray New-HTMLSection -HeaderText "Password Quality" { New-HTMLTable -DataTable $Users -Filtering { New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $true -BackgroundColor LimeGreen -FailBackgroundColor BlizzardBlue New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator lt -Value 30 -BackgroundColor LimeGreen -HighlightHeaders LastLogonDays, LastLogonDate New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator gt -Value 30 -BackgroundColor Orange -HighlightHeaders LastLogonDays, LastLogonDate New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator gt -Value 60 -BackgroundColor Alizarin -HighlightHeaders LastLogonDays, LastLogonDate New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType string -Operator eq -Value '' -BackgroundColor None -HighlightHeaders LastLogonDays, LastLogonDate New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -Operator ge -Value 0 -BackgroundColor LimeGreen -HighlightHeaders PasswordLastSet, PasswordLastChangedDays New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -Operator gt -Value 300 -BackgroundColor Orange -HighlightHeaders PasswordLastSet, PasswordLastChangedDays New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -Operator gt -Value 360 -BackgroundColor Alizarin -HighlightHeaders PasswordLastSet, PasswordLastChangedDays New-HTMLTableCondition -Name 'PasswordNotRequired' -ComparisonType string -Operator eq -Value $false -BackgroundColor LimeGreen -FailBackgroundColor Alizarin New-HTMLTableCondition -Name 'PasswordExpired' -ComparisonType string -Operator eq -Value $false -BackgroundColor LimeGreen -FailBackgroundColor Alizarin -HighlightHeaders PasswordExpired, DaysToExpire, DateExpiry foreach ($Property in $PropertiesHighlight) { New-HTMLTableCondition -Name $Property -ComparisonType string -Operator eq -Value $true -BackgroundColor Salmon -FailBackgroundColor LightGreen } New-HTMLTableCondition -Name 'DuplicatePasswordGroups' -ComparisonType string -Operator ne -Value "" -BackgroundColor Orange -FailBackgroundColor LightGreen } -ScrollX -ExcludeProperty 'RuleName', 'RuleOptions', 'CountryCode', 'Type', 'ManagerDN', 'DistinguishedName', 'MemberOf' } if ($SeparateDuplicateGroups) { Write-Color -Text '[i] ', 'Generating duplicate password groups section' -Color Yellow, DarkGray New-HTMLSection -HeaderText "Duplicate Password Groups" { $TotalDuplicateGroups = 0 $EnabledUsersInDuplicateGroups = 0 $DisabledUsersInDuplicateGroups = 0 $DuplicateGroups = [ordered] @{} foreach ($User in $Users) { if ($User.DuplicatePasswordGroups) { if ($User.Enabled) { $EnabledUsersInDuplicateGroups++ } else { $DisabledUsersInDuplicateGroups++ } if (-not $DuplicateGroups[$User.DuplicatePasswordGroups]) { $DuplicateGroups[$User.DuplicatePasswordGroups] = [PSCustomObject] @{ GroupName = $User.DuplicatePasswordGroups UsersTotal = 0 UsersEnabled = 0 UsersDisabled = 0 WeakPassword = $false Users = [System.Collections.Generic.List[string]]::new() Country = [System.Collections.Generic.List[string]]::new() UsersBySamAccountName = [System.Collections.Generic.List[string]]::new() UsersByUPN = [System.Collections.Generic.List[string]]::new() UsersByEmail = [System.Collections.Generic.List[string]]::new() } } if ($User.WeakPassword) { $DuplicateGroups[$User.DuplicatePasswordGroups].WeakPassword = $true } $DuplicateGroups[$User.DuplicatePasswordGroups].Users.Add($User.Name) if ($User.Enabled) { $DuplicateGroups[$User.DuplicatePasswordGroups].UsersEnabled++ } else { $DuplicateGroups[$User.DuplicatePasswordGroups].UsersDisabled++ } $DuplicateGroups[$User.DuplicatePasswordGroups].UsersTotal++ if ($User.EmailAddress) { $DuplicateGroups[$User.DuplicatePasswordGroups].UsersByEmail.Add($User.EmailAddress) } if ($User.UserPrincipalName) { $DuplicateGroups[$User.DuplicatePasswordGroups].UsersByUPN.Add($User.UserPrincipalName) } if ($User.SamAccountName) { $DuplicateGroups[$User.DuplicatePasswordGroups].UsersBySamAccountName.Add($User.SamAccountName) } $DuplicateGroups[$User.DuplicatePasswordGroups].Country.Add($User.Country) } } $TotalDuplicateGroups = $DuplicateGroups.Keys.Count foreach ($Group in $DuplicateGroups.Values) { $Group.Country = $Group.Country | Select-Object -Unique } New-HTMLContainer { New-HTMLSection { New-HTMLPanel { New-HTMLToast -TextHeader 'Total Duplicate Groups' -Text "Groups of users to review: $TotalDuplicateGroups" -BarColorLeft MayaBlue -IconSolid info-circle -IconColor MayaBlue } -Invisible New-HTMLPanel { New-HTMLToast -TextHeader 'Enabled Users' -Text "Users with duplicate password that are enabled: $EnabledUsersInDuplicateGroups" -BarColorLeft OrangeRed -IconSolid info-circle -IconColor OrangeRed } -Invisible New-HTMLPanel { New-HTMLToast -TextHeader 'Disabled Users' -Text "Users with duplicate password that are disabled: $DisabledUsersInDuplicateGroups" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel } -Invisible } -Invisible New-HTMLText -Text @( 'The following table shows the users that have the same password as other users in the same group. ' 'The table is sortable and filterable. ' 'The table also shows the country of the user. ' 'The table also shows the email address, UPN and SamAccountName of the user.' ) -FontSize 12px New-HTMLSection -Invisible { New-HTMLTable -DataTable $DuplicateGroups.Values -Filtering -Title "Duplicate Password Group: $DuplicateGroup" { New-HTMLTableCondition -Name 'WeakPassword' -ComparisonType string -Operator eq -Value $true -BackgroundColor Salmon -FailBackgroundColor LightBlue }-ScrollX -ExcludeProperty 'RuleName', 'RuleOptions', 'Type', 'CountryCode' } New-HTMLText -Text @( "Please NOTE: " "number of " "users" " , may not be the same as the number of users in " "UsersBySamAccountName" ", " "UsersByUpn" " or " "UsersByEmail" " columns. We only show users with email address, UPN or SamAccountName if it exists. " "If the account doesn't have email, UPN or SamAccountName, we don't show it in the table." ) -FontSize 12px -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold, normal, normal } } } if ($AddWorldMap) { Write-Color -Text '[i] ', 'Generating duplicate passwords map' -Color Yellow, DarkGray New-HTMLSection -HeaderText 'Duplicate Passwords Per Country' { New-HTMLTabPanel { New-HTMLTab -Name 'Map showing duplicate passwords per country' { New-HTMLSection -Invisible { New-HTMLPanel { New-HTMLMap -Map world_countries { foreach ($Country in $CountriesCodes['DuplicatePasswordUsers'].Keys) { if ($Country -eq 'Unknown') { New-MapArea -Area 'GL' -Value $CountriesCodes['DuplicatePasswordUsers'][$Country] -Tooltip { New-HTMLText -Text @( 'Unknown / Unavailable' '<br>' "Users with duplicate passwords $($CountriesCodes['DuplicatePasswordUsers'][$Country])" ) -Color Black, Black, Blue -FontWeight bold, normal, normal -SkipParagraph -FontSize 15px, 14px, 14px } } else { New-MapArea -Area $Country -Value $CountriesCodes['DuplicatePasswordUsers'][$Country] -Tooltip { New-HTMLText -Text @( Convert-CountryCodeToCountry -CountryCode $Country '<br>' "Users with duplicate passwords $($CountriesCodes['DuplicatePasswordUsers'][$Country])" ) -Color Black, Black, Blue -FontWeight bold, normal, normal -SkipParagraph -FontSize 15px, 14px, 14px } } } New-MapLegendOption -Type 'Area' -Mode horizontal New-MapLegendOption -Type 'Plot' -Mode horizontal New-MapLegendSlice -Type 'Area' -Label 'Duplicate passwords up to 5' -Min 0 -Max 5 -SliceColor 'Bisque' -StrokeWidth 0 New-MapLegendSlice -Type 'Area' -Label 'Duplicate between 5 and 15' -Min 6 -Max 15 -SliceColor 'Amber' -StrokeWidth 0 New-MapLegendSlice -Type 'Area' -Label 'Duplicate between 16 and 30' -Min 16 -Max 30 -SliceColor 'CarnationPink' -StrokeWidth 0 New-MapLegendSlice -Type 'Area' -Label 'Duplicate between 31 and 50' -Min 31 -Max 50 -SliceColor 'BrinkPink' -StrokeWidth 0 New-MapLegendSlice -Type 'Area' -Label 'Duplicate over 50' -Min 51 -SliceColor 'Red' -StrokeWidth 0 } -ShowAreaLegend New-HTMLText -Text @( "The map shows the number of users with duplicate passwords per country. The legend shows the number of users with duplicate passwords per color." ) -FontSize 12px } } } New-HTMLTab -Name 'Duplicate Passwords Per Country' { New-HTMLTable -DataTable $Countries['DuplicatePasswordUsers'] -Filtering } New-HTMLTab -Name 'Duplicate Passwords Per Continent' { New-HTMLTable -DataTable $Continents['DuplicatePasswordUsers'] -Filtering } } } Write-Color -Text '[i] ', 'Generating weak password map' -Color Yellow, DarkGray New-HTMLSection -HeaderText 'Weak Password Per Country' { New-HTMLTabPanel { New-HTMLTab -Name 'Map showing weak password per country' { New-HTMLSection -Invisible { New-HTMLPanel { New-HTMLMap -Map world_countries { foreach ($Country in $CountriesCodes['WeakPassword'].Keys) { if ($Country -eq 'Unknown') { New-MapArea -Area 'GL' -Value $CountriesCodes['WeakPassword'][$Country] -Tooltip { New-HTMLText -Text @( 'Unknown / Unavailable' '<br>' "Users with weak passwords $($CountriesCodes['WeakPassword'][$Country])" ) -Color Black, Black, Blue -FontWeight bold, normal, normal -SkipParagraph -FontSize 15px, 14px, 14px } } else { New-MapArea -Area $Country -Value $CountriesCodes['WeakPassword'][$Country] -Tooltip { New-HTMLText -Text @( Convert-CountryCodeToCountry -CountryCode $Country '<br>' "Users with weak passwords $($CountriesCodes['WeakPassword'][$Country])" ) -Color Black, Black, Blue -FontWeight bold, normal, normal -SkipParagraph -FontSize 15px, 14px, 14px } } } New-MapLegendOption -Type 'Area' -Mode horizontal New-MapLegendOption -Type 'Plot' -Mode horizontal New-MapLegendSlice -Type 'Area' -Label 'Weak passwords up to 5' -Min 0 -Max 5 -SliceColor 'Bisque' -StrokeWidth 0 New-MapLegendSlice -Type 'Area' -Label 'Weak between 5 and 15' -Min 6 -Max 15 -SliceColor 'Amber' -StrokeWidth 0 New-MapLegendSlice -Type 'Area' -Label 'Weak between 16 and 30' -Min 16 -Max 30 -SliceColor 'CarnationPink' -StrokeWidth 0 New-MapLegendSlice -Type 'Area' -Label 'Weak between 31 and 50' -Min 31 -Max 50 -SliceColor 'BrinkPink' -StrokeWidth 0 New-MapLegendSlice -Type 'Area' -Label 'Weak over 50' -Min 51 -SliceColor 'Red' -StrokeWidth 0 } -ShowAreaLegend } } } New-HTMLTab -Name 'Weak Password Per Country' { New-HTMLTable -DataTable $Countries['WeakPassword'] -Filtering } New-HTMLTab -Name 'Weak Password Per Continent' { New-HTMLTable -DataTable $Continents['WeakPassword'] -Filtering } } } if ($LogPath -and (Test-Path -LiteralPath $LogPath)) { $LogContent = Get-Content -Raw -LiteralPath $LogPath New-HTMLSection -Name 'Log' { New-HTMLCodeBlock -Code $LogContent -Style generic } } } } -ShowHTML:(-not $DontShow.IsPresent) -Online:$Online.IsPresent -TitleText "Password Solution - Quality Password Check" -Author "Password Solution" -FilePath $FilePath $EndLogHTML = Stop-TimeLog -Time $TimeStartHTML -Option OneLiner $EndLog = Stop-TimeLog -Time $TimeStart -Option OneLiner Write-Color '[i]', ' Time to generate HTML ', $EndLogHTML -Color Yellow, DarkGray, Yellow, DarkGray, Magenta Write-Color '[i]', ' Time to generate ', $EndLog -Color Yellow, DarkGray, Yellow, DarkGray, Magenta Write-Color '[i]', "[PasswordSolution] ", 'Version', ' [Informative] ', $Script:Reporting['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta if ($PassThru) { $PasswordQuality } } function Start-PasswordSolution { <# .SYNOPSIS Starts Password Expiry Notifications for the whole forest .DESCRIPTION Starts Password Expiry Notifications for the whole forest .PARAMETER ConfigurationDSL Parameter description .PARAMETER EmailParameters Parameters for Email. Uses Mailozaurr splatting behind the scenes, so it supports all options that Mailozaurr does. .PARAMETER OverwriteEmailProperty Property responsible for overwriting the default email field in Active Directory. Useful when the password notification has to go somewhere else than users email address. .PARAMETER UserSection Parameter description .PARAMETER ManagerSection Parameter description .PARAMETER SecuritySection Parameter description .PARAMETER AdminSection Parameter description .PARAMETER UsersExternalSystem Property responsible for overwriting the default email field in Active Directory. Useful when the password notification has to go somewhere else than users email address. It comes in a specific format as generated by `New-PasswordConfigurationExternalUsers` .PARAMETER Rules Parameter description .PARAMETER TemplatePreExpiry Parameter description .PARAMETER TemplatePreExpirySubject Parameter description .PARAMETER TemplatePostExpiry Parameter description .PARAMETER TemplatePostExpirySubject Parameter description .PARAMETER TemplateManager Parameter description .PARAMETER TemplateManagerSubject Parameter description .PARAMETER TemplateSecurity Parameter description .PARAMETER TemplateSecuritySubject Parameter description .PARAMETER TemplateManagerNotCompliant Parameter description .PARAMETER TemplateManagerNotCompliantSubject Parameter description .PARAMETER TemplateAdmin Parameter description .PARAMETER TemplateAdminSubject Parameter description .PARAMETER Logging Parameter description .PARAMETER HTMLReports Parameter description .PARAMETER SearchPath Parameter description .EXAMPLE An example .NOTES General notes #> [CmdletBinding(DefaultParameterSetName = 'DSL')] param( [Parameter(ParameterSetName = 'Legacy', Position = 0)] [Parameter(ParameterSetName = 'DSL', Position = 0)][scriptblock] $ConfigurationDSL, [Parameter(Mandatory, ParameterSetName = 'Legacy')][System.Collections.IDictionary] $EmailParameters, [Parameter(ParameterSetName = 'Legacy')][string] $OverwriteEmailProperty, [Parameter(ParameterSetName = 'Legacy')][string] $OverwriteManagerProperty, [Parameter(Mandatory, ParameterSetName = 'Legacy')][System.Collections.IDictionary] $UserSection, [Parameter(Mandatory, ParameterSetName = 'Legacy')][System.Collections.IDictionary] $ManagerSection, [Parameter(Mandatory, ParameterSetName = 'Legacy')][System.Collections.IDictionary] $SecuritySection, [Parameter(Mandatory, ParameterSetName = 'Legacy')][System.Collections.IDictionary] $AdminSection, [Parameter(ParameterSetName = 'Legacy')][System.Collections.IDictionary] $UsersExternalSystem, [Parameter(Mandatory, ParameterSetName = 'Legacy')][Array] $Rules, [Parameter(ParameterSetName = 'Legacy')][scriptblock] $TemplatePreExpiry, [Parameter(ParameterSetName = 'Legacy')][string] $TemplatePreExpirySubject, [Parameter(ParameterSetName = 'Legacy')][scriptblock] $TemplatePostExpiry, [Parameter(ParameterSetName = 'Legacy')][string] $TemplatePostExpirySubject, [Parameter(Mandatory, ParameterSetName = 'Legacy')][scriptblock] $TemplateManager, [Parameter(Mandatory, ParameterSetName = 'Legacy')][string] $TemplateManagerSubject, [Parameter(Mandatory, ParameterSetName = 'Legacy')][scriptblock] $TemplateSecurity, [Parameter(Mandatory, ParameterSetName = 'Legacy')][string] $TemplateSecuritySubject, [Parameter(Mandatory, ParameterSetName = 'Legacy')][scriptblock] $TemplateManagerNotCompliant, [Parameter(Mandatory, ParameterSetName = 'Legacy')][string] $TemplateManagerNotCompliantSubject, [Parameter(Mandatory, ParameterSetName = 'Legacy')][scriptblock] $TemplateAdmin, [Parameter(Mandatory, ParameterSetName = 'Legacy')][string] $TemplateAdminSubject, [Parameter(ParameterSetName = 'Legacy')][System.Collections.IDictionary] $Logging = @{}, [Parameter(ParameterSetName = 'Legacy')][Array] $HTMLReports, [Parameter(ParameterSetName = 'Legacy')][string] $SearchPath, [Parameter(ParameterSetName = 'Legacy')][string[]] $FilterOrganizationalUnit ) $TimeStart = Start-TimeLog $Script:Reporting = [ordered] @{} $Script:Reporting['Version'] = Get-GitHubVersion -Cmdlet 'Start-PasswordSolution' -RepositoryOwner 'evotecit' -RepositoryName 'PasswordSolution' Write-Color -Text '[i]', "[PasswordSolution] ", 'Version', ' [Informative] ', $Script:Reporting['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta $TodayDate = Get-Date $Today = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $Summary = [ordered] @{} $Summary['Notify'] = [ordered] @{} $Summary['NotifyManager'] = [ordered] @{} $Summary['NotifySecurity'] = [ordered] @{} $Summary['Rules'] = [ordered] @{} $AllSkipped = [ordered] @{} $Locations = [ordered] @{} $SplatPasswordConfiguration = [ordered] @{ ConfigurationDSL = $ConfigurationDSL EmailParameters = $EmailParameters OverwriteEmailProperty = $OverwriteEmailProperty OverwriteManagerProperty = $OverwriteManagerProperty UserSection = $UserSection ManagerSection = $ManagerSection SecuritySection = $SecuritySection AdminSection = $AdminSection Rules = $Rules TemplatePreExpiry = $TemplatePreExpiry TemplatePreExpirySubject = $TemplatePreExpirySubject TemplatePostExpiry = $TemplatePostExpiry TemplatePostExpirySubject = $TemplatePostExpirySubject TemplateManager = $TemplateManager TemplateManagerSubject = $TemplateManagerSubject TemplateSecurity = $TemplateSecurity TemplateSecuritySubject = $TemplateSecuritySubject TemplateManagerNotCompliant = $TemplateManagerNotCompliant TemplateManagerNotCompliantSubject = $TemplateManagerNotCompliantSubject TemplateAdmin = $TemplateAdmin TemplateAdminSubject = $TemplateAdminSubject Logging = $Logging HTMLReports = $HTMLReports SearchPath = $SearchPath UsersExternalSystem = $UsersExternalSystem FilterOrganizationalUnit = $FilterOrganizationalUnit } $InitialVariables = Set-PasswordConfiguration @SplatPasswordConfiguration if (-not $InitialVariables) { return } $EmailParameters = $InitialVariables.EmailParameters $OverwriteEmailProperty = $InitialVariables.OverwriteEmailProperty $OverwriteManagerProperty = $InitialVariables.OverwriteManagerProperty $UserSection = $InitialVariables.UserSection $ManagerSection = $InitialVariables.ManagerSection $SecuritySection = $InitialVariables.SecuritySection $AdminSection = $InitialVariables.AdminSection $Rules = $InitialVariables.Rules $TemplatePreExpiry = $InitialVariables.TemplatePreExpiry $TemplatePreExpirySubject = $InitialVariables.TemplatePreExpirySubject $TemplatePostExpiry = $InitialVariables.TemplatePostExpiry $TemplatePostExpirySubject = $InitialVariables.TemplatePostExpirySubject $TemplateManager = $InitialVariables.TemplateManager $TemplateManagerSubject = $InitialVariables.TemplateManagerSubject $TemplateSecurity = $InitialVariables.TemplateSecurity $TemplateSecuritySubject = $InitialVariables.TemplateSecuritySubject $TemplateManagerNotCompliant = $InitialVariables.TemplateManagerNotCompliant $TemplateManagerNotCompliantSubject = $InitialVariables.TemplateManagerNotCompliantSubject $TemplateAdmin = $InitialVariables.TemplateAdmin $TemplateAdminSubject = $InitialVariables.TemplateAdminSubject $Logging = $InitialVariables.Logging $HTMLReports = $InitialVariables.HTMLReports $SearchPath = $InitialVariables.SearchPath $UsersExternalSystem = $InitialVariables.UsersExternalSystem $FilterOrganizationalUnit = $InitialVariables.FilterOrganizationalUnit Set-LoggingCapabilities -LogPath $Logging.LogFile -LogMaximum $Logging.LogMaximum -ShowTime:$Logging.ShowTime -TimeFormat $Logging.TimeFormat Write-Color -Text '[i]', "[PasswordSolution] ", 'Version', ' [Informative] ', $Script:Reporting['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta -NoConsoleOutput [Array] $ExtendedProperties = foreach ($Rule in $Rules ) { if ($Rule.OverwriteEmailProperty) { $Rule.OverwriteEmailProperty } if ($Rule.OverwriteManagerProperty) { $Rule.OverwriteManagerProperty } } $SummarySearch = Import-SearchInformation -SearchPath $SearchPath Write-Color -Text "[i]", " Starting process to find expiring users" -Color Yellow, White, Green, White, Green, White, Green, White $ExternalSystemReplacements = [ordered] @{} $GlobalManagerCache = [ordered] @{} $CachedUsers = Find-Password -AsHashTable -OverwriteEmailProperty $OverwriteEmailProperty -RulesProperties $ExtendedProperties -OverwriteManagerProperty $OverwriteManagerProperty -UsersExternalSystem $UsersExternalSystem -ExternalSystemReplacements $ExternalSystemReplacements -FilterOrganizationalUnit $FilterOrganizationalUnit -CacheManager $GlobalManagerCache Write-Color -Text "[i]", " Found ", $CachedUsers.Count, " users to be processed by Password Rules according to filtering settings" -Color Yellow, White, Green, White, Green, White, Green, White if ($Rules.Count -eq 0) { Write-Color -Text "[e]", " No rules found. Please add some rules to configuration" -Color Yellow, White, Red return } foreach ($Rule in $Rules) { $SplatProcessingRule = [ordered] @{ Rule = $Rule Summary = $Summary CachedUsers = $CachedUsers AllSkipped = $AllSkipped Locations = $Locations Loggin = $Logging TodayDate = $TodayDate UsersExternalSystem = $UsersExternalSystem } Invoke-PasswordRuleProcessing @SplatProcessingRule } $SplatUserNotifications = [ordered] @{ UserSection = $UserSection Summary = $Summary Logging = $Logging TemplatePreExpiry = $TemplatePreExpiry TemplatePreExpirySubject = $TemplatePreExpirySubject TemplatePostExpiry = $TemplatePostExpiry TemplatePostExpirySubject = $TemplatePostExpirySubject EmailParameter = $EmailParameters } [Array] $SummaryUsersEmails = Send-PasswordUserNofifications @SplatUserNotifications $SplatManagerNotifications = [ordered] @{ ManagerSection = $ManagerSection Summary = $Summary CachedUsers = $CachedUsers TemplateManager = $TemplateManager TemplateManagerSubject = $TemplateManagerSubject TemplateManagerNotCompliant = $TemplateManagerNotCompliant TemplateManagerNotCompliantSubject = $TemplateManagerNotCompliantSubject EmailParameters = $EmailParameters Loggin = $Logging GlobalManagersCache = $GlobalManagerCache } [Array] $SummaryManagersEmails = Send-PasswordManagerNofifications @SplatManagerNotifications $SplatSecurityNotifications = [ordered] @{ SecuritySection = $SecuritySection Summary = $Summary TemplateSecurity = $TemplateSecurity TemplateSecuritySubject = $TemplateSecuritySubject Logging = $Logging } [Array] $SummaryEscalationEmails = Send-PasswordSecurityNotifications @SplatSecurityNotifications $TimeEnd = Stop-TimeLog -Time $TimeStart -Option OneLiner Export-SearchInformation -SearchPath $SearchPath -SummarySearch $SummarySearch -Today $Today -SummaryUsersEmails $SummaryUsersEmails -SummaryManagersEmails $SummaryManagersEmails -SummaryEscalationEmails $SummaryEscalationEmails $HtmlAttachments = [System.Collections.Generic.List[string]]::new() foreach ($Report in $HTMLReports) { if ($Report.Enable) { $ReportSettings = @{ Report = $Report EmailParameters = $EmailParameters Logging = $Logging SearchPath = $SearchPath Rules = $Rules UserSection = $UserSection ManagerSection = $ManagerSection SecuritySection = $SecuritySection AdminSection = $AdminSection CachedUsers = $CachedUsers Summary = $Summary SummaryUsersEmails = $SummaryUsersEmails SummaryManagersEmails = $SummaryManagersEmails SummaryEscalationEmails = $SummaryEscalationEmails SummarySearch = $SummarySearch Locations = $Locations AllSkipped = $AllSkipped ExternalSystemReplacements = $ExternalSystemReplacements } New-HTMLReport @ReportSettings if ($Report.AttachToEmail) { if (Test-Path -LiteralPath $Report.FilePath) { $HtmlAttachments.Add($Report.FilePath) } else { Write-Color -Text "[w] HTML report ", $Report.FilePath, " does not exist! Probably a temporary path was used. " -Color DarkYellow, Red, DarkYellow } } } } $AdminSplat = [ordered] @{ AdminSection = $AdminSection TemplateAdmin = $TemplateAdmin TemplateAdminSubject = $TemplateAdminSubject TimeEnd = $TimeEnd EmailParameters = $EmailParameters HtmlAttachment = $HtmlAttachments } Send-PasswordAdminNotifications @AdminSplat } Export-ModuleMember -Function @('Find-Password', 'Find-PasswordNotification', 'Find-PasswordQuality', 'New-PasswordConfigurationEmail', 'New-PasswordConfigurationExternalUsers', 'New-PasswordConfigurationOption', 'New-PasswordConfigurationReport', 'New-PasswordConfigurationRule', 'New-PasswordConfigurationRuleReminder', 'New-PasswordConfigurationTemplate', 'New-PasswordConfigurationType', 'Show-PasswordQuality', 'Start-PasswordSolution') -Alias @() # SIG # Begin signature block # MIItsQYJKoZIhvcNAQcCoIItojCCLZ4CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAQMVPR7sZXunES # ARhIy3ZX+3tg+ueVps6YbXmrkNPsdqCCJrQwggWNMIIEdaADAgECAhAOmxiO+dAt # 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa # Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD # ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC # ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E # MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy # unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF # xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1 # 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB # MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR # WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6 # nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB # YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S # UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x # q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB # NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP # TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC # AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp # Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0 # aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB # LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc # Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov # Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy # oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW # juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF # mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z # twGpn1eqXijiuZQwggWQMIIDeKADAgECAhAFmxtXno4hMuI5B72nd3VcMA0GCSqG # SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx # GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy # dXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH # NDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvkXUo8MCIw # aTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdtHauyefLK # EdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu34LzB4Tm # dDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0QF+xembu # d8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2kZhAkHnD # eMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM1LyuGwN1 # XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmIdph2PVld # QnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZK37AlLTS # YW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72gLD76GSm # M9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqsX40/ybzT # QRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyhHsXAj6Kx # fgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD # VR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzANBgkq # hkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNkaA9Wz3eucPn9mkqZucl4 # XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjSPMFDQK4dUPVS/JA7u5iZ # aWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK7VB6fWIhCoDIc2bRoAVg # X+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eBcg3AFDLvMFkuruBx8lbk # apdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp5aPNoiBB19GcZNnqJqGL # FNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msgdDDS4Dk0EIUhFQEI6FUy # 3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vriRbgjU2wGb2dVf0a1TD9u # KFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ79ARj6e/CVABRoIoqyc54 # zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5nLGbsQAe79APT0JsyQq8 # 7kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3i0objwG2J5VT6LaJbVu8 # aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0HEEcRrYc9B9F1vM/zZn4w # ggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH # NDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVT # MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1 # c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqG # SIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbS # g9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9 # /UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXn # HwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0 # VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4f # sbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40Nj # gHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0 # QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvv # mz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T # /jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk # 42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5r # mQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E # FgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5n # P+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcG # CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu # Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln # aUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8v # Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNV # HSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIB # AH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxp # wc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIl # zpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQ # cAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfe # Kuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+j # Sbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJsh # IUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6 # OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDw # N7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR # 81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2 # VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIGsDCCBJigAwIBAgIQ # CK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQGEwJVUzEV # MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t # MSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjEwNDI5MDAw # MDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln # aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT # aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjANBgkqhkiG9w0BAQEF # AAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zrPYGXcMW7xIUmMJ+k # jmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHMgQM+TXAkZLON4gh9 # NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8IrgnQnAZaf6mIBJNYc9 # URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyCEUhSaN4QvRRXXegY # E2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0p6MDDnSlrzm2q2AS # 4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQakhCBj7A7CdfHmzJa # wv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0XLyTRSiDNipmKF+w # c86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960IHnWmZcy740hQ83eR # Gv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2FKZbS110YU0/EpF2 # 3r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBHX8mBUHOFECMhWWCK # ZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q27IwyCQLMbDwMVhEC # AwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFGg34Ou2 # O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9P # MA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzB3BggrBgEFBQcB # AQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggr # BgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1 # c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGln # aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwHAYDVR0gBBUwEzAH # BgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIBADojRD2NCHbuj7w6 # mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6jfCbVN7w6XUhtldU/ # SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmImoqKwba9oUgYftzY # gBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtfJqGVWEjVGv7XJz/9 # kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrxoj7bQ7gzyE84FJKZ # 9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3LIU/Gs4m6Ri+kAew # Q3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx4b6cpwoG1iZnt5Lm # Tl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9Oj9FpsToFpFSi0HA # SIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+ICw2/O/TOHnuO77Xr # y7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug0wcCampAMEhLNKhR # ILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5Vzu0nAPthkX0tGFu # v2jiJmCG6sivqf6UHedjGzqGVnhOMIIGwjCCBKqgAwIBAgIQBUSv85SdCDmmv9s/ # X+VhFjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln # aUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5 # NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIzMDcxNDAwMDAwMFoXDTM0MTAx # MzIzNTk1OVowSDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu # MSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMzCCAiIwDQYJKoZIhvcN # AQEBBQADggIPADCCAgoCggIBAKNTRYcdg45brD5UsyPgz5/X5dLnXaEOCdwvSKOX # ejsqnGfcYhVYwamTEafNqrJq3RApih5iY2nTWJw1cb86l+uUUI8cIOrHmjsvlmbj # aedp/lvD1isgHMGXlLSlUIHyz8sHpjBoyoNC2vx/CSSUpIIa2mq62DvKXd4ZGIX7 # ReoNYWyd/nFexAaaPPDFLnkPG2ZS48jWPl/aQ9OE9dDH9kgtXkV1lnX+3RChG4PB # uOZSlbVH13gpOWvgeFmX40QrStWVzu8IF+qCZE3/I+PKhu60pCFkcOvV5aDaY7Mu # 6QXuqvYk9R28mxyyt1/f8O52fTGZZUdVnUokL6wrl76f5P17cz4y7lI0+9S769Sg # LDSb495uZBkHNwGRDxy1Uc2qTGaDiGhiu7xBG3gZbeTZD+BYQfvYsSzhUa+0rRUG # FOpiCBPTaR58ZE2dD9/O0V6MqqtQFcmzyrzXxDtoRKOlO0L9c33u3Qr/eTQQfqZc # ClhMAD6FaXXHg2TWdc2PEnZWpST618RrIbroHzSYLzrqawGw9/sqhux7UjipmAmh # cbJsca8+uG+W1eEQE/5hRwqM/vC2x9XH3mwk8L9CgsqgcT2ckpMEtGlwJw1Pt7U2 # 0clfCKRwo+wK8REuZODLIivK8SgTIUlRfgZm0zu++uuRONhRB8qUt+JQofM604qD # y0B7AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAW # BgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglg # hkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0O # BBYEFKW27xPn783QZKHVVqllMaPe1eNJMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6 # Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEy # NTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUF # BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6 # Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZT # SEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAIEa1t6g # qbWYF7xwjU+KPGic2CX/yyzkzepdIpLsjCICqbjPgKjZ5+PF7SaCinEvGN1Ott5s # 1+FgnCvt7T1IjrhrunxdvcJhN2hJd6PrkKoS1yeF844ektrCQDifXcigLiV4JZ0q # BXqEKZi2V3mP2yZWK7Dzp703DNiYdk9WuVLCtp04qYHnbUFcjGnRuSvExnvPnPp4 # 4pMadqJpddNQ5EQSviANnqlE0PjlSXcIWiHFtM+YlRpUurm8wWkZus8W8oM3NG6w # QSbd3lqXTzON1I13fXVFoaVYJmoDRd7ZULVQjK9WvUzF4UbFKNOt50MAcN7MmJ4Z # iQPq1JE3701S88lgIcRWR+3aEUuMMsOI5ljitts++V+wQtaP4xeR0arAVeOGv6wn # LEHQmjNKqDbUuXKWfpd5OEhfysLcPTLfddY2Z1qJ+Panx+VPNTwAvb6cKmx5Adza # ROY63jg7B145WPR8czFVoIARyxQMfq68/qTreWWqaNYiyjvrmoI1VygWy2nyMpqy # 0tg6uLFGhmu6F/3Ed2wVbK6rr3M66ElGt9V/zLY4wNjsHPW2obhDLN9OTH0eaHDA # dwrUAuBcYLso/zjlUlrWrBciI0707NMX+1Br/wd3H3GXREHJuEbTbDJ8WC9nR2Xl # G3O2mflrLAZG70Ee8PBf4NvZrZCARK+AEEGKMIIHXzCCBUegAwIBAgIQB8JSdCgU # otar/iTqF+XdLjANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UE # ChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQg # Q29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMB4XDTIzMDQxNjAw # MDAwMFoXDTI2MDcwNjIzNTk1OVowZzELMAkGA1UEBhMCUEwxEjAQBgNVBAcMCU1p # a2/FgsOzdzEhMB8GA1UECgwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMSEwHwYD # VQQDDBhQcnplbXlzxYJhdyBLxYJ5cyBFVk9URUMwggIiMA0GCSqGSIb3DQEBAQUA # A4ICDwAwggIKAoICAQCUmgeXMQtIaKaSkKvbAt8GFZJ1ywOH8SwxlTus4McyrWmV # OrRBVRQA8ApF9FaeobwmkZxvkxQTFLHKm+8knwomEUslca8CqSOI0YwELv5EwTVE # h0C/Daehvxo6tkmNPF9/SP1KC3c0l1vO+M7vdNVGKQIQrhxq7EG0iezBZOAiukNd # GVXRYOLn47V3qL5PwG/ou2alJ/vifIDad81qFb+QkUh02Jo24SMjWdKDytdrMXi0 # 235CN4RrW+8gjfRJ+fKKjgMImbuceCsi9Iv1a66bUc9anAemObT4mF5U/yQBgAuA # o3+jVB8wiUd87kUQO0zJCF8vq2YrVOz8OJmMX8ggIsEEUZ3CZKD0hVc3dm7cWSAw # 8/FNzGNPlAaIxzXX9qeD0EgaCLRkItA3t3eQW+IAXyS/9ZnnpFUoDvQGbK+Q4/bP # 0ib98XLfQpxVGRu0cCV0Ng77DIkRF+IyR1PcwVAq+OzVU3vKeo25v/rntiXCmCxi # W4oHYO28eSQ/eIAcnii+3uKDNZrI15P7VxDrkUIc6FtiSvOhwc3AzY+vEfivUkFK # RqwvSSr4fCrrkk7z2Qe72Zwlw2EDRVHyy0fUVGO9QMuh6E3RwnJL96ip0alcmhKA # BGoIqSW05nXdCUbkXmhPCTT5naQDuZ1UkAXbZPShKjbPwzdXP2b8I9nQ89VSgQID # AQABo4ICAzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYD # VR0OBBYEFHrxaiVZuDJxxEk15bLoMuFI5233MA4GA1UdDwEB/wQEAwIHgDATBgNV # HSUEDDAKBggrBgEFBQcDAzCBtQYDVR0fBIGtMIGqMFOgUaBPhk1odHRwOi8vY3Js # My5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQw # OTZTSEEzODQyMDIxQ0ExLmNybDBToFGgT4ZNaHR0cDovL2NybDQuZGlnaWNlcnQu # Y29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAy # MUNBMS5jcmwwPgYDVR0gBDcwNTAzBgZngQwBBAEwKTAnBggrBgEFBQcCARYbaHR0 # cDovL3d3dy5kaWdpY2VydC5jb20vQ1BTMIGUBggrBgEFBQcBAQSBhzCBhDAkBggr # BgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBo # dHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2Rl # U2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqG # SIb3DQEBCwUAA4ICAQC3EeHXUPhpe31K2DL43Hfh6qkvBHyR1RlD9lVIklcRCR50 # ZHzoWs6EBlTFyohvkpclVCuRdQW33tS6vtKPOucpDDv4wsA+6zkJYI8fHouW6Tqa # 1W47YSrc5AOShIcJ9+NpNbKNGih3doSlcio2mUKCX5I/ZrzJBkQpJ0kYha/pUST2 # CbE3JroJf2vQWGUiI+J3LdiPNHmhO1l+zaQkSxv0cVDETMfQGZKKRVESZ6Fg61b0 # djvQSx510MdbxtKMjvS3ZtAytqnQHk1ipP+Rg+M5lFHrSkUlnpGa+f3nuQhxDb7N # 9E8hUVevxALTrFifg8zhslVRH5/Df/CxlMKXC7op30/AyQsOQxHW1uNx3tG1DMgi # zpwBasrxh6wa7iaA+Lp07q1I92eLhrYbtw3xC2vNIGdMdN7nd76yMIjdYnAn7r38 # wwtaJ3KYD0QTl77EB8u/5cCs3ShZdDdyg4K7NoJl8iEHrbqtooAHOMLiJpiL2i9Y # n8kQMB6/Q6RMO3IUPLuycB9o6DNiwQHf6Jt5oW7P09k5NxxBEmksxwNbmZvNQ65Z # n3exUAKqG+x31Egz5IZ4U/jPzRalElEIpS0rgrVg8R8pEOhd95mEzp5WERKFyXhe # 6nB6bSYHv8clLAV0iMku308rpfjMiQkqS3LLzfUJ5OHqtKKQNMLxz9z185UCszGC # BlMwggZPAgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ # bmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBS # U0E0MDk2IFNIQTM4NCAyMDIxIENBMQIQB8JSdCgUotar/iTqF+XdLjANBglghkgB # ZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJ # AzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8G # CSqGSIb3DQEJBDEiBCBJ5PsOEHNE0dy2V9hL4BKpPdqyrWJsryg/q4L1ygMTuzAN # BgkqhkiG9w0BAQEFAASCAgAAZ37tDzm59ialyYU2evAHITva2sSGrcaYUuTm8GqR # 6afuA261xWoE4EvA1AJU5Bj9rL/ABfYwX73GqcnJ5EkaREAjKVnSbxWsPPOyIUcE # ppUDCuP0xXC7p11n7wsEfiIgJBJ/bXFc9qkox/M3U0OU2kvu61oJGuy4aatMBHD/ # omKevsKuai7sNHG6oqqNuVHKJiBvV0bjUWyJKvOkJ53mR08pU6LcCP85aXUBHzJW # 4BpceOrFnr4eo+mDREeGrucQZiD+ttMW+uHlg4KV/7wPa5wsVpclXCDuTjs2fSQB # j8Wv4oanA0BpWd5+Q405gN8SGC/y9gwVFHgCMn00Ag99qphhBMIX47/ePjIdZ+2O # z+LemouoNji3VWaOHagBsYOJZTrBGdRCZCxGcYs/yNDgM5mYcNIwim8Y8RtM14gt # IQjr3Mqv83+qgBwCl/rAswMFzNUKHbaz42XhMXMQSG3kO0cbfe2dnPJf2RMdx0p2 # Uvjgw2jdUL5hUq1rRu30QzVm0E3WmpemYRwQGfEmfOPaZruxi6SlUmZdRf7SB+US # elXn9kLd6bcSRVYLmK8AXoFUeuMjqfx9V5+bh+UlUkqO8XqLxE6p/syCe6qAMaT0 # iT3c+wmLDBgCwmLAUXX3O3bWPhivGca3kQAlDOLWFC1NiB9kvd03xLJrE8BMHSNN # /6GCAyAwggMcBgkqhkiG9w0BCQYxggMNMIIDCQIBATB3MGMxCzAJBgNVBAYTAlVT # MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1 # c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0ECEAVEr/OUnQg5 # pr/bP1/lYRYwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcN # AQcBMBwGCSqGSIb3DQEJBTEPFw0yNDA4MjMxMDI3NTlaMC8GCSqGSIb3DQEJBDEi # BCAylf3lYRaS+ADx8hGwS0jNRPMcO3YQQVIx+T3XOxnr7jANBgkqhkiG9w0BAQEF # AASCAgBIc3CaSSG0QlceizeLqBS08lHuyTLBMGXoVXH7ZqEvyJMjrMlwK9A4+KpY # 1KEzBDujRq4/Ds74dc9+wyUsI3lsLjcwnPcV0fE1uAsgk4mBa1vCljwljLLHCP6t # BgU//bLlccLsQFjcays2SANFFUcN7JLBdv2KKFbF7DzBLk55B0s9ngVQBSnNd1CU # IxijTS1rw0gfdVAD6bputu32/vbkiFIz/uzEhFJQ4BGYIvGmBF2eo99RXD7NMJht # RhTPst+67/wFslEpY00vJApmdQRV7Xl2sfi3EO2ZF5E5T6+reTEYPIvs3uj2rytE # SaBLXKVj9WhOaY1e7NbOw3SU+Sni7GA4JzUOvSyJh1Uwy722aXKIlTUzjF4XIRGw # 2TeVdThg2D48wH+5ksb4r9Z/j1GfaAiohOt8do+FBPbOMA+JQ6phbRclkb6PRoyN # 50KJDKURmBsLRGd9X/PNI+jLyihiXuh5Ws4hvmGyhGKWtvpQarSc/0eCXiKlR6lx # dsjrUfwGxGMnQB6Ca1x5REBfGd8kGPVuLXmIQAx3u8X7suHbvOV+SoiYhyKBo8JR # CWVFsTBXkbJLaqL82JMnndISsqiYsy7CNpwq0wmhizOPKgVS1yAPYo7fBnJMBuOx # hPEQiCDRH/OA/oOfehYRzfwI7+WtMJXSQ2O1Qcics1VLBnaDog== # SIG # End signature block |