PasswordSolution.psm1
function ConvertFrom-DistinguishedName { <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER DistinguishedName Parameter description .PARAMETER ToOrganizationalUnit Parameter description .PARAMETER ToDC Parameter description .PARAMETER ToDomainCN Parameter description .EXAMPLE $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName -ToOrganizationalUnit Output: OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz .EXAMPLE $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName Output: Przemyslaw Klys .NOTES General notes #> [CmdletBinding()] param([alias('Identity', 'DN')][Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)][string[]] $DistinguishedName, [switch] $ToOrganizationalUnit, [switch] $ToDC, [switch] $ToDomainCN) process { foreach ($Distinguished in $DistinguishedName) { if ($ToDomainCN) { $DN = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1' $CN = $DN -replace ',DC=', '.' -replace "DC=" $CN } elseif ($ToOrganizationalUnit) { [Regex]::Match($Distinguished, '(?=OU=)(.*\n?)(?<=.)').Value } elseif ($ToDC) { $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1' } else { $Regex = '^CN=(?<cn>.+?)(?<!\\),(?<ou>(?:(?:OU|CN).+?(?<!\\),)+(?<dc>DC.+?))$' $Output = foreach ($_ in $Distinguished) { $_ -match $Regex $Matches } $Output.cn } } } } function Convert-UserAccountControl { [cmdletBinding()] param([alias('UAC')][int] $UserAccountControl, [string] $Separator) $UserAccount = [ordered] @{"SCRIPT" = 1 "ACCOUNTDISABLE" = 2 "HOMEDIR_REQUIRED" = 8 "LOCKOUT" = 16 "PASSWD_NOTREQD" = 32 "ENCRYPTED_TEXT_PWD_ALLOWED" = 128 "TEMP_DUPLICATE_ACCOUNT" = 256 "NORMAL_ACCOUNT" = 512 "INTERDOMAIN_TRUST_ACCOUNT" = 2048 "WORKSTATION_TRUST_ACCOUNT" = 4096 "SERVER_TRUST_ACCOUNT" = 8192 "DONT_EXPIRE_PASSWORD" = 65536 "MNS_LOGON_ACCOUNT" = 131072 "SMARTCARD_REQUIRED" = 262144 "TRUSTED_FOR_DELEGATION" = 524288 "NOT_DELEGATED" = 1048576 "USE_DES_KEY_ONLY" = 2097152 "DONT_REQ_PREAUTH" = 4194304 "PASSWORD_EXPIRED" = 8388608 "TRUSTED_TO_AUTH_FOR_DELEGATION" = 16777216 "PARTIAL_SECRETS_ACCOUNT" = 67108864 } $Output = foreach ($_ in $UserAccount.Keys) { $binaryAnd = $UserAccount[$_] -band $UserAccountControl if ($binaryAnd -ne "0") { $_ } } if ($Separator) { $Output -join $Separator } else { $Output } } function Get-GitHubLatestRelease { [CmdLetBinding()] param([alias('ReleasesUrl')][uri] $Url) $ProgressPreference = 'SilentlyContinue' 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 } } $ProgressPreference = 'Continue' } function Get-WinADForestDetails { [CmdletBinding()] param([alias('ForestName')][string] $Forest, [string[]] $ExcludeDomains, [string[]] $ExcludeDomainControllers, [alias('Domain', 'Domains')][string[]] $IncludeDomains, [alias('DomainControllers', 'ComputerName')][string[]] $IncludeDomainControllers, [switch] $SkipRODC, [string] $Filter = '*', [switch] $TestAvailability, [ValidateSet('All', 'Ping', 'WinRM', 'PortOpen', 'Ping+WinRM', 'Ping+PortOpen', 'WinRM+PortOpen')] $Test = 'All', [int[]] $Ports = 135, [int] $PortsTimeout = 100, [int] $PingCount = 1, [switch] $Extended, [System.Collections.IDictionary] $ExtendedForestInformation) if ($Global:ProgressPreference -ne 'SilentlyContinue') { $TemporaryProgress = $Global:ProgressPreference $Global:ProgressPreference = 'SilentlyContinue' } if (-not $ExtendedForestInformation) { $Findings = [ordered] @{} try { if ($Forest) { $ForestInformation = Get-ADForest -ErrorAction Stop -Identity $Forest } else { $ForestInformation = Get-ADForest -ErrorAction Stop } } catch { Write-Warning "Get-WinADForestDetails - Error discovering DC for Forest - $($_.Exception.Message)" return } if (-not $ForestInformation) { return } $Findings['Forest'] = $ForestInformation $Findings['ForestDomainControllers'] = @() $Findings['QueryServers'] = @{} $Findings['DomainDomainControllers'] = @{} [Array] $Findings['Domains'] = foreach ($_ in $ForestInformation.Domains) { if ($IncludeDomains) { if ($_ -in $IncludeDomains) { $_.ToLower() } continue } if ($_ -notin $ExcludeDomains) { $_.ToLower() } } foreach ($Domain in $ForestInformation.Domains) { try { $DC = Get-ADDomainController -DomainName $Domain -Discover -ErrorAction Stop $OrderedDC = [ordered] @{Domain = $DC.Domain Forest = $DC.Forest HostName = [Array] $DC.HostName IPv4Address = $DC.IPv4Address IPv6Address = $DC.IPv6Address Name = $DC.Name Site = $DC.Site } } catch { Write-Warning "Get-WinADForestDetails - Error discovering DC for domain $Domain - $($_.Exception.Message)" continue } if ($Domain -eq $Findings['Forest']['Name']) { $Findings['QueryServers']['Forest'] = $OrderedDC } $Findings['QueryServers']["$Domain"] = $OrderedDC } [Array] $Findings['ForestDomainControllers'] = foreach ($Domain in $Findings.Domains) { $QueryServer = $Findings['QueryServers'][$Domain]['HostName'][0] [Array] $AllDC = try { try { $DomainControllers = Get-ADDomainController -Filter $Filter -Server $QueryServer -ErrorAction Stop } catch { Write-Warning "Get-WinADForestDetails - Error listing DCs for domain $Domain - $($_.Exception.Message)" continue } foreach ($S in $DomainControllers) { if ($IncludeDomainControllers.Count -gt 0) { If (-not $IncludeDomainControllers[0].Contains('.')) { if ($S.Name -notin $IncludeDomainControllers) { continue } } else { if ($S.HostName -notin $IncludeDomainControllers) { continue } } } if ($ExcludeDomainControllers.Count -gt 0) { If (-not $ExcludeDomainControllers[0].Contains('.')) { if ($S.Name -in $ExcludeDomainControllers) { continue } } else { if ($S.HostName -in $ExcludeDomainControllers) { continue } } } $Server = [ordered] @{Domain = $Domain HostName = $S.HostName Name = $S.Name Forest = $ForestInformation.RootDomain Site = $S.Site IPV4Address = $S.IPV4Address IPV6Address = $S.IPV6Address IsGlobalCatalog = $S.IsGlobalCatalog IsReadOnly = $S.IsReadOnly IsSchemaMaster = ($S.OperationMasterRoles -contains 'SchemaMaster') IsDomainNamingMaster = ($S.OperationMasterRoles -contains 'DomainNamingMaster') IsPDC = ($S.OperationMasterRoles -contains 'PDCEmulator') IsRIDMaster = ($S.OperationMasterRoles -contains 'RIDMaster') IsInfrastructureMaster = ($S.OperationMasterRoles -contains 'InfrastructureMaster') OperatingSystem = $S.OperatingSystem OperatingSystemVersion = $S.OperatingSystemVersion OperatingSystemLong = ConvertTo-OperatingSystem -OperatingSystem $S.OperatingSystem -OperatingSystemVersion $S.OperatingSystemVersion LdapPort = $S.LdapPort SslPort = $S.SslPort DistinguishedName = $S.ComputerObjectDN Pingable = $null WinRM = $null PortOpen = $null Comment = '' } if ($TestAvailability) { if ($Test -eq 'All' -or $Test -like 'Ping*') { $Server.Pingable = Test-Connection -ComputerName $Server.IPV4Address -Quiet -Count $PingCount } if ($Test -eq 'All' -or $Test -like '*WinRM*') { $Server.WinRM = (Test-WinRM -ComputerName $Server.HostName).Status } if ($Test -eq 'All' -or '*PortOpen*') { $Server.PortOpen = (Test-ComputerPort -Server $Server.HostName -PortTCP $Ports -Timeout $PortsTimeout).Status } } [PSCustomObject] $Server } } catch { [PSCustomObject]@{Domain = $Domain HostName = '' Name = '' Forest = $ForestInformation.RootDomain IPV4Address = '' IPV6Address = '' IsGlobalCatalog = '' IsReadOnly = '' Site = '' SchemaMaster = $false DomainNamingMasterMaster = $false PDCEmulator = $false RIDMaster = $false InfrastructureMaster = $false LdapPort = '' SslPort = '' DistinguishedName = '' Pingable = $null WinRM = $null PortOpen = $null Comment = $_.Exception.Message -replace "`n", " " -replace "`r", " " } } if ($SkipRODC) { [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC | Where-Object { $_.IsReadOnly -eq $false } } else { [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC } [Array] $Findings['DomainDomainControllers'][$Domain] } if ($Extended) { $Findings['DomainsExtended'] = @{} $Findings['DomainsExtendedNetBIOS'] = @{} foreach ($DomainEx in $Findings['Domains']) { try { $Findings['DomainsExtended'][$DomainEx] = Get-ADDomain -Server $Findings['QueryServers'][$DomainEx].HostName[0] | ForEach-Object { [ordered] @{AllowedDNSSuffixes = $_.AllowedDNSSuffixes | ForEach-Object -Process { $_ } ChildDomains = $_.ChildDomains | ForEach-Object -Process { $_ } ComputersContainer = $_.ComputersContainer DeletedObjectsContainer = $_.DeletedObjectsContainer DistinguishedName = $_.DistinguishedName DNSRoot = $_.DNSRoot DomainControllersContainer = $_.DomainControllersContainer DomainMode = $_.DomainMode DomainSID = $_.DomainSID.Value ForeignSecurityPrincipalsContainer = $_.ForeignSecurityPrincipalsContainer Forest = $_.Forest InfrastructureMaster = $_.InfrastructureMaster LastLogonReplicationInterval = $_.LastLogonReplicationInterval LinkedGroupPolicyObjects = $_.LinkedGroupPolicyObjects | ForEach-Object -Process { $_ } LostAndFoundContainer = $_.LostAndFoundContainer ManagedBy = $_.ManagedBy Name = $_.Name NetBIOSName = $_.NetBIOSName ObjectClass = $_.ObjectClass ObjectGUID = $_.ObjectGUID ParentDomain = $_.ParentDomain PDCEmulator = $_.PDCEmulator PublicKeyRequiredPasswordRolling = $_.PublicKeyRequiredPasswordRolling | ForEach-Object -Process { $_ } QuotasContainer = $_.QuotasContainer ReadOnlyReplicaDirectoryServers = $_.ReadOnlyReplicaDirectoryServers | ForEach-Object -Process { $_ } ReplicaDirectoryServers = $_.ReplicaDirectoryServers | ForEach-Object -Process { $_ } RIDMaster = $_.RIDMaster SubordinateReferences = $_.SubordinateReferences | ForEach-Object -Process { $_ } SystemsContainer = $_.SystemsContainer UsersContainer = $_.UsersContainer } } $NetBios = $Findings['DomainsExtended'][$DomainEx]['NetBIOSName'] $Findings['DomainsExtendedNetBIOS'][$NetBios] = $Findings['DomainsExtended'][$DomainEx] } catch { Write-Warning "Get-WinADForestDetails - Error gathering Domain Information for domain $DomainEx - $($_.Exception.Message)" continue } } } if ($TemporaryProgress) { $Global:ProgressPreference = $TemporaryProgress } $Findings } else { $Findings = Copy-DictionaryManual -Dictionary $ExtendedForestInformation [Array] $Findings['Domains'] = foreach ($_ in $Findings.Domains) { if ($IncludeDomains) { if ($_ -in $IncludeDomains) { $_.ToLower() } continue } if ($_ -notin $ExcludeDomains) { $_.ToLower() } } foreach ($_ in [string[]] $Findings.DomainDomainControllers.Keys) { if ($_ -notin $Findings.Domains) { $Findings.DomainDomainControllers.Remove($_) } } foreach ($_ in [string[]] $Findings.DomainsExtended.Keys) { if ($_ -notin $Findings.Domains) { $Findings.DomainsExtended.Remove($_) $NetBiosName = $Findings.DomainsExtended.$_.'NetBIOSName' if ($NetBiosName) { $Findings.DomainsExtendedNetBIOS.Remove($NetBiosName) } } } [Array] $Findings['ForestDomainControllers'] = foreach ($Domain in $Findings.Domains) { [Array] $AllDC = foreach ($S in $Findings.DomainDomainControllers["$Domain"]) { if ($IncludeDomainControllers.Count -gt 0) { If (-not $IncludeDomainControllers[0].Contains('.')) { if ($S.Name -notin $IncludeDomainControllers) { continue } } else { if ($S.HostName -notin $IncludeDomainControllers) { continue } } } if ($ExcludeDomainControllers.Count -gt 0) { If (-not $ExcludeDomainControllers[0].Contains('.')) { if ($S.Name -in $ExcludeDomainControllers) { continue } } else { if ($S.HostName -in $ExcludeDomainControllers) { continue } } } $S } if ($SkipRODC) { [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC | Where-Object { $_.IsReadOnly -eq $false } } else { [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC } [Array] $Findings['DomainDomainControllers'][$Domain] } $Findings } } function Write-Color { <# .SYNOPSIS Write-Color is a wrapper around Write-Host. It provides: - Easy manipulation of colors, - Logging output to file (log) - Nice formatting options out of the box. .DESCRIPTION Author: przemyslaw.klys at evotec.pl Project website: https://evotec.xyz/hub/scripts/write-color-ps1/ Project support: https://github.com/EvotecIT/PSWriteColor Original idea: Josh (https://stackoverflow.com/users/81769/josh) .EXAMPLE Write-Color -Text "Red ", "Green ", "Yellow " -Color Red,Green,Yellow .EXAMPLE Write-Color -Text "This is text in Green ", "followed by red ", "and then we have Magenta... ", "isn't it fun? ", "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan .EXAMPLE Write-Color -Text "This is text in Green ", "followed by red ", "and then we have Magenta... ", "isn't it fun? ", "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan -StartTab 3 -LinesBefore 1 -LinesAfter 1 .EXAMPLE Write-Color "1. ", "Option 1" -Color Yellow, Green Write-Color "2. ", "Option 2" -Color Yellow, Green Write-Color "3. ", "Option 3" -Color Yellow, Green Write-Color "4. ", "Option 4" -Color Yellow, Green Write-Color "9. ", "Press 9 to exit" -Color Yellow, Gray -LinesBefore 1 .EXAMPLE Write-Color -LinesBefore 2 -Text "This little ","message is ", "written to log ", "file as well." ` -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" -TimeFormat "yyyy-MM-dd HH:mm:ss" Write-Color -Text "This can get ","handy if ", "want to display things, and log actions to file ", "at the same time." ` -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" .EXAMPLE # Added in 0.5 Write-Color -T "My text", " is ", "all colorful" -C Yellow, Red, Green -B Green, Green, Yellow wc -t "my text" -c yellow -b green wc -text "my text" -c red .NOTES Additional Notes: - TimeFormat https://msdn.microsoft.com/en-us/library/8kb3ddd4.aspx #> [alias('Write-Colour')] [CmdletBinding()] param ([alias ('T')] [String[]]$Text, [alias ('C', 'ForegroundColor', 'FGC')] [ConsoleColor[]]$Color = [ConsoleColor]::White, [alias ('B', 'BGC')] [ConsoleColor[]]$BackGroundColor = $null, [alias ('Indent')][int] $StartTab = 0, [int] $LinesBefore = 0, [int] $LinesAfter = 0, [int] $StartSpaces = 0, [alias ('L')] [string] $LogFile = '', [Alias('DateFormat', 'TimeFormat')][string] $DateTimeFormat = 'yyyy-MM-dd HH:mm:ss', [alias ('LogTimeStamp')][bool] $LogTime = $true, [int] $LogRetry = 2, [ValidateSet('unknown', 'string', 'unicode', 'bigendianunicode', 'utf8', 'utf7', 'utf32', 'ascii', 'default', 'oem')][string]$Encoding = 'Unicode', [switch] $ShowTime, [switch] $NoNewLine) $DefaultColor = $Color[0] if ($null -ne $BackGroundColor -and $BackGroundColor.Count -ne $Color.Count) { Write-Error "Colors, BackGroundColors parameters count doesn't match. Terminated." return } if ($LinesBefore -ne 0) { for ($i = 0; $i -lt $LinesBefore; $i++) { Write-Host -Object "`n" -NoNewline } } if ($StartTab -ne 0) { for ($i = 0; $i -lt $StartTab; $i++) { Write-Host -Object "`t" -NoNewline } } if ($StartSpaces -ne 0) { for ($i = 0; $i -lt $StartSpaces; $i++) { Write-Host -Object ' ' -NoNewline } } if ($ShowTime) { Write-Host -Object "[$([datetime]::Now.ToString($DateTimeFormat))] " -NoNewline } if ($Text.Count -ne 0) { if ($Color.Count -ge $Text.Count) { if ($null -eq $BackGroundColor) { for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline } } else { for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline } } } else { if ($null -eq $BackGroundColor) { for ($i = 0; $i -lt $Color.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline } for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -NoNewline } } else { for ($i = 0; $i -lt $Color.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline } for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -BackgroundColor $BackGroundColor[0] -NoNewline } } } } if ($NoNewLine -eq $true) { Write-Host -NoNewline } else { Write-Host } if ($LinesAfter -ne 0) { for ($i = 0; $i -lt $LinesAfter; $i++) { Write-Host -Object "`n" -NoNewline } } if ($Text.Count -and $LogFile) { $TextToFile = "" for ($i = 0; $i -lt $Text.Length; $i++) { $TextToFile += $Text[$i] } $Saved = $false $Retry = 0 Do { $Retry++ try { if ($LogTime) { "[$([datetime]::Now.ToString($DateTimeFormat))] $TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false } else { "$TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false } $Saved = $true } catch { if ($Saved -eq $false -and $Retry -eq $LogRetry) { $PSCmdlet.WriteError($_) } else { Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Retrying... ($Retry/$LogRetry)" } } } Until ($Saved -eq $true -or $Retry -ge $LogRetry) } } function ConvertTo-OperatingSystem { <# .SYNOPSIS Allows easy conversion of OperatingSystem, Operating System Version to proper Windows 10 naming based on WMI or AD .DESCRIPTION Allows easy conversion of OperatingSystem, Operating System Version to proper Windows 10 naming based on WMI or AD .PARAMETER OperatingSystem Operating System as returned by Active Directory .PARAMETER OperatingSystemVersion Operating System Version as returned by Active Directory .EXAMPLE $Computers = Get-ADComputer -Filter * -Properties OperatingSystem, OperatingSystemVersion | ForEach-Object { $OPS = ConvertTo-OperatingSystem -OperatingSystem $_.OperatingSystem -OperatingSystemVersion $_.OperatingSystemVersion Add-Member -MemberType NoteProperty -Name 'OperatingSystemTranslated' -Value $OPS -InputObject $_ -Force $_ } $Computers | Select-Object DNS*, Name, SamAccountName, Enabled, OperatingSystem*, DistinguishedName | Format-Table .NOTES General notes #> [CmdletBinding()] param([string] $OperatingSystem, [string] $OperatingSystemVersion) if ($OperatingSystem -like '*Windows 10*') { $Systems = @{'10.0 (19043)' = 'Windows 10 21H1' '10.0 (19042)' = 'Windows 10 20H2' '10.0 (19041)' = 'Windows 10 2004' '10.0 (18363)' = "Windows 10 1909" '10.0 (18362)' = "Windows 10 1903" '10.0 (17763)' = "Windows 10 1809" '10.0 (17134)' = "Windows 10 1803" '10.0 (16299)' = "Windows 10 1709" '10.0 (15063)' = "Windows 10 1703" '10.0 (14393)' = "Windows 10 1607" '10.0 (10586)' = "Windows 10 1511" '10.0 (10240)' = "Windows 10 1507" '10.0 (18898)' = 'Windows 10 Insider Preview' '10.0.19043' = 'Windows 10 21H1' '10.0.19042' = 'Windows 10 20H2' '10.0.19041' = 'Windows 10 2004' '10.0.18363' = "Windows 10 1909" '10.0.18362' = "Windows 10 1903" '10.0.17763' = "Windows 10 1809" '10.0.17134' = "Windows 10 1803" '10.0.16299' = "Windows 10 1709" '10.0.15063' = "Windows 10 1703" '10.0.14393' = "Windows 10 1607" '10.0.10586' = "Windows 10 1511" '10.0.10240' = "Windows 10 1507" '10.0.18898' = 'Windows 10 Insider Preview' } $System = $Systems[$OperatingSystemVersion] if (-not $System) { $System = $OperatingSystem } } elseif ($OperatingSystem -like '*Windows Server*') { $Systems = @{'5.2 (3790)' = 'Windows Server 2003' '6.1 (7601)' = 'Windows Server 2008 R2' '10.0 (18362)' = "Windows Server, version 1903 (Semi-Annual Channel) 1903" '10.0 (17763)' = "Windows Server 2019 (Long-Term Servicing Channel) 1809" '10.0 (17134)' = "Windows Server, version 1803 (Semi-Annual Channel) 1803" '10.0 (14393)' = "Windows Server 2016 (Long-Term Servicing Channel) 1607" '10.0.18362' = "Windows Server, version 1903 (Semi-Annual Channel) 1903" '10.0.17763' = "Windows Server 2019 (Long-Term Servicing Channel) 1809" '10.0.17134' = "Windows Server, version 1803 (Semi-Annual Channel) 1803" '10.0.14393' = "Windows Server 2016 (Long-Term Servicing Channel) 1607" } $System = $Systems[$OperatingSystemVersion] if (-not $System) { $System = $OperatingSystem } } else { $System = $OperatingSystem } if ($System) { $System } else { 'Unknown' } } function Copy-DictionaryManual { [CmdletBinding()] param([System.Collections.IDictionary] $Dictionary) $clone = @{} foreach ($Key in $Dictionary.Keys) { $value = $Dictionary.$Key $clonedValue = switch ($Dictionary.$Key) { { $null -eq $_ } { $null continue } { $_ -is [System.Collections.IDictionary] } { Copy-DictionaryManual -Dictionary $_ continue } { $type = $_.GetType() $type.IsPrimitive -or $type.IsValueType -or $_ -is [string] } { $_ continue } default { $_ | Select-Object -Property * } } if ($value -is [System.Collections.IList]) { $clone[$Key] = @($clonedValue) } else { $clone[$Key] = $clonedValue } } $clone } function Test-ComputerPort { [CmdletBinding()] param ([alias('Server')][string[]] $ComputerName, [int[]] $PortTCP, [int[]] $PortUDP, [int]$Timeout = 5000) begin { if ($Global:ProgressPreference -ne 'SilentlyContinue') { $TemporaryProgress = $Global:ProgressPreference $Global:ProgressPreference = 'SilentlyContinue' } } process { foreach ($Computer in $ComputerName) { foreach ($P in $PortTCP) { $Output = [ordered] @{'ComputerName' = $Computer 'Port' = $P 'Protocol' = 'TCP' 'Status' = $null 'Summary' = $null 'Response' = $null } $TcpClient = Test-NetConnection -ComputerName $Computer -Port $P -InformationLevel Detailed -WarningAction SilentlyContinue if ($TcpClient.TcpTestSucceeded) { $Output['Status'] = $TcpClient.TcpTestSucceeded $Output['Summary'] = "TCP $P Successful" } else { $Output['Status'] = $false $Output['Summary'] = "TCP $P Failed" $Output['Response'] = $Warnings } [PSCustomObject]$Output } foreach ($P in $PortUDP) { $Output = [ordered] @{'ComputerName' = $Computer 'Port' = $P 'Protocol' = 'UDP' 'Status' = $null 'Summary' = $null } $UdpClient = [System.Net.Sockets.UdpClient]::new($Computer, $P) $UdpClient.Client.ReceiveTimeout = $Timeout $Encoding = [System.Text.ASCIIEncoding]::new() $byte = $Encoding.GetBytes("Evotec") [void]$UdpClient.Send($byte, $byte.length) $RemoteEndpoint = [System.Net.IPEndPoint]::new([System.Net.IPAddress]::Any, 0) try { $Bytes = $UdpClient.Receive([ref]$RemoteEndpoint) [string]$Data = $Encoding.GetString($Bytes) If ($Data) { $Output['Status'] = $true $Output['Summary'] = "UDP $P Successful" $Output['Response'] = $Data } } catch { $Output['Status'] = $false $Output['Summary'] = "UDP $P Failed" $Output['Response'] = $_.Exception.Message } $UdpClient.Close() $UdpClient.Dispose() [PSCustomObject]$Output } } } end { if ($TemporaryProgress) { $Global:ProgressPreference = $TemporaryProgress } } } function Test-WinRM { [CmdletBinding()] param ([alias('Server')][string[]] $ComputerName) $Output = foreach ($Computer in $ComputerName) { $Test = [PSCustomObject] @{Output = $null Status = $null ComputerName = $Computer } try { $Test.Output = Test-WSMan -ComputerName $Computer -ErrorAction Stop $Test.Status = $true } catch { $Test.Status = $false } $Test } $Output } function Add-ManagerInformation { [CmdletBinding()] param( [System.Collections.IDictionary] $SummaryDictionary, [string] $Type, [string] $ManagerType, [Object] $Key, [PSCustomObject] $User, [PSCustomObject] $Rule # [bool] $Enabled ) #if ($Enabled) { if ($Key) { if ($Key -is [string]) { $KeyDN = $Key } else { $KeyDN = $Key.EmailAddress } if (-not $SummaryDictionary[$KeyDN]) { $SummaryDictionary[$KeyDN] = [ordered] @{ Manager = $Key ManagerDefault = [ordered] @{} ManagerNotCompliant = [ordered] @{} 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 Get-GitHubVersion { [cmdletBinding()] param( [Parameter(Mandatory)][string] $Cmdlet, [Parameter(Mandatory)][string] $RepositoryOwner, [Parameter(Mandatory)][string] $RepositoryName ) $App = Get-Command -Name $Cmdlet -ErrorAction SilentlyContinue if ($App) { [Array] $GitHubReleases = (Get-GitHubLatestRelease -Url "https://api.github.com/repos/$RepositoryOwner/$RepositoryName/releases" -Verbose:$false) $LatestVersion = $GitHubReleases[0] if (-not $LatestVersion.Errors) { if ($App.Version -eq $LatestVersion.Version) { "Current/Latest: $($LatestVersion.Version) at $($LatestVersion.PublishDate)" } elseif ($App.Version -lt $LatestVersion.Version) { "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Update?" } elseif ($App.Version -gt $LatestVersion.Version) { "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Lucky you!" } } else { "Current: $($App.Version)" } } } function Find-Password { [CmdletBinding()] param( [alias('ForestName')][string] $Forest, [string[]] $ExcludeDomains, [alias('Domain', 'Domains')][string[]] $IncludeDomains, [System.Collections.IDictionary] $ExtendedForestInformation, [string] $OverwriteEmailProperty, #[Array] $ConditionProperties, #[System.Collections.IDictionary] $CachedUsers, #[System.Collections.IDictionary] $CachedUsersPrepared, #[System.Collections.IDictionary] $CachedManagers [switch] $AsHashTable ) $Today = Get-Date $Properties = @( 'Manager', 'DisplayName', 'GivenName', 'Surname', 'SamAccountName', 'EmailAddress', 'msDS-UserPasswordExpiryTimeComputed', 'PasswordExpired', 'PasswordLastSet', 'PasswordNotRequired', 'Enabled', 'PasswordNeverExpires', 'Mail', 'MemberOf', 'LastLogonDate', 'Name' 'userAccountControl' if ($OverwriteEmailProperty) { $OverwriteEmailProperty } #if ($ConditionProperties) { # $ConditionProperties #} ) # We're caching all users to make sure it's speedy gonzales when querying for Managers if (-not $CachedUsers) { $CachedUsers = [ordered] @{ } } if (-not $Cache) { $Cache = [ordered] @{ } } Write-Color -Text "[i] Discovering forest information" -Color White, Yellow, White, Yellow, White, Yellow, White $ForestInformation = Get-WinADForestDetails -Forest $Forest -ExcludeDomains $ExcludeDomains -IncludeDomains $IncludeDomains -ExtendedForestInformation $ExtendedForestInformation [Array] $Users = foreach ($Domain in $ForestInformation.Domains) { Write-Color -Text "[i] Discovering DC for domain ", "$($Domain)", " in forest ", $ForestInformation.Name -Color White, Yellow, White, Yellow, White, Yellow, White $Server = $ForestInformation['QueryServers'][$Domain]['HostName'][0] Write-Color -Text "[i] Getting users from ", "$($Domain)", " using ", $Server -Color White, Yellow, White, Yellow, White, Yellow, White # We query all users instead of using filter. Since we need manager field and manager data this way it should be faster (query once - get it all) #$DomainUsers = Get-ADUser -Server $Server -Filter '*' -Properties $Properties -ErrorAction Stop #foreach ($_ in $DomainUsers) { #$CachedUsers["$($_.DistinguishedName)"] = $_ # We reuse filtering, account is enabled, password is required and password is not set to change on next logon #if ($_.Enabled -eq $true -and $_.PasswordNotRequired -ne $true -and $null -ne $_.PasswordLastSet) { # $_ #} #} try { Get-ADUser -Server $Server -Filter '*' -Properties $Properties -ErrorAction Stop } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Color '[e] Error: ', $ErrorMessage -Color White, Red } } foreach ($User in $Users) { $Cache[$User.DistinguishedName] = $User } Write-Color -Text "[i] Preparing all users for password expirations in forest ", $Forest.Name -Color White, Yellow, White, Yellow, White, Yellow, White $ProcessedUsers = foreach ($User in $Users) { #$UserManager = $Cache["$($User.Manager)"] if ($User.Manager) { $Manager = $Cache[$User.Manager].DisplayName $ManagerSamAccountName = $Cache[$User.Manager].SamAccountName $ManagerEmail = $Cache[$User.Manager].Mail $ManagerEnabled = $Cache[$User.Manager].Enabled $ManagerLastLogon = $Cache[$User.Manager].LastLogonDate if ($ManagerLastLogon) { $ManagerLastLogonDays = $( - $($ManagerLastLogon - $Today).Days) } else { $ManagerLastLogonDays = $null } $ManagerStatus = if ($ManagerEnabled) { 'Enabled' } else { 'Disabled' } } else { if ($User.ObjectClass -eq 'user') { $ManagerStatus = 'Missing' } else { $ManagerStatus = 'Not available' } $Manager = $null $ManagerSamAccountName = $null $ManagerEmail = $null $ManagerEnabled = $null $ManagerLastLogon = $null $ManagerLastLogonDays = $null } if ($OverwriteEmailProperty) { # fix this for a user $EmailTemp = $User.$OverwriteEmailProperty if ($EmailTemp -like '*@*') { $EmailAddress = $EmailTemp } else { $EmailAddress = $User.EmailAddress } # Fix this for manager as well if ($Cache["$($User.Manager)"]) { if ($Cache["$($User.Manager)"].$OverwriteEmailProperty -like '*@*') { # $UserManager.Mail = $UserManager.$OverwriteEmailProperty $ManagerEmail = $Cache["$($User.Manager)"].$OverwriteEmailProperty } } } else { $EmailAddress = $User.EmailAddress } if ($User.PasswordLastSet) { $PasswordDays = (New-TimeSpan -Start ($User.PasswordLastSet) -End ($Today)).Days } if ($User."msDS-UserPasswordExpiryTimeComputed" -ne 9223372036854775807) { # This is standard situation where users password is expiring as needed try { $DateExpiry = ([datetime]::FromFileTime($User."msDS-UserPasswordExpiryTimeComputed")) } catch { $DateExpiry = $User."msDS-UserPasswordExpiryTimeComputed" } try { $DaysToExpire = (New-TimeSpan -Start ($Today) -End ([datetime]::FromFileTime($User."msDS-UserPasswordExpiryTimeComputed"))).Days } catch { $DaysToExpire = $null } $PasswordNeverExpires = $User.PasswordNeverExpires } else { # This is non-standard situation. This basically means most likely Fine Grained Group Policy is in action where it makes PasswordNeverExpires $true # Since FGP policies are a bit special they do not tick the PasswordNeverExpires box, but at the same time value for "msDS-UserPasswordExpiryTimeComputed" is set to 9223372036854775807 $PasswordNeverExpires = $true } if ($PasswordNeverExpires -or $null -eq $User.PasswordLastSet) { $DateExpiry = $null $DaysToExpire = $null } $UserAccountControl = Convert-UserAccountControl -UserAccountControl $User.UserAccountControl if ($UserAccountControl -contains 'INTERDOMAIN_TRUST_ACCOUNT') { continue } $MyUser = [ordered] @{ UserPrincipalName = $User.UserPrincipalName SamAccountName = $User.SamAccountName Domain = ConvertFrom-DistinguishedName -DistinguishedName $User.DistinguishedName -ToDomainCN Enabled = $User.Enabled EmailAddress = $EmailAddress DateExpiry = $DateExpiry DaysToExpire = $DaysToExpire PasswordExpired = $User.PasswordExpired PasswordDays = $PasswordDays PasswordLastSet = $User.PasswordLastSet PasswordNotRequired = $User.PasswordNotRequired PasswordNeverExpires = $PasswordNeverExpires #PasswordAtNextLogon = $null -eq $User.PasswordLastSet Manager = $Manager ManagerSamAccountName = $ManagerSamAccountName ManagerEmail = $ManagerEmail ManagerStatus = $ManagerStatus ManagerLastLogonDays = $ManagerLastLogonDays DisplayName = $User.DisplayName 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 } foreach ($Property in $ConditionProperties) { $MyUser["$Property"] = $User.$Property } #[PSCustomObject] $MyUser $CachedUsers["$($User.DistinguishedName)"] = [PSCustomObject] $MyUser } if ($AsHashTable) { $CachedUsers } else { $CachedUsers.Values } } function Find-PasswordNotification { [CmdletBinding()] param( [Parameter(Mandatory)][string] $SearchPath, [switch] $Manager ) if ($SearchPath) { if (Test-Path -LiteralPath $SearchPath) { try { $SummarySearch = Import-Clixml -LiteralPath $SearchPath -ErrorAction Stop #$SummarySearch = Get-Content -LiteralPath $SearchPath -Raw | ConvertFrom-Json } catch { Write-Color -Text "[e]", " Couldn't load the file $SearchPath", ". Skipping...", $_.Exception.Message -Color White, Yellow, White, Yellow, White, Yellow, White } if ($SummarySearch -and $Manager) { $SummarySearch.EmailEscalations.Values } elseif ($SummarySearch -and $Manager -eq $false) { $SummarySearch.EmailSent.Values } } } } function Send-PasswordEmail { [CmdletBinding()] param( [scriptblock] $Template, [PSCustomObject] $User, [Array] $ManagedUsers, [Array] $ManagedUsersManagerNotCompliant, #[Array] $ManagedUsersManagerDisabled, #[Array] $ManagedUsersManagerMissing, #[Array] $ManagedUsersManagerMissingEmail, [System.Collections.IDictionary] $EmailParameters, [string] $Subject ) if ($Template) { $SourceParameters = [ordered] @{ ManagerDisplayName = $User.DisplayName ManagerUsersTable = $ManagedUsers ManagerUsersTableManagerNotCompliant = $ManagedUsersManagerNotCompliant #ManagerUsersTableManagerDisabled = $ManagedUsersManagerDisabled #ManagerUsersTableManagerMissing = $ManagedUsersManagerMissing #ManagerUsersTableManagerNoEmail = $ManagedUsersManagerMissingEmail UserPrincipalName = $User.UserPrincipalName # : adm.pklys@ad.evotec.xyz SamAccountName = $User.SamAccountName # : adm.pklys Domain = $User.Domain # : ad.evotec.xyz Enabled = $User.Enabled EmailAddress = $User.EmailAddress # : DateExpiry = $User.DateExpiry # : DaysToExpire = $User.DaysToExpire # : PasswordExpired = $User.PasswordExpired # : False PasswordLastSet = $User.PasswordLastSet # : 05.09.2020 11:07:29 PasswordNotRequired = $User.PasswordNotRequired # : False PasswordNeverExpires = $User.PasswordNeverExpires # : True ManagerSamAccountName = $User.ManagerSamAccountName # : przemyslaw.klys ManagerEmail = $User.ManagerEmail # : przemyslaw.klys@evotec.pl ManagerStatus = $User.ManagerStatus # : Enabled ManagerLastLogonDays = $User.ManagerLastLogonDays # : 0 Manager = $User.Manager # : Przemysław Kłys DisplayName = $User.DisplayName # : Administrator Przemysław Kłys GivenName = $User.GivenName # : Administrator Przemysław Surname = $User.Surname # : Kłys OrganizationalUnit = $User.OrganizationalUnit # : OU=Special,OU=Accounts,OU=Production,DC=ad,DC=evotec,DC=xyz MemberOf = $User.MemberOf # : {CN=GDS-TestGroup4,OU=Security,OU=Groups,OU=Production,DC=ad,DC=evotec,DC=xyz, CN=GDS-TestGroup2,OU=Security,OU=Groups,OU=Production,DC=ad,DC=evotec,DC=xyz, CN=Domain Admins,CN=Users,DC=ad,DC=evotec,DC=xyz} DistinguishedName = $User.DistinguishedName # : CN=Administrator Przemysław Kłys,OU=Special,OU=Accounts,OU=Production,DC=ad,DC=evotec,DC=xyz ManagerDN = $User.ManagerDN # : CN=Przemysław Kłys,OU=Users,OU=Accounts,OU=Production,DC=ad,DC=evotec,DC=xyz } $Body = EmailBody -EmailBody $Template -Parameter $SourceParameters # Below command would require to define variables as they are used in scriptblock #$EmailParameters.Subject = $ExecutionContext.InvokeCommand.ExpandString($Subject) # following replacement is a bit more cumbersome the the one above but a bit more secure and doesn't require creating 20+ unused variables $EmailParameters.Subject = Add-ParametersToString -String $Subject -Parameter $SourceParameters $EmailParameters.Body = $Body Send-EmailMessage @EmailParameters } } function Start-PasswordSolution { [CmdletBinding()] param( [Parameter(Mandatory)][System.Collections.IDictionary] $EmailParameters, [string] $OverwriteEmailProperty, [Parameter(Mandatory)][System.Collections.IDictionary] $UserSection, [Parameter(Mandatory)][System.Collections.IDictionary] $ManagerSection, [Parameter(Mandatory)][System.Collections.IDictionary] $SecuritySection, [Parameter(Mandatory)][System.Collections.IDictionary] $AdminSection, [Parameter(Mandatory)][Array] $Rules, [scriptblock] $TemplatePreExpiry, [string] $TemplatePreExpirySubject, [scriptblock] $TemplatePostExpiry, [string] $TemplatePostExpirySubject, [Parameter(Mandatory)][scriptblock] $TemplateManager, [Parameter(Mandatory)][string] $TemplateManagerSubject, [Parameter(Mandatory)][scriptblock] $TemplateSecurity, [Parameter(Mandatory)][string] $TemplateSecuritySubject, [Parameter(Mandatory)][scriptblock] $TemplateManagerNotCompliant, [Parameter(Mandatory)][string] $TemplateManagerNotCompliantSubject, [Parameter(Mandatory)][System.Collections.IDictionary] $Logging, [Parameter(Mandatory)][System.Collections.IDictionary] $HTMLOptions, [string] $FilePath, [string] $SearchPath ) $Script:Reporting = [ordered] @{} $Script:Reporting['Version'] = Get-GitHubVersion -Cmdlet 'Start-PasswordSolution' -RepositoryOwner 'evotecit' -RepositoryName 'PasswordSolution' $Today = Get-Date -Format "yyyy-MM-dd HH:mm:ss" if (-not $Logging) { $Logging = @{ ShowTime = $true LogFile = "" TimeFormat = "yyyy-MM-dd HH:mm:ss" } } $PSDefaultParameterValues = @{ "Write-Color:LogFile" = $Logging.LogFile "Write-Color:ShowTime" = $Logging.ShowTime "Write-Color:TimeFormat" = $Logging.TimeFormat } if ($Logging.LogFile) { $FolderPath = [io.path]::GetDirectoryName($Logging.LogFile) if (-not (Test-Path -LiteralPath $FolderPath)) { $null = New-Item -Path $FolderPath -ItemType Directory -Force } $CurrentLogs = Get-ChildItem -LiteralPath $FolderPath | Sort-Object -Property CreationTime -Descending | Select-Object -Skip $Logging.LogMaximum if ($CurrentLogs) { Write-Color -Text '[i] ', "Logs directory has more than ", $Logging.LogMaximum, " log files. Cleanup required..." -Color Yellow, DarkCyan, Red, DarkCyan foreach ($Log in $CurrentLogs) { try { Remove-Item -LiteralPath $Log.FullName -Confirm:$false Write-Color -Text '[+] ', "Deleted ", "$($Log.FullName)" -Color Yellow, White, Green } catch { Write-Color -Text '[-] ', "Couldn't delete log file $($Log.FullName). Error: ', "$($_.Exception.Message) -Color Yellow, White, Red } } } } if ($SearchPath) { if (Test-Path -LiteralPath $SearchPath) { try { Write-Color -Text "[i]", " Loading file ", $SearchPath -Color White, Yellow, White, Yellow, White, Yellow, White $SummarySearch = Import-Clixml -LiteralPath $SearchPath -ErrorAction Stop } catch { Write-Color -Text "[e]", " Couldn't load the file $SearchPath", ". Skipping...", $_.Exception.Message -Color White, Yellow, White, Yellow, White, Yellow, White } } } if (-not $SummarySearch) { $SummarySearch = [ordered] @{ EmailSent = [ordered] @{ } EmailManagers = [ordered] @{ } EmailEscalations = [ordered] @{ } } } $Summary = [ordered] @{} $Summary['Notify'] = [ordered] @{} $Summary['NotifyManager'] = [ordered] @{} $Summary['NotifySecurity'] = [ordered] @{} $Summary['Rules'] = [ordered] @{} Write-Color -Text "[i]", " Starting process to find expiring users" -Color Yellow, White, Green, White, Green, White, Green, White $CachedUsers = Find-Password -AsHashTable -OverwriteEmailProperty $OverwriteEmailProperty foreach ($Rule in $Rules) { # Go for each rule and check if the user is in any of those rules if ($Rule.Enable -eq $true) { Write-Color -Text "[i]", " Processing rule ", $Rule.Name, ' status: ', $Rule.Enable -Color Yellow, White, Green, White, Green, White, Green, White # Lets create summary for the rule if (-not $Summary['Rules'][$Rule.Name] ) { $Summary['Rules'][$Rule.Name] = [ordered] @{} } # this will make sure to expand array of multiple arrays of ints if provided # for example: (-150..-100),(-60..0), 1, 2, 3 $Rule.Reminders = $Rule.Reminders | ForEach-Object { $_ } foreach ($User in $CachedUsers.Values) { if ($User.Enabled -eq $false) { # We don't want to have disabled users continue } if ($Rule.ExcludeOU.Count -gt 0) { $FoundOU = $false foreach ($OU in $Rule.ExcludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } # if OU is found we need to exclude the user if ($FoundOU) { continue } } if ($Rule.IncludeOU.Count -gt 0) { # Rule defined that only user withi specific OU has to be found $FoundOU = $false foreach ($OU in $Rule.IncludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } if (-not $FoundOU) { continue } } if ($Rule.ExcludeGroup.Count -gt 0) { # Rule defined that only user withi specific group has to be found $FoundGroup = $false foreach ($Group in $Rule.ExcludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } # If found, we need to skip user if ($FoundGroup) { continue } } if ($Rule.IncludeGroup.Count -gt 0) { # Rule defined that only user withi specific group has to be found $FoundGroup = $false foreach ($Group in $Rule.IncludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } if (-not $FoundGroup) { continue } } if ($Rule.IncludeName.Count -gt 0) { $IncludeName = $false foreach ($Name in $Rule.IncludeName) { foreach ($Property in $Rule.IncludeNameProperties) { if ($User.$Property -like $Name) { $IncludeName = $true break } } if ($IncludeName) { break } } if (-not $IncludeName) { continue } } if ($Summary['Notify'][$User.DistinguishedName] -and $Summary['Notify'][$User.DistinguishedName].ProcessManagersOnly -ne $true) { # User already exists in the notifications - rules are overlapping, we only take the first one # We also check for ProcessManagersOnly because we don't want first rule to ignore any other rules for users continue } if ($Rule.IncludePasswordNeverExpires -and $Rule.IncludeExpiring) { if ($User.PasswordNeverExpire -eq $true) { $DaysToPasswordExpiry = $Rule.PasswordNeverExpiresDays - $User.PasswordDays $User.DaysToExpire = $DaysToPasswordExpiry } } elseif ($Rule.IncludeExpiring) { if ($User.PasswordNeverExpire -eq $true) { # we skip those that never expire continue } } elseif ($Rule.IncludePasswordNeverExpires) { if ($User.PasswordNeverExpire -eq $true) { $DaysToPasswordExpiry = $Rule.PasswordNeverExpiresDays - $User.PasswordDays $User.DaysToExpire = $DaysToPasswordExpiry } else { # we skip users who expire continue } } else { Write-Color -Text "[i]", " Processing rule ", $Rule.Name, " doesn't include IncludePasswordNeverExpires nor IncludeExpiring so skipping." -Color Yellow, White, Green, White, Green, White, Green, White continue } <# if ($Rule.IncludePasswordNeverExpires -eq $true -and $Rule.OnlyPasswordNeverExpiresOnly) { if ($User.PasswordNeverExpire -eq $true) { $DaysToPasswordExpiry = $Rule.PasswordNeverExpiresDays - $User.PasswordDays $User.DaysToExpire = $DaysToPasswordExpiry } } elseif ($null -eq $Rule.PasswordNeverExpires) { } #> # Lets find users that expire if ($User.DaysToExpire -in $Rule.Reminders) { if ($Logging.NotifyOnUserMatchingRule) { Write-Color -Text "[i]", " User ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire -Color Yellow, White, Yellow, White, Yellow, White, White, Blue } $Summary['Notify'][$User.DistinguishedName] = [ordered] @{ User = $User Rule = $Rule ProcessManagersOnly = $Rule.ProcessManagersOnly } $Summary['Rules'][$Rule.Name][$User.DistinguishedName] = [ordered] @{ User = $User Rule = $Rule ProcessManagersOnly = $Rule.ProcessManagersOnly } if ($Rule.SendToManager) { if ($Rule.SendToManager.Manager -and $Rule.SendToManager.Manager.Enable -eq $true -and $User.ManagerStatus -eq 'Enabled' -and $User.ManagerEmail -like "*@*") { # Manager is enabled and has an email, this is standard situation for manager in AD # But before we go and do that, maybe user wants to send emails to managers if those users are in specific group or OU if ($Rule.SendToManager.Manager.IncludeOU.Count -gt 0) { # Rule defined that only user withi specific OU has to be found $FoundOU = $false foreach ($OU in $Rule.SendToManager.Manager.IncludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } if (-not $FoundOU) { continue } } if ($Rule.SendToManager.Manager.ExcludeOU.Count -gt 0) { $FoundOU = $false foreach ($OU in $Rule.SendToManager.Manager.ExcludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } # if OU is found we need to exclude the user if ($FoundOU) { continue } } if ($Rule.SendToManager.Manager.ExcludeGroup.Count -gt 0) { # Rule defined that only user withi specific group has to be found $FoundGroup = $false foreach ($Group in $Rule.SendToManager.Manager.ExcludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } # if Group found, we need to skip this user if ($FoundGroup) { continue } } if ($Rule.SendToManager.Manager.IncludeGroup.Count -gt 0) { # Rule defined that only user withi specific group has to be found $FoundGroup = $false foreach ($Group in $Rule.SendToManager.Manager.IncludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } if (-not $FoundGroup) { continue } } $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifyManager'] Type = 'ManagerDefault' ManagerType = 'Ok' Key = $User.ManagerDN User = $User Rule = $Rule #Enabled = $true } Add-ManagerInformation @Splat } # Not compliant (missing, disabled, no email), covers all the below options if ($Rule.SendToManager.ManagerNotCompliant -and $Rule.SendToManager.ManagerNotCompliant.Enable -and $Rule.SendToManager.ManagerNotCompliant.Manager) { # But before we go and do that, maybe user wants to send emails to managers if those users are in specific group or OU if ($Rule.SendToManager.ManagerNotCompliant.IncludeOU.Count -gt 0) { # Rule defined that only user withi specific OU has to be found $FoundOU = $false foreach ($OU in $Rule.SendToManager.ManagerNotCompliant.IncludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } if (-not $FoundOU) { continue } } if ($Rule.SendToManager.ManagerNotCompliant.ExcludeOU.Count -gt 0) { $FoundOU = $false foreach ($OU in $Rule.SendToManager.ManagerNotCompliant.ExcludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } # if OU is found we need to exclude the user if ($FoundOU) { continue } } if ($Rule.SendToManager.ManagerNotCompliant.ExcludeGroup.Count -gt 0) { # Rule defined that only user withi specific group has to be found $FoundGroup = $false foreach ($Group in $Rule.SendToManager.ManagerNotCompliant.ExcludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } # if Group found, we need to skip this user if ($FoundGroup) { continue } } if ($Rule.SendToManager.ManagerNotCompliant.IncludeGroup.Count -gt 0) { # Rule defined that only user withi specific group has to be found $FoundGroup = $false foreach ($Group in $Rule.SendToManager.ManagerNotCompliant.IncludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } if (-not $FoundGroup) { continue } } if ($Rule.SendToManager.ManagerNotCompliant.MissingEmail -and $User.ManagerStatus -eq 'Enabled') { # Manager is enabled but missing email $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifyManager'] Type = 'ManagerNotCompliant' ManagerType = 'No email' Key = $Rule.SendToManager.ManagerNotCompliant.Manager User = $User Rule = $Rule } Add-ManagerInformation @Splat } elseif ($Rule.SendToManager.ManagerNotCompliant.Disabled -and $User.ManagerStatus -eq 'Disabled') { # Manager is disabled, regardless if he/she has email $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifyManager'] Type = 'ManagerNotCompliant' ManagerType = 'Manager disabled' Key = $Rule.SendToManager.ManagerNotCompliant.Manager User = $User Rule = $Rule } Add-ManagerInformation @Splat } elseif ($Rule.SendToManager.ManagerNotCompliant.LastLogon -and $User.ManagerLastLogonDays -ge $Rule.SendToManager.ManagerNotCompliant.LastLogonDays) { # Manager Last Logon over X days $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifyManager'] Type = 'ManagerNotCompliant' ManagerType = 'Manager not logging in' Key = $Rule.SendToManager.ManagerNotCompliant.Manager User = $User Rule = $Rule } Add-ManagerInformation @Splat } elseif ($Rule.SendToManager.ManagerNotCompliant.Missing -and $User.ManagerStatus -eq 'Missing') { # Manager is missing $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifyManager'] Type = 'ManagerNotCompliant' ManagerType = 'Manager not set' Key = $Rule.SendToManager.ManagerNotCompliant.Manager User = $User Rule = $Rule } Add-ManagerInformation @Splat } } } } if ($Rule.SendToManager) { if ($Rule.SendToManager.SecurityEscalation -and $Rule.SendToManager.SecurityEscalation.Enable -eq $true -and $Rule.SendToManager.SecurityEscalation.Manager) { $Rule.SendToManager.SecurityEscalation.Reminders = $Rule.SendToManager.SecurityEscalation.Reminders | ForEach-Object { $_ } if ($User.DaysToExpire -in $Rule.SendToManager.SecurityEscalation.Reminders) { if ($Rule.SendToManager.SecurityEscalation.IncludeOU.Count -gt 0) { # Rule defined that only user withi specific OU has to be found $FoundOU = $false foreach ($OU in $Rule.SendToManager.SecurityEscalation.IncludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } if (-not $FoundOU) { continue } } if ($Rule.SendToManager.SecurityEscalation.ExcludeOU.Count -gt 0) { $FoundOU = $false foreach ($OU in $Rule.SendToManager.SecurityEscalation.ExcludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } # if OU is found we need to exclude the user if ($FoundOU) { continue } } if ($Rule.SendToManager.SecurityEscalation.ExcludeGroup.Count -gt 0) { # Rule defined that only user withi specific group has to be found $FoundGroup = $false foreach ($Group in $Rule.SendToManager.SecurityEscalation.ExcludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } # if Group found, we need to skip this user if ($FoundGroup) { continue } } if ($Rule.SendToManager.SecurityEscalation.IncludeGroup.Count -gt 0) { # Rule defined that only user withi specific group has to be found $FoundGroup = $false foreach ($Group in $Rule.SendToManager.SecurityEscalation.IncludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } if (-not $FoundGroup) { continue } } $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifySecurity'] Type = 'Security' ManagerType = 'Escalation' Key = $Rule.SendToManager.SecurityEscalation.Manager User = $User Rule = $Rule } Add-ManagerInformation @Splat } } } } } else { Write-Color -Text "[i]", " Processing rule ", $Rule.Name, ' status: ', $Rule.Enable -Color Red, White, Red, White, Red, White, Red, White } } if ($UserSection.Enable) { Write-Color -Text "[i] Sending notifications to users " -Color White, Yellow, White, Yellow, White, Yellow, White $CountUsers = 0 [Array] $SummaryUsersEmails = foreach ($Notify in $Summary['Notify'].Values) { $CountUsers++ $User = $Notify.User $Rule = $Notify.Rule if ($Notify.ProcessManagersOnly -eq $true) { if ($Logging.NotifyOnSkipUserManagerOnly) { Write-Color -Text "[i]", " Skipping User (Manager Only - $($Rule.Name)) ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire -Color Yellow, White, Magenta, White, Magenta, White, White, Blue } continue } $EmailSplat = [ordered] @{} if ($Notify.User.DaysToExpire -ge 0) { if ($Notify.Rule.TemplatePreExpiry) { # User uses template per rule $EmailSplat.Template = $Notify.Rule.TemplatePreExpiry } elseif ($TemplatePreExpiry) { # User uses global template $EmailSplat.Template = $TemplatePreExpiry } else { # User uses built-in template $EmailSplat.Template = { } } if ($Notify.Rule.TemplatePreExpirySubject) { $EmailSplat.Subject = $Notify.Rule.TemplatePreExpirySubject } elseif ($TemplatePreExpirySubject) { $EmailSplat.Subject = $TemplatePreExpirySubject } else { $EmailSplat.Subject = '[Password] Your password will expire on $DateExpiry ($TimeToExpire days)' } } else { if ($Notify.Rule.TemplatePostExpiry) { $EmailSplat.Template = $Notify.Rule.TemplatePostExpiry } elseif ($TemplatePostExpiry) { $EmailSplat.Template = $TemplatePostExpiry } else { $EmailSplat.Template = { } } if ($Notify.Rule.TemplatePostExpirySubject) { $EmailSplat.Subject = $Notify.Rule.TemplatePostExpirySubject } elseif ($TemplatePostExpirySubject) { $EmailSplat.Subject = $TemplatePostExpirySubject } else { $EmailSplat.Subject = '[Password] Your password expired on $DateExpiry ($TimeToExpire days ago)' } } $EmailSplat.User = $Notify.User $EmailSplat.EmailParameters = $EmailParameters if ($UserSection.SendToDefaultEmail -ne $true) { $EmailSplat.EmailParameters.To = $Notify.User.EmailAddress } else { $EmailSplat.EmailParameters.To = $UserSection.DefaultEmail } if ($Notify.User.EmailAddress -like "*@*") { # Regardless if we send email to default email or to user, if user doesn't have email address we shouldn't send an email $EmailResult = Send-PasswordEmail @EmailSplat [PSCustomObject] @{ UserPrincipalName = $EmailSplat.User.UserPrincipalName SamAccountName = $EmailSplat.User.SamAccountName Domain = $EmailSplat.User.Domain Rule = $Notify.Rule.Name Status = $EmailResult.Status StatusWhen = Get-Date -Format "yyyy-MM-dd HH:mm:ss" StatusError = $EmailResult.Error SentTo = $EmailResult.SentTo DateExpiry = $EmailSplat.User.DateExpiry DaysToExpire = $EmailSplat.User.DaysToExpire PasswordExpired = $EmailSplat.User.PasswordExpired PasswordNeverExpires = $EmailSplat.User.PasswordNeverExpires PasswordLastSet = $EmailSplat.User.PasswordLastSet } } else { # Email not sent $EmailResult = @{ Status = $false Error = 'No email address for user' SentTo = '' } [PSCustomObject] @{ UserPrincipalName = $EmailSplat.User.UserPrincipalName SamAccountName = $EmailSplat.User.SamAccountName Domain = $EmailSplat.User.Domain Rule = $Notify.Rule.Name Status = $EmailResult.Status StatusWhen = Get-Date -Format "yyyy-MM-dd HH:mm:ss" StatusError = $EmailResult.Error SentTo = $EmailResult.SentTo DateExpiry = $EmailSplat.User.DateExpiry DaysToExpire = $EmailSplat.User.DaysToExpire PasswordExpired = $EmailSplat.User.PasswordExpired PasswordNeverExpires = $EmailSplat.User.PasswordNeverExpires PasswordLastSet = $EmailSplat.User.PasswordLastSet } } if ($Logging.NotifyOnUserSend) { Write-Color -Text "[i]", " Sending notifications to users ", $Notify.User.DisplayName, " (", $Notify.User.EmailAddress, ")", " status: ", $EmailResult.Status, " sent to: ", $EmailResult.SentTo, ", details: ", $EmailResult.Error -Color Yellow, White, Yellow, White, Yellow, White, White, Blue, White, Blue } if ($UserSection.SendCountMaximum -gt 0) { if ($UserSection.SendCountMaximum -le $CountUsers) { Write-Color -Text "[i]", " Send count maximum reached. There may be more accounts that match the rule." -Color Red, DarkMagenta break } } } Write-Color -Text "[i] Sending notifications to users (sent: ", $SummaryUsersEmails.Count, " out of ", $Summary['Notify'].Values.Count, ")" -Color White, Yellow, White, Yellow, White, Yellow, White } else { Write-Color -Text "[i] Sending notifications to users is ", "disabled!" -Color White, Yellow, DarkMagenta } if ($ManagerSection.Enable) { Write-Color -Text "[i] Sending notifications to managers " -Color White, Yellow, White, Yellow, White, Yellow, White $CountManagers = 0 [Array] $SummaryManagersEmails = foreach ($Manager in $Summary['NotifyManager'].Keys) { $CountManagers++ if ($CachedUsers[$Manager]) { # This user is "findable" in AD $ManagerUser = $CachedUsers[$Manager] } else { # This user is provided by user in config file $ManagerUser = $Summary['NotifyManager'][$Manager]['Manager'] } [Array] $ManagedUsers = $Summary['NotifyManager'][$Manager]['ManagerDefault'].Values.Output [Array] $ManagedUsersManagerNotCompliant = $Summary['NotifyManager'][$Manager]['ManagerNotCompliant'].Values.Output $EmailSplat = [ordered] @{} if ($Summary['NotifyManager'][$Manager].ManagerDefault.Count -gt 0) { if ($TemplateManager) { # User uses global template $EmailSplat.Template = $TemplateManager } else { # User uses built-in template $EmailSplat.Template = { } } if ($TemplateManagerSubject) { $EmailSplat.Subject = $TemplateManagerSubject } else { $EmailSplat.Subject = "[Password Expiring] Dear Manager - Your accounts are expiring!" } } elseif ($Summary['NotifyManager'][$Manager].ManagerNotCompliant.Count -gt 0) { if ($TemplateManagerNotCompliant) { # User uses global template $EmailSplat.Template = $TemplateManagerNotCompliant } else { # User uses built-in template $EmailSplat.Template = { } } if ($TemplateManagerNotCompliantSubject) { $EmailSplat.Subject = $TemplateManagerNotCompliantSubject } else { $EmailSplat.Subject = "[Password Escalation] Accounts are expiring with non-compliant manager" } } $EmailSplat.User = $ManagerUser $EmailSplat.ManagedUsers = $ManagedUsers $EmailSplat.ManagedUsersManagerNotCompliant = $ManagedUsersManagerNotCompliant $EmailSplat.EmailParameters = $EmailParameters if ($ManagerSection.SendToDefaultEmail -ne $true) { $EmailSplat.EmailParameters.To = $ManagerUser.EmailAddress } else { $EmailSplat.EmailParameters.To = $ManagerSection.DefaultEmail } if ($Logging.NotifyOnManagerSend) { Write-Color -Text "[i] Sending notifications to managers ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } $EmailResult = Send-PasswordEmail @EmailSplat if ($Logging.NotifyOnManagerSend) { Write-Color -Text "[r] Sending notifications to managers ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ") (status: ", $EmailResult.Status, " sent to: ", $EmailResult.SentTo, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } [PSCustomObject] @{ DisplayName = $ManagerUser.DisplayName SamAccountName = $ManagerUser.SamAccountName Domain = $ManagerUser.Domain Status = $EmailResult.Status StatusWhen = Get-Date -Format "yyyy-MM-dd HH:mm:ss" SentTo = $EmailResult.SentTo StatusError = $EmailResult.Error Accounts = $ManagedUsers.SamAccountName AccountsCount = $ManagedUsers.Count Template = 'Unknown' ManagerNotCompliant = $ManagedUsersManagerNotCompliant.SamAccountName ManagerNotCompliantCount = $ManagedUsersManagerNotCompliant.Count #ManagerDisabled = $ManagedUsersManagerDisabled.SamAccountName #ManagerDisabledCount = $ManagedUsersManagerDisabled.Count #ManagerMissing = $ManagedUsersManagerMissing.SamAccountName #ManagerMissingCount = $ManagedUsersManagerMissing.Count #ManagerMissingEmail = $ManagedUsersManagerMissingEmail.SamAccountName #ManagerMissingEmailCount = $ManagedUsersManagerMissingEmail.Count } if ($ManagerSection.SendCountMaximum -gt 0) { if ($ManagerSection.SendCountMaximum -le $CountManagers) { Write-Color -Text "[i]", " Send count maximum reached. There may be more managers that match the rule." -Color Red, DarkMagenta break } } } Write-Color -Text "[i] Sending notifications to managers (sent: ", $SummaryManagersEmails.Count, " out of ", $Summary['NotifyManager'].Values.Count, ")" -Color White, Yellow, White, Yellow, White, Yellow, White #Write-Color -Text "[i] Sending notifications to managers (sent: ", $SummaryManagersEmails.Count, ")" -Color White, Yellow, White, Yellow, White, Yellow, White } else { Write-Color -Text "[i] Sending notifications to managers is ", "disabled!" -Color White, Yellow, DarkMagenta } if ($SecuritySection.Enable) { Write-Color -Text "[i] Sending notifications to security " -Color White, Yellow, White, Yellow, White, Yellow, White $CountSecurity = 0 [Array] $SummaryEscalationEmails = foreach ($Manager in $Summary['NotifySecurity'].Keys) { $CountSecurity++ # This user is provided by user in config file $ManagerUser = $Summary['NotifySecurity'][$Manager]['Manager'] [Array] $ManagedUsers = $Summary['NotifySecurity'][$Manager]['Security'].Values.Output $EmailSplat = [ordered] @{} if ($Summary['NotifySecurity'][$Manager].Security.Count -gt 0) { # User uses global template $EmailSplat.Template = $TemplateSecurity if ($TemplateSecuritySubject) { $EmailSplat.Subject = $TemplateSecuritySubject } else { $EmailSplat.Subject = "[Password Expiring] Dear Security - Accounts expired" } } else { continue } $EmailSplat.User = $ManagerUser $EmailSplat.ManagedUsers = $ManagedUsers | Select-Object -Property 'Status', 'DisplayName', 'Enabled', 'SamAccountName', 'Domain', 'DateExpiry', 'DaysToExpire', 'PasswordLastSet', 'PasswordExpired' #$EmailSplat.ManagedUsersManagerNotCompliant = $ManagedUsersManagerNotCompliant #$EmailSplat.ManagedUsersManagerDisabled = $ManagedUsersManagerDisabled #$EmailSplat.ManagedUsersManagerMissing = $ManagedUsersManagerMissing #$EmailSplat.ManagedUsersManagerMissingEmail = $ManagedUsersManagerMissingEmail $EmailSplat.EmailParameters = $EmailParameters if ($ManagerSection.SendToDefaultEmail -ne $true) { $EmailSplat.EmailParameters.To = $ManagerUser.EmailAddress } else { $EmailSplat.EmailParameters.To = $SecuritySection.DefaultEmail } if ($Logging.NotifyOnSecuritySend) { Write-Color -Text "[i] Sending notifications to security ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } $EmailResult = Send-PasswordEmail @EmailSplat if ($Logging.NotifyOnSecuritySend) { Write-Color -Text "[r] Sending notifications to security ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ") (status: ", $EmailResult.Status, " sent to: ", $EmailResult.SentTo, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } [PSCustomObject] @{ DisplayName = $ManagerUser.DisplayName SamAccountName = $ManagerUser.SamAccountName Domain = $ManagerUser.Domain Status = $EmailResult.Status StatusWhen = Get-Date -Format "yyyy-MM-dd HH:mm:ss" SentTo = $EmailResult.SentTo StatusError = $EmailResult.Error Accounts = $ManagedUsers.SamAccountName AccountsCount = $ManagedUsers.Count Template = 'Unknown' # ManagerNotCompliant = $ManagedUsersManagerNotCompliant.SamAccountName # ManagerNotCompliantCount = $ManagedUsersManagerNotCompliant.Count #ManagerDisabled = $ManagedUsersManagerDisabled.SamAccountName #ManagerDisabledCount = $ManagedUsersManagerDisabled.Count #ManagerMissing = $ManagedUsersManagerMissing.SamAccountName #ManagerMissingCount = $ManagedUsersManagerMissing.Count #ManagerMissingEmail = $ManagedUsersManagerMissingEmail.SamAccountName #ManagerMissingEmailCount = $ManagedUsersManagerMissingEmail.Count } if ($ManagerSection.SendCountMaximum -gt 0) { if ($ManagerSection.SendCountMaximum -le $CountSecurity) { Write-Color -Text "[i]", " Send count maximum reached. There may be more managers that match the rule." -Color Red, DarkMagenta break } } } Write-Color -Text "[i] Sending notifications to security (sent: ", $SummaryEscalationEmails.Count, " out of ", $Summary['NotifySecurity'].Values.Count, ")" -Color White, Yellow, White, Yellow, White, Yellow, White } else { Write-Color -Text "[i] Sending notifications to security is ", "disabled!" -Color White, Yellow, DarkMagenta } if ($HTMLOptions.DisableWarnings -eq $true) { $WarningAction = 'SilentlyContinue' } else { $WarningAction = 'Continue' } # Create report New-HTML { New-HTMLHeader { New-HTMLSection -Invisible { New-HTMLSection { New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue } -JustifyContent flex-start -Invisible New-HTMLSection { New-HTMLText -Text "Password Solution - $($Script:Reporting['Version'])" -Color Blue } -JustifyContent flex-end -Invisible } } New-TableOption -DataStore JavaScript -ArrayJoin -BoolAsString New-HTMLTab -Name 'All Users' { New-HTMLTable -DataTable $CachedUsers.Values -Filtering { New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string } } foreach ($Rule in $Summary['Rules'].Keys) { New-HTMLTab -Name $Rule { New-HTMLTable -DataTable $Summary['Rules'][$Rule].Values.User -Filtering { New-TableCondition -Name 'Enabled' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string } } } New-HTMLTab -Name 'Email sent to users' { New-HTMLTable -DataTable $SummaryUsersEmails { New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string } } New-HTMLTab -Name 'Email sent to manager' { New-HTMLTable -DataTable $SummaryManagersEmails { New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' } } New-HTMLTab -Name 'Email sent to Security' { 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' } } } -ShowHTML:$HTMLOptions.ShowHTML -FilePath $FilePath -Online:$HTMLOptions.Online -WarningAction $WarningAction if ($SearchPath) { $SummarySearch['EmailSent'][$Today] = $SummaryUsersEmails $SummarySearch['EmailEscalations'][$Today] = $SummaryEscalationEmails $SummarySearch['EmailManagers'][$Today] = $SummaryManagersEmails try { $SummarySearch | Export-Clixml -LiteralPath $SearchPath #$SummarySearch | ConvertTo-Json | Out-File -LiteralPath $SearchPath } catch { Write-Color -Text "[e]", " Couldn't save to file $SearchPath", ". Error: ", $_.Exception.Message -Color White, Yellow, White, Yellow, White, Yellow, White } } } Export-ModuleMember -Function @('Find-Password', 'Find-PasswordNotification', 'Send-PasswordEmail', 'Start-PasswordSolution') -Alias @() # SIG # Begin signature block # MIIdWQYJKoZIhvcNAQcCoIIdSjCCHUYCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUuhallTlD7R5yUuUo/Gs/+nTB # i7CgghhnMIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0B # AQUFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD # VQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVk # IElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQsw # CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu # ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg # Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg # +XESpa7cJpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lT # XDGEKvYPmDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5 # a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g # 0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1 # roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf # GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0G # A1UdDgQWBBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLL # gjEtUYunpyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3 # cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmr # EthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+ # fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5Q # Z7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu # 838fYxAe+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw # 8jCCBP4wggPmoAMCAQICEA1CSuC+Ooj/YEAhzhQA8N0wDQYJKoZIhvcNAQELBQAw # cjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ # d3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVk # IElEIFRpbWVzdGFtcGluZyBDQTAeFw0yMTAxMDEwMDAwMDBaFw0zMTAxMDYwMDAw # MDBaMEgxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjEgMB4G # A1UEAxMXRGlnaUNlcnQgVGltZXN0YW1wIDIwMjEwggEiMA0GCSqGSIb3DQEBAQUA # A4IBDwAwggEKAoIBAQDC5mGEZ8WK9Q0IpEXKY2tR1zoRQr0KdXVNlLQMULUmEP4d # yG+RawyW5xpcSO9E5b+bYc0VkWJauP9nC5xj/TZqgfop+N0rcIXeAhjzeG28ffnH # bQk9vmp2h+mKvfiEXR52yeTGdnY6U9HR01o2j8aj4S8bOrdh1nPsTm0zinxdRS1L # sVDmQTo3VobckyON91Al6GTm3dOPL1e1hyDrDo4s1SPa9E14RuMDgzEpSlwMMYpK # jIjF9zBa+RSvFV9sQ0kJ/SYjU/aNY+gaq1uxHTDCm2mCtNv8VlS8H6GHq756Wwog # L0sJyZWnjbL61mOLTqVyHO6fegFz+BnW/g1JhL0BAgMBAAGjggG4MIIBtDAOBgNV # HQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcD # CDBBBgNVHSAEOjA4MDYGCWCGSAGG/WwHATApMCcGCCsGAQUFBwIBFhtodHRwOi8v # d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwHwYDVR0jBBgwFoAU9LbhIB3+Ka7S5GGlsqIl # ssgXNW4wHQYDVR0OBBYEFDZEho6kurBmvrwoLR1ENt3janq8MHEGA1UdHwRqMGgw # MqAwoC6GLGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtdHMu # Y3JsMDKgMKAuhixodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc2hhMi1hc3N1cmVk # LXRzLmNybDCBhQYIKwYBBQUHAQEEeTB3MCQGCCsGAQUFBzABhhhodHRwOi8vb2Nz # cC5kaWdpY2VydC5jb20wTwYIKwYBBQUHMAKGQ2h0dHA6Ly9jYWNlcnRzLmRpZ2lj # ZXJ0LmNvbS9EaWdpQ2VydFNIQTJBc3N1cmVkSURUaW1lc3RhbXBpbmdDQS5jcnQw # DQYJKoZIhvcNAQELBQADggEBAEgc3LXpmiO85xrnIA6OZ0b9QnJRdAojR6OrktIl # xHBZvhSg5SeBpU0UFRkHefDRBMOG2Tu9/kQCZk3taaQP9rhwz2Lo9VFKeHk2eie3 # 8+dSn5On7UOee+e03UEiifuHokYDTvz0/rdkd2NfI1Jpg4L6GlPtkMyNoRdzDfTz # ZTlwS/Oc1np72gy8PTLQG8v1Yfx1CAB2vIEO+MDhXM/EEXLnG2RJ2CKadRVC9S0y # OIHa9GCiurRS+1zgYSQlT7LfySmoc0NR2r1j1h9bm/cuG08THfdKDXF+l7f0P4Tr # weOjSaH6zqe/Vs+6WXZhiV9+p7SOZ3j5NpjhyyjaW4emii8wggUwMIIEGKADAgEC # AhAECRgbX9W7ZnVTQ7VvlVAIMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVT # MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j # b20xJDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xMzEw # MjIxMjAwMDBaFw0yODEwMjIxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNV # BAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwggEi # MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD407Mcfw4Rr2d3B9MLMUkZz9D7 # RZmxOttE9X/lqJ3bMtdx6nadBS63j/qSQ8Cl+YnUNxnXtqrwnIal2CWsDnkoOn7p # 0WfTxvspJ8fTeyOU5JEjlpB3gvmhhCNmElQzUHSxKCa7JGnCwlLyFGeKiUXULaGj # 6YgsIJWuHEqHCN8M9eJNYBi+qsSyrnAxZjNxPqxwoqvOf+l8y5Kh5TsxHM/q8grk # V7tKtel05iv+bMt+dDk2DZDv5LVOpKnqagqrhPOsZ061xPeM0SAlI+sIZD5SlsHy # DxL0xY4PwaLoLFH3c7y9hbFig3NBggfkOItqcyDQD2RzPJ6fpjOp/RnfJZPRAgMB # AAGjggHNMIIByTASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjAT # BgNVHSUEDDAKBggrBgEFBQcDAzB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGG # GGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2Nh # Y2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCB # gQYDVR0fBHoweDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lD # ZXJ0QXNzdXJlZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNl # cnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDBPBgNVHSAESDBGMDgG # CmCGSAGG/WwAAgQwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu # Y29tL0NQUzAKBghghkgBhv1sAzAdBgNVHQ4EFgQUWsS5eyoKo6XqcQPAYPkt9mV1 # DlgwHwYDVR0jBBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wDQYJKoZIhvcNAQEL # BQADggEBAD7sDVoks/Mi0RXILHwlKXaoHV0cLToaxO8wYdd+C2D9wz0PxK+L/e8q # 3yBVN7Dh9tGSdQ9RtG6ljlriXiSBThCk7j9xjmMOE0ut119EefM2FAaK95xGTlz/ # kLEbBw6RFfu6r7VRwo0kriTGxycqoSkoGjpxKAI8LpGjwCUR4pwUR6F6aGivm6dc # IFzZcbEMj7uo+MUSaJ/PQMtARKUT8OZkDCUIQjKyNookAv4vcn4c10lFluhZHen6 # dGRrsutmQ9qzsIzV6Q3d9gEgzpkxYz0IGhizgZtPxpMQBvwHgfqL2vmCSfdibqFT # +hKUGIUukpHqaGxEMrJmoecYpJpkUe8wggUxMIIEGaADAgECAhAKoSXW1jIbfkHk # Bdo2l8IVMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxE # aWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMT # G0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xNjAxMDcxMjAwMDBaFw0z # MTAxMDcxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0 # IFNIQTIgQXNzdXJlZCBJRCBUaW1lc3RhbXBpbmcgQ0EwggEiMA0GCSqGSIb3DQEB # AQUAA4IBDwAwggEKAoIBAQC90DLuS82Pf92puoKZxTlUKFe2I0rEDgdFM1EQfdD5 # fU1ofue2oPSNs4jkl79jIZCYvxO8V9PD4X4I1moUADj3Lh477sym9jJZ/l9lP+Cb # 6+NGRwYaVX4LJ37AovWg4N4iPw7/fpX786O6Ij4YrBHk8JkDbTuFfAnT7l3ImgtU # 46gJcWvgzyIQD3XPcXJOCq3fQDpct1HhoXkUxk0kIzBdvOw8YGqsLwfM/fDqR9mI # UF79Zm5WYScpiYRR5oLnRlD9lCosp+R1PrqYD4R/nzEU1q3V8mTLex4F0IQZchfx # FwbvPc3WTe8GQv2iUypPhR3EHTyvz9qsEPXdrKzpVv+TAgMBAAGjggHOMIIByjAd # BgNVHQ4EFgQU9LbhIB3+Ka7S5GGlsqIlssgXNW4wHwYDVR0jBBgwFoAUReuir/SS # y4IxLVGLp6chnfNtyA8wEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMC # AYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUF # BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6 # Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5j # cnQwgYEGA1UdHwR6MHgwOqA4oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9E # aWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwUAYDVR0gBEkw # RzA4BgpghkgBhv1sAAIEMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2lj # ZXJ0LmNvbS9DUFMwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEBCwUAA4IBAQBxlRLp # UYdWac3v3dp8qmN6s3jPBjdAhO9LhL/KzwMC/cWnww4gQiyvd/MrHwwhWiq3BTQd # aq6Z+CeiZr8JqmDfdqQ6kw/4stHYfBli6F6CJR7Euhx7LCHi1lssFDVDBGiy23UC # 4HLHmNY8ZOUfSBAYX4k4YU1iRiSHY4yRUiyvKYnleB/WCxSlgNcSR3CzddWThZN+ # tpJn+1Nhiaj1a5bA9FhpDXzIAbG5KHW3mWOFIoxhynmUfln8jA/jb7UBJrZspe6H # USHkWGCbugwtK22ixH67xCUrRwIIfEmuE7bhfEJCKMYYVs9BNLZmXbZ0e/VWMyIv # IjayS6JKldj1po5SMIIFPTCCBCWgAwIBAgIQBNXcH0jqydhSALrNmpsqpzANBgkq # hkiG9w0BAQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5j # MRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBT # SEEyIEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTIwMDYyNjAwMDAwMFoX # DTIzMDcwNzEyMDAwMFowejELMAkGA1UEBhMCUEwxEjAQBgNVBAgMCcWabMSFc2tp # ZTERMA8GA1UEBxMIS2F0b3dpY2UxITAfBgNVBAoMGFByemVteXPFgmF3IEvFgnlz # IEVWT1RFQzEhMB8GA1UEAwwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMIIBIjAN # BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7KB3iyBrhkLUbbFe9qxhKKPBYqD # Bqlnr3AtpZplkiVjpi9dMZCchSeT5ODsShPuZCIxJp5I86uf8ibo3vi2S9F9AlfF # jVye3dTz/9TmCuGH8JQt13ozf9niHecwKrstDVhVprgxi5v0XxY51c7zgMA2g1Ub # +3tii0vi/OpmKXdL2keNqJ2neQ5cYly/GsI8CREUEq9SZijbdA8VrRF3SoDdsWGf # 3tZZzO6nWn3TLYKQ5/bw5U445u/V80QSoykszHRivTj+H4s8ABiforhi0i76beA6 # Ea41zcH4zJuAp48B4UhjgRDNuq8IzLWK4dlvqrqCBHKqsnrF6BmBrv+BXQIDAQAB # o4IBxTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0O # BBYEFBixNSfoHFAgJk4JkDQLFLRNlJRmMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUE # DDAKBggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2Ny # bDQuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUw # QzA3BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNl # cnQuY29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcw # AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8v # Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNp # Z25pbmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAmr1s # z4lsLARi4wG1eg0B8fVJFowtect7SnJUrp6XRnUG0/GI1wXiLIeow1UPiI6uDMsR # XPHUF/+xjJw8SfIbwava2eXu7UoZKNh6dfgshcJmo0QNAJ5PIyy02/3fXjbUREHI # NrTCvPVbPmV6kx4Kpd7KJrCo7ED18H/XTqWJHXa8va3MYLrbJetXpaEPpb6zk+l8 # Rj9yG4jBVRhenUBUUj3CLaWDSBpOA/+sx8/XB9W9opYfYGb+1TmbCkhUg7TB3gD6 # o6ESJre+fcnZnPVAPESmstwsT17caZ0bn7zETKlNHbc1q+Em9kyBjaQRcEQoQQNp # ezQug9ufqExx6lHYDjGCBFwwggRYAgEBMIGGMHIxCzAJBgNVBAYTAlVTMRUwEwYD # VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAv # BgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EC # EATV3B9I6snYUgC6zZqbKqcwCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcCAQwxCjAI # oAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIB # CzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFMPtPuLZ6cXGX9KU/KRh # QlFmaQM3MA0GCSqGSIb3DQEBAQUABIIBABeuhaakJ5+U/1FOvoPGiZ2Sahn9ZfVW # V+0QinwcUJTgtvyv/njTSDVwItnS1dGUv/xhlgEDsvzm9nR6Nge0aMY8QcCaxfhV # 3MhKNeA+/hvESW7mH3t5feC/ZsVe+yVloxIBx/ZZHH7GyeAIkhzk06KtWxHEsxXv # ueFCcd/6tOMDNt7HblQqptQv6whxN6FqQTJot77UrvM4/c1UhOEOZ3N4+NVeMJu4 # 2lCb4GjEZhQCjA6QNXDkTzZIebcineS9r0fcD4fW66vKjlWgGotga5s6Whkclr7C # OUtt5BibKRmySlO9cn4V6WEG/4TJWh2zRoHDLfneQwEkZdxHccBUKqWhggIwMIIC # LAYJKoZIhvcNAQkGMYICHTCCAhkCAQEwgYYwcjELMAkGA1UEBhMCVVMxFTATBgNV # BAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8G # A1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIFRpbWVzdGFtcGluZyBDQQIQ # DUJK4L46iP9gQCHOFADw3TANBglghkgBZQMEAgEFAKBpMBgGCSqGSIb3DQEJAzEL # BgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIxMDcwMTIwNDIyOVowLwYJKoZI # hvcNAQkEMSIEIEf8TCLivqoRs27g91NIanMcFj59nQDVXnU237tg0ya0MA0GCSqG # SIb3DQEBAQUABIIBAANyxLB5mEAbsJwQK8ScxG60+hyJ4H5laPQKFLXxgkRGNqaw # kcx3Xz2AmTOvSJRhSu3Hgm6ouuXQrSgsJJDTKUNt1pTRq5w2O+S84syY7NByoRkG # siSRbZA4MPloT6tg2/t2yiISLqT7INIbUZqSg3IkdO3g0t5sxWqNwDZO8lTuqwfM # tm8DbaVYkzjgy82R0F4gbd3/SBfaTMPat348f1ZaEXeY1H9veC3V1GFnu9q2F8Wj # +eetNZppSjQZFLnD2FLdHXcpctK6ClrjpwZm+OrAxgqjY4A63P0tpC8M/QbGUyhQ # DIfdqVJQRnO+AOJny9i44c0kTKaSiQIMUrtR1ek= # SIG # End signature block |