Jorge's Quest For Knowledge!

All About Identity And Security On-Premises And In The Cloud – It's Just Like An Addiction, The More You Have, The More You Want To Have!

Archive for the ‘Tooling/Scripting’ Category

(2017-06-08) WHOAMI, The PowerShell Way

Posted by Jorge on 2017-06-08


Many of you probably know the command:

WHOAMI /USER /GROUPS

image

Figure 1: Executing WHOAMI

Every wanted to do the same in PowerShell?

[Security.Principal.WindowsIdentity]::GetCurrent())

image

Figure 2: Executing WHOAMI The PowerShell Equivalent

Although it gives you the required information, it still does not list the groups in a nice way. So let’s try that!

Unfortunately not a on-liner, but rather 2 lines of code Smile

$tableLayout = @{Expression={((New-Object System.Security.Principal.SecurityIdentifier($_.Value)).Translate([System.Security.Principal.NTAccount])).Value};Label="Group Name";Width=40},@{Expression={$_.Value};Label="Group SID";Width=45},@{Expression={$_.Type};Label="Group Type";Width=75}

([Security.Principal.WindowsIdentity]::GetCurrent()).Claims | FT $tableLayout

image

Figure 3: List Of Group Names, Group SIDs And Group Types From The Access Token

Cheers,
Jorge

 

————————————————————————————————————————————————————-
This posting is provided "AS IS" with no warranties and confers no rights!
Always evaluate/test everything yourself first before using/implementing this in production!
This is today’s opinion/technology, it might be different tomorrow and will definitely be different in 10 years!
DISCLAIMER:
https://jorgequestforknowledge.wordpress.com/disclaimer/
————————————————————————————————————————————————————-
########################### Jorge’s Quest For Knowledge ##########################
####################
http://JorgeQuestForKnowledge.wordpress.com/ ###################
————————————————————————————————————————————————————-

Advertisements

Posted in PowerShell, Tooling/Scripting | 1 Comment »

(2017-03-16) Domain Join Through An RODC Instead Of An RWDC (Update 2)

Posted by Jorge on 2017-03-16


In the blog post (2009-01-01) Domain Join through an RODC instead of an RWDC I explained the so called read-only domain join against an RODC. In that blog post you will find a VBS script that helps you achieve that goal. Prior to the VBS script you see multiple ways of pre-creating the computer and having the password of the computer account replicate to the RODC.

In this blog post I provide an updated PowerShell script (don’t forget the execution policy on the server!) that performs the read-only domain join. You can get the PowerShell script through this link, or you can copy it from below.

### Abstract: This PoSH Script Joins A Stand Alone Server To An AD Domain Through A Targeted RODC ### Written by: Jorge de Almeida Pinto [MVP-EMS] ### BLOG: https://jorgequestforknowledge.wordpress.com/ ### ### 2013-04-12: Initial version of the script in PowerShell (v0.1) ### 2017-03-16: Added description for code 1219, added logging (same folder as script), check and clean any site related setting in registry, ### added check that script is executed with admin credentials, added check for connectivity to RODC (v0.2) ### ### WARNING: This script checks connectivity to the targeted RODC for a specific set of ports. The script is configured with default ports ### that are required, but it is also configured with a "Custom RPC Static Port For NetLogon" (40961) as that is what I have configured in ### my test/demo environment. If you are using a different port number, then make sure to change that first before running the script. ### If you use the dynamic range of RPC ports OR you do not have a firewall between your servers and RODCs, then remove that custom port number! ### <# .SYNOPSIS Joins a stand alone server to an AD domain through a targeted RODC. .DESCRIPTION Joins a stand alone server to an AD domain through a targeted RODC. .PARAMETER adDomain The FQDN of the AD domain, the server needs to be joined to. .PARAMETER rodcFQDN The FQDN of the RODC that will be targeted to join the server to the AD domain through a read-only join. .PARAMETER ipAddressLocalHost The IP address of the local server. .PARAMETER compAccountPWD The password of the computer account that was set during the pre-creation of that computer account. .EXAMPLE - Join the server SERVER1 to the AD domain COMPANY.COM through the RODC RODC1.COMPANY.COM .\Read-Only-Domain-Join.ps1 -adDomain COMPANY.COM -rodcFQDN RODC1.COMPANY.COM -ipAddressLocalHost 192.168.6.3 -compAccountPWD 'MyPa$$w0rd' .NOTES This script requires local administrator permissions. #> Param( [Parameter(Mandatory=$TRUE, ValueFromPipeline=$TRUE, ValueFromPipelineByPropertyName=$TRUE, HelpMessage='Please specify the FQDN of the AD domain to join to.')] [ValidateNotNullOrEmpty()] [string]$adDomain, [Parameter(Mandatory=$TRUE, ValueFromPipeline=$TRUE, ValueFromPipelineByPropertyName=$TRUE, HelpMessage='Please specify the FQDN of the RODC to target for the read-only domain join.')] [ValidateNotNullOrEmpty()] [string]$rodcFQDN, [Parameter(Mandatory=$TRUE, ValueFromPipeline=$TRUE, ValueFromPipelineByPropertyName=$TRUE, HelpMessage='Please specify the IP address of the local stand alone server.')] [ValidateNotNullOrEmpty()] [string]$ipAddressLocalHost, [Parameter(Mandatory=$TRUE, ValueFromPipeline=$TRUE, ValueFromPipelineByPropertyName=$TRUE, HelpMessage='Please specify the password that was set for the pre-created computer account.')] [ValidateNotNullOrEmpty()] [string]$compAccountPWD ) ### FUNCTION: Logging Data To The Log File Function Logging($dataToLog, $lineType) { $datetimeLogLine = "[" + $(Get-Date -format "yyyy-MM-dd HH:mm:ss") + "] : " Out-File -filepath "$logFileFullPath" -append -inputObject "$datetimeLogLine$dataToLog" #Write-Output($datetimeLogLine + $dataToLog) If ($lineType -eq $NULL) { Write-Host "$datetimeLogLine$dataToLog" } If ($lineType -eq "SUCCESS") { Write-Host "$datetimeLogLine$dataToLog" -ForeGroundColor Green } If ($lineType -eq "ERROR") { Write-Host "$datetimeLogLine$dataToLog" -ForeGroundColor Red } If ($lineType -eq "WARNING") { Write-Host "$datetimeLogLine$dataToLog" -ForeGroundColor Red } If ($lineType -eq "HEADER") { Write-Host "$datetimeLogLine$dataToLog" -ForeGroundColor Magenta } If ($lineType -eq "REMARK") { Write-Host "$datetimeLogLine$dataToLog" -ForeGroundColor Cyan } } ### FUNCTION: Test Credentials For Admin Privileges Function Test-Admin { $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() (New-Object Security.Principal.WindowsPrincipal $currentUser).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) } ### FUNCTION: Test The Port Connection # Source: # Based Upon http://gallery.technet.microsoft.com/scriptcenter/97119ed6-6fb2-446d-98d8-32d823867131 Function PortConnectionCheck($fqdnServer,$port,$timeOut) { $tcpPortSocket = $null $portConnect = $null $tcpPortWait = $null $tcpPortSocket = New-Object System.Net.Sockets.TcpClient $portConnect = $tcpPortSocket.BeginConnect($fqdnServer,$port,$null,$null) $tcpPortWait = $portConnect.AsyncWaitHandle.WaitOne($timeOut,$false) If(!$tcpPortWait) { $tcpPortSocket.Close() Return "ERROR" } Else { #$error.Clear() $ErrorActionPreference = "SilentlyContinue" $tcpPortSocket.EndConnect($portConnect) | Out-Null If (!$?) { Return "ERROR" } Else { Return "SUCCESS" } $tcpPortSocket.Close() $ErrorActionPreference = "Continue" } } ### FUNCTION: Determine The Network ID To Which The IP Address And Subnet Mask Belong # Written By Nathan Linley | http://myitpath.blogspot.com # Source Of Original Script: http://poshcode.org/2888 Function Get-NetworkID ([string]$ipAddress, [string]$subnetMask) { $ipOctets = $ipAddress.split(".") $subnetOctets = $subnetMask.split(".") $result = "" For ($i = 0; $i -lt 4; $i++) { $result += $ipOctets[$i] -band $subnetOctets[$i] $result += "." } $result = $result.substring(0,$result.length -1) return $result } ### FUNCTION: Determine The Subnet Mask Based Upon The Specified Mask Bits Function Get-SubnetMask-ByLength ([int]$length) { If ($length -eq $null -or $length -gt 32 -or $length -lt 0) { Write-Error "Function 'Get-SubnetMask-ByLength'...: Invalid Subnet Mask Length Provided. Please Provide A Number BETWEEN 0 And 32" Return $null } switch ($length) { "32" {return "255.255.255.255"} "31" {return "255.255.255.254"} "30" {return "255.255.255.252"} "29" {return "255.255.255.248"} "28" {return "255.255.255.240"} "27" {return "255.255.255.224"} "26" {return "255.255.255.192"} "25" {return "255.255.255.128"} "24" {return "255.255.255.0"} "23" {return "255.255.254.0"} "22" {return "255.255.252.0"} "21" {return "255.255.248.0"} "20" {return "255.255.240.0"} "19" {return "255.255.224.0"} "18" {return "255.255.192.0"} "17" {return "255.255.128.0"} "16" {return "255.255.0.0"} "15" {return "255.254.0.0"} "14" {return "255.252.0.0"} "13" {return "255.248.0.0"} "12" {return "255.240.0.0"} "11" {return "255.224.0.0"} "10" {return "255.192.0.0"} "9" {return "255.128.0.0"} "8" {return "255.0.0.0"} "7" {return "254.0.0.0"} "6" {return "252.0.0.0"} "5" {return "248.0.0.0"} "4" {return "240.0.0.0"} "3" {return "224.0.0.0"} "2" {return "192.0.0.0"} "1" {return "128.0.0.0"} "0" {return "0.0.0.0"} } } ### Clear The Screen Clear-Host ### Configure The Appropriate Screen And Buffer Size To Make Sure Everything Fits Nicely $uiConfig = (Get-Host).UI.RawUI $uiConfig.WindowTitle = "+++ READ-ONLY DOMAIN JOIN THROUGH AN RODC +++" $uiConfig.ForegroundColor = "Yellow" $uiConfigBufferSize = $uiConfig.BufferSize $uiConfigBufferSize.Width = 140 $uiConfigBufferSize.Height = 9999 $uiConfigScreenSizeMax = $uiConfig.MaxPhysicalWindowSize $uiConfigScreenSizeMaxWidth = $uiConfigScreenSizeMax.Width $uiConfigScreenSizeMaxHeight = $uiConfigScreenSizeMax.Height $uiConfigScreenSize = $uiConfig.WindowSize If ($uiConfigScreenSizeMaxWidth -lt 140) { $uiConfigScreenSize.Width = $uiConfigScreenSizeMaxWidth } Else { $uiConfigScreenSize.Width = 140 } If ($uiConfigScreenSizeMaxHeight -lt 75) { $uiConfigScreenSize.Height = $uiConfigScreenSizeMaxHeight - 5 } Else { $uiConfigScreenSize.Height = 75 } $uiConfig.BufferSize = $uiConfigBufferSize $uiConfig.WindowSize = $uiConfigScreenSize ### Definition Of Some Constants $execDateTime = Get-Date $execDateTimeYEAR = $execDateTime.Year $execDateTimeMONTH = $execDateTime.Month $execDateTimeDAY = $execDateTime.Day $execDateTimeHOUR = $execDateTime.Hour $execDateTimeMINUTE = $execDateTime.Minute $execDateTimeSECOND = $execDateTime.Second $execDateTimeCustom = [STRING]$execDateTimeYEAR + "-" + $("{0:D2}" -f $execDateTimeMONTH) + "-" + $("{0:D2}" -f $execDateTimeDAY) + "_" + $("{0:D2}" -f $execDateTimeHOUR) + "." + $("{0:D2}" -f $execDateTimeMINUTE) + "." + $("{0:D2}" -f $execDateTimeSECOND) $localComputer = Get-WmiObject -Class Win32_ComputerSystem $localComputerName = $localComputer.Name $scriptFileFullPath = $MyInvocation.MyCommand.Definition $currentScriptFolderPath = Split-Path $scriptFileFullPath $logFileFullPath = Join-Path $currentScriptFolderPath $($execDateTimeCustom + "_Read-Only-Domain-Join_" + $localComputerName + ".log") $rodcNBT = $rodcFQDN.Substring(0,$rodcFQDN.IndexOf(".")) $userName = $adDomain + "\" + $localComputerName + "`$" $userPassword = $compAccountPWD $ports = 53,88,135,389,445,464,636,3268,3269,40961 # DNS, Kerberos, RPC Endpoint Mapper, LDAP, SMB, Kerberos Change/Set Password, LDAP-SSL, GC, GC-SSL, Custom RPC Static Port For NetLogon ### Definition Of Some Variables Set-Variable JOIN_DOMAIN -option Constant -value 1 # Joins a computer to a domain. If this value is not specified, the join is a computer to a workgroup Set-Variable ACCT_CREATE -option Constant -value 2 # Creates an account on a domain Set-Variable ACCT_DELETE -option Constant -value 4 # Deletes an account when a domain exists Set-Variable WIN9X_UPGRADE -option Constant -value 16 # The join operation is part of an upgrade from Windows 98 or Windows 95 to Windows 2000 or Windows NT Set-Variable DOMAIN_JOIN_IF_JOINED -option Constant -value 32 # Allows a join to a new domain, even if the computer is already joined to a domain Set-Variable JOIN_UNSECURE -option Constant -value 64 # Performs an unsecured join Set-Variable MACHINE_PASSWORD_PASSED -option Constant -value 128 # The machine, not the user, password passed. This option is only valid for unsecure joins Set-Variable DEFERRED_SPN_SET -option Constant -value 256 # Writing SPN and DnsHostName attributes on the computer object should be deferred until the rename that follows the join Set-Variable NETSETUP_JOIN_READONLY -option Constant -value 2048 # Use an RODC to perform the domain join against Set-Variable INSTALL_INVOCATION -option Constant -value 262144 # The APIs were invoked during install ### Domain Join Options To Use $domainJoinOption = $JOIN_DOMAIN + $MACHINE_PASSWORD_PASSED + $NETSETUP_JOIN_READONLY Logging "" Logging "**********************************************************" "HEADER" Logging "* *" "HEADER" Logging "* --> Read-Only Domain Join Through An RODC <-- *" "HEADER" Logging "* *" "HEADER" Logging "* Written By: Jorge de Almeida Pinto [MVP-EMS] *" "HEADER" Logging "* *" "HEADER" Logging " BLOG: 'Jorge's Quest For Knowledge' *" "HEADER" Logging " (https://jorgequestforknowledge.wordpress.com/) *" "HEADER" Logging "* *" "HEADER" Logging "**********************************************************" "HEADER" Logging "" ### Pre-Requisites Check Logging "" Logging "------------------------------------------------------------------------------------------------------------------" "HEADER" Logging "+++ PRE-REQUISITES CHECK +++" "HEADER" Logging "" Logging "ATTENTION: To Execute This Script, The Following Pre-Requisites Must Be met:" "WARNING" Logging " * Local Server Is Configured Correctly With IP Address, Subnet Mask And DNS Servers..." "WARNING" Logging " * Admin Account Must Be(Direct) Member Of Local 'Administrators' Group!..." "WARNING" Logging " * If UAC Is Used, Admin Account Must Be Running Within An Elevated Administrator Command Prompt!..." "WARNING" Logging " * Required Ports Must Be Opened Between This Server And Targeted RODC!..." "WARNING" Logging "" Logging "ATTENTION: This Script Will Fail Without The Pre-Requisites Mentioned Above!" "WARNING" Logging "" Logging "Press Any Key To Continue...(TWICE)" Logging "" $x = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") $x = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") ### Checking For Admin Credentials And If Those Admin Credentials have Been Elevated Due To UAC If (!(Test-Admin)) { Logging "" Logging "WARNING:" "ERROR" Logging " * Your Admin Account IS NOT A (Direct) Member Of The Local 'Administrators' Group!..." "ERROR" Logging " * Your Admin Account IS NOT Running Within An Elevated Administrator Command Prompt!..." "ERROR" Logging "" Logging "Aborting Script..." "ERROR" Logging "" EXIT } Else { Logging "" Logging "SUCCESS:" "SUCCESS" Logging " * Your Admin Account IS A (Direct) Member Of The Local 'Administrators' Group!..." "SUCCESS" Logging " * Your Admin Account IS Running Within An Elevated Administrator Command Prompt!..." "SUCCESS" Logging "" Logging "Continuing Script..." "SUCCESS" Logging "" } ### Checking Connectivity (TCP Only!) Between This Server And The Target RODC $checkOK = $true $ports | %{ $port = $_ $connectionResult = $null $connectionResult = PortConnectionCheck $rodcFQDN $port 500 If ($connectionResult -eq "SUCCESS") { Logging "The RODC '$rodcFQDN' IS Accessible And Listening On Port '$port'..." "SUCCESS" } If ($connectionResult -eq "ERROR") { Logging "The RODC '$rodcFQDN' IS NOT Accessible And Listening On Port '$port'..." "ERROR" $checkOK = $false } } If (!$checkOK) { Logging "" Logging "WARNING:" "ERROR" Logging " * One Or More Of The Required Ports IS/ARE NOT Available..." "ERROR" Logging "" Logging "Aborting Script..." "ERROR" Logging "" EXIT } Else { Logging "" Logging "SUCCESS:" "SUCCESS" Logging " * All The Required Ports ARE Available..." "SUCCESS" Logging "" Logging "Continuing Script..." "SUCCESS" Logging "" } ### Checking Local Registry Settings For Site Definition $regNameExistSiteName = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" -Name SiteName -ErrorAction SilentlyContinue If ($regNameExistSiteName) { Remove-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" -Name SiteName -Force Logging "" Logging "Registry Value 'SiteName' In 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters' Exists..." Logging "" Logging "Registry Value 'SiteName' Has Been Deleted..." Logging "" } $regNameExistDynamicSiteName = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" -Name DynamicSiteName -ErrorAction SilentlyContinue If ($regNameExistDynamicSiteName) { Remove-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" -Name DynamicSiteName -Force Logging "" Logging "Registry Value 'DynamicSiteName' In 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters' Exists..." Logging "" Logging "Registry Value 'DynamicSiteName' Has Been Deleted..." Logging "" } ### Change Preferred Error Action $ErrorActionPreference = "SilentlyContinue" ### Connecting To AD On The RODC And Getting Some NCs $rootDSEldapPath = "LDAP://$rodcFQDN/rootDSE" $directoryEntryrootDSE = New-Object System.DirectoryServices.DirectoryEntry($rootDSEldapPath, $userName, $userPassword) $defaultNamingContext = $directoryEntryrootDSE.defaultNamingContext $configurationNamingContext = $directoryEntryrootDSE.configurationNamingContext ### Checking Pre-Created Computer Account Exists And The Correct Password Of The Computer Account Is Being Used $defaultNCldapPath = "LDAP://$rodcFQDN/$defaultNamingContext" $defaultNCdirectoryEntry = New-Object System.DirectoryServices.DirectoryEntry($defaultNCldapPath, $userName, $userPassword) $SearcherSRVCompAccount = $null $SearcherSRVCompAccount = New-Object DirectoryServices.DirectorySearcher($defaultNCdirectoryEntry) $SearcherSRVCompAccount.SearchScope = "Subtree" $SearcherSRVCompAccount.Filter = "(&(objectClass=computer)(sAMAccountName=$localComputerName`$))" $SearcherSRVCompAccountResult = $null $SearcherSRVCompAccountResult = $SearcherSRVCompAccount.FindOne() $dnSRVCompAccount = $null $dnSRVCompAccount = $SearcherSRVCompAccountResult.Properties.distinguishedname If ($dnSRVCompAccount) { Logging "" Logging "SUCCESS:" "SUCCESS" Logging " * A Computer Account For This Server DOES Exist...And" "SUCCESS" Logging " * A Correct Password Is Being Used..." "SUCCESS" Logging "" Logging "Continuing Script..." "SUCCESS" Logging "" } Else { Logging "" Logging "WARNING:" "ERROR" Logging " * A Computer Account For This Server DOES NOT Exist...Or" "ERROR" Logging " * An Incorrect Password Is Being Used..." "ERROR" Logging "" Logging "Aborting Script..." "ERROR" Logging "" EXIT } ### Change Preferred Error Action To Default $ErrorActionPreference = "Continue" $regNameExistSiteName = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" -Name SiteName -ErrorAction SilentlyContinue If ($regNameExistSiteName) { Remove-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" -Name SiteName -Force Logging "" Logging "Registry Value 'SiteName' In 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters' Exists..." Logging "" Logging "Registry Value 'SiteName' Has Been Deleted..." Logging "" } $regNameExistDynamicSiteName = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" -Name DynamicSiteName -ErrorAction SilentlyContinue If ($regNameExistDynamicSiteName) { Remove-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" -Name DynamicSiteName -Force Logging "" Logging "Registry Value 'DynamicSiteName' In 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters' Exists..." Logging "" Logging "Registry Value 'DynamicSiteName' Has Been Deleted..." Logging "" } ### Initiating Read-Only Domain Join Logging "" Logging "------------------------------------------------------------------------------------------------------------------" "HEADER" Logging "+++ INITIATING READ-ONLY DOMAIN JOIN +++" "HEADER" ### Determining The AD Site Of The Specified/Targeted RODC $rodcCompAccountldapPath = "LDAP://$rodcFQDN/CN=$rodcNBT,OU=Domain Controllers,$defaultNamingContext" $rodcCompAccountdirectoryEntry = $null $rodcCompAccountdirectoryEntry = New-Object System.DirectoryServices.DirectoryEntry($rodcCompAccountldapPath, $userName, $userPassword) $SearcherRodcCompAccount = $null $SearcherRodcCompAccount = New-Object DirectoryServices.DirectorySearcher($rodcCompAccountdirectoryEntry) $SearcherRodcCompAccount.SearchScope = "Base" $SearcherRodcCompAccount.Filter = "(&(objectClass=computer)(dNSHostName=$rodcFQDN))" $SearcherRodcCompAccount.PropertiesToLoad.Add("msDS-SiteName") | Out-Null $SearcherRodcCompAccountResult = $null $SearcherRodcCompAccountResult = $SearcherRodcCompAccount.FindOne() $rodcADSite = $null [string]$rodcADSite = $SearcherRodcCompAccountResult.Properties."msds-sitename" ### Matching The IP Address Of The Local Server Against An AD Site In The AD Forest $subnetsContainerldapPath = "LDAP://$rodcFQDN/CN=Subnets,CN=Sites,$configurationNamingContext" $subnetsContainerdirectoryEntry = $null $subnetsContainerdirectoryEntry = New-Object System.DirectoryServices.DirectoryEntry($subnetsContainerldapPath, $userName, $userPassword) $searcherSubnets = $null $searcherSubnets = New-Object DirectoryServices.DirectorySearcher($subnetsContainerdirectoryEntry) $searcherSubnets.SearchScope = "Subtree" $searcherSubnets.PropertiesToLoad.Add("name") | Out-Null $searcherSubnets.PropertiesToLoad.Add("siteObject") | Out-Null # We Can Take Network Masks In Both Length And Full Octet Format # We Need To Use Both. LDAP Searches # Use Length, And Network ID Generation Is By Full Octet Format. $startMaskLength = 32 For ($i = $startMaskLength; $i -ge 0; $i--) { # Loop Through Netmasks From /32 To /0 Looking For A Subnet Match In AD # Go Through All Masks From Longest To Shortest $subnetMask = &Get-SubnetMask-ByLength $i $networkID = &Get-NetworkID $ipAddressLocalHost $subnetMask # LDAP Search For The Network $searcherSubnets.filter = "(&(objectClass=subnet)(objectCategory=subnet)(cn=" + $networkID + "/" + $i + "))" $subnetObjectResult = $null $subnetObjectResult = $searcherSubnets.FindOne() #$subnetObjectsList = $searcherSubnets.FindAll() #$subnetsTable = @() #$subnetObjectsList.Properties | %{ # $subnetsTableObj = "" | Select "AD Subnet","AD Site" # $subnetsTableObj."AD Subnet" = ($_.name)[0] # $subnetsTableObj."AD Site" = $(($_.siteobject)[0]).Substring(3,$(($_.siteobject)[0]).IndexOf(",")-3) # $subnetsTable += $subnetsTableObj #} #$subnetsTable | FT -Autosize If ($subnetObjectResult -ne $null) { # If A Match Is Found, Return It Since It Is The Longest Length (Closest Match) $localComputerADSubnet = $null [string]$localComputerADSubnet = $($subnetObjectResult.Properties.name) $localComputerADSite = $null [string]$localComputerADSite = $($subnetObjectResult.Properties.siteobject).Substring(3,$($subnetObjectResult.Properties.siteobject).IndexOf(",")-3) #return $localComputerADSite Break } $subnetObjectResult = $null [string]$localComputerADSubnet = $null [string]$localComputerADSite = $null } If ($localComputerADSubnet -eq $null -Or $localComputerADSite -eq $null) { [string]$localComputerADSubnet = "NO_MATCH_FOUND" [string]$localComputerADSite = "NO_MATCH_FOUND" } ### Present The Information Logging "" Logging "Trying To Join The Local Computer '$localComputerName' To The AD Domain '$adDomain' Using The RODC '$rodcFQDN'..." Logging "" Logging "FQDN AD Domain............: $adDomain" Logging "FQDN RODC.................: $rodcFQDN" Logging "AD Site RODC..............: $rodcADSite" Logging "AD Site Local Computer....: $localComputerADSite" Logging "Matching AD Subnet........: $localComputerADSubnet" Logging "Local Computer Name.......: $localComputerName ($localComputerName`$)" Logging "Distinguished Name........: $dnSRVCompAccount" Logging "Computer Account Password.: $compAccountPWD" Logging "" ### AD Sites Must Match, Otherwise Something Is Wrong If ($rodcADSite.ToUpper() -ne $localComputerADSite.ToUpper() -Or $localComputerADSite -eq "NO_MATCH_FOUND") { Logging "" Logging "WARNING:" "ERROR" Logging " * The AD Site Of The Local Computer DOES NOT Match The AD Site Of The Specified RODC..." "ERROR" Logging " * Make Sure The IP Address Of The Local Server Is Configured Correctly So That It Will Match Against The Same AD Site As The Targeteed RODC..." "ERROR" Logging " * The Cause Of The Mismatch Can Be:" "ERROR" Logging " * The Specified IP Address IS NOT Correct..." "ERROR" Logging " * The Specified RODC IS NOT Correct..." "ERROR" Logging " * The AD Subnet For The Local Computer Is Linked To The Incorrect AD Site..." "ERROR" Logging "" Logging "Aborting Script..." "ERROR" Logging "" EXIT } ### Joining The Local Computer To The AD Domain Using The Specified Domain Join Options $returnErrorCode = $localComputer.JoinDomainOrWorkGroup($adDomain + "\" + $rodcFQDN, $compAccountPWD, $null, $null, $domainJoinOption) # List of 'system error codes' (http://msdn.microsoft.com/en-us/library/ms681381.aspx) and # List of 'network management error codes' (http://msdn.microsoft.com/en-us/library/aa370674(VS.85).aspx) $returnErrorDescription = switch ($($returnErrorCode.ReturnValue)) { 0 {"SUCCESS: The Operation Completed Successfully."} 5 {"FAILURE: Access Is Denied."} 53 {"FAILURE: The Network Path Was Not Found."} 64 {"FAILURE: The Specified Network Name Is No Longer Available."} 87 {"FAILURE: The Parameter Is Incorrect."} 1219 {"FAILURE: Logon Failure: Multiple Credentials In Use For Target Server."} 1326 {"FAILURE: Logon Failure: Unknown Username Or Bad Password."} 1355 {"FAILURE: The Specified Domain Either Does Not Exist Or Could Not Be Contacted."} 2691 {"FAILURE: The Machine Is Already Joined To The Domain."} default {"FAILURE: Unknown Error!"} } If ($($returnErrorCode.ReturnValue) -eq "0") { Logging "Domain Join Result Code...: $($returnErrorCode.ReturnValue)" "SUCCESS" Logging "Domain Join Result Text...: $returnErrorDescription" "SUCCESS" } Else { Logging "Domain Join Result Code...: $($returnErrorCode.ReturnValue)" "ERROR" Logging "Domain Join Result Text...: $returnErrorDescription" "ERROR" } If ($($returnErrorCode.ReturnValue) -eq "0") { Logging "" Logging "REMARK:" "REMARK" Logging " * The Computer Account Password Will Be Changed Shortly After The Domain Join!" "REMARK" Logging "" Logging "!!! THE COMPUTER WILL REBOOT AUTOMATICALLY IN 2 MINUTES !!!" "REMARK" Logging "" Logging "!!! TO STOP THE REBOOT USE THE COMMAND: SHUTDOWN /A !!!" "REMARK" SHUTDOWN /R /T 120 } Logging "" Logging "+++ FINISHED +++" "HEADER" Logging "------------------------------------------------------------------------------------------------------------------" "HEADER"

Have fun!

Cheers,

Jorge

———————————————————————————————

* This posting is provided "AS IS" with no warranties and confers no rights!

* Always evaluate/test yourself before using/implementing this!

* DISCLAIMER: https://jorgequestforknowledge.wordpress.com/disclaimer/

———————————————————————————————

############### Jorge’s Quest For Knowledge #############

######### http://JorgeQuestForKnowledge.wordpress.com/ ########

———————————————————————————————

Posted in Active Directory Domain Services (ADDS), Domain Join, PowerShell, Read-Only Domain Controller, Tooling/Scripting | Leave a Comment »

(2016-07-24) Fixing Web Content Data In ADFS 2012 R2 (v3.0) When Leveraging WID As A Database Store

Posted by Jorge on 2016-07-24


This blog post only applies if you are using ADFS v3.0 (ADFS 2012 R2) AND you are using WID as the database store! It does not apply when using SQL, and it does not apply when using ADFS v4.0 (ADFS 2016) with WID!

In ADFS v3.0 (and higher) it is possible to configure custom web content for:

  1. Relying Party Trust Web Content (*)
  2. Global Web Content
  3. Authentication Provider Web Content (*)
  4. Web Config
  5. Web Theme

When using WID, you must execute the configuration on the primary ADFS server. After 5 minutes (default) at a maximum, the secondary ADFS servers, get the changes from the primary ADFS server. Well, with regards to web content, almost right

For the web content stuff marked with a (*), there is bug where the content defined at the primary ADFS server DOES NOT replicate to the secondary ADFS servers. Because of that users may experience inconsistent results

Example configurations for [1]:

Set-AdfsRelyingPartyWebContent -Name ‘SALESFORCE dot COM’ -ErrorPageAuthorizationErrorMessage "<B><Font size=’4′ color=’red’>Authorization Has Been Denied For ‘SALESFORCE.COM’.</Font></B><BR><BR>You Either Do Not Have The Correct Authorization Or You Have Been Assigned More Than One Profile ID.<BR><BR>Please Contact <A HREF=’mailto:ADM.ROOT@IAMTEC.NL?subject=Access Request For Application 'SALESFORCE.COM'’>ADM.ROOT</A> To Resolve This If You Require Access."

Example configurations for [3]:

Set-AdfsAuthenticationProviderWebContent -Name AzureMfaServerAuthentication -DisplayName ‘Azure AD MFA AuthN’ -Description ‘Azure AD MFA Based Upon SMS, Phone Call Or Authenticator App’

The user experience is as follows….

When a user hits the primary ADFS server, the following is displayed, which is the custom web content for a relying party trust:

image

Figure 1: Custom Web Content For A Relying Party Trust On The Primary ADFS Server

When a user hits any of the secondary ADFS servers, the following is displayed, which is the default web content for a relying party trust:

image

Figure 2: Default Web Content For A Relying Party Trust On The Primary ADFS Server

When a user hits the primary ADFS server, the following is displayed, which is the custom web content for a authentication provider:

image

Figure 3: Custom Web Content For An Authentication Provider Relying Party Trust On The Primary ADFS Server

When a user hits any of the secondary ADFS servers, the following is displayed, which is the default web content for an authentication provider:

image

Figure 4: Default Web Content For An Authentication Provider Relying Party Trust On The Primary ADFS Server

So, as you can see the user experience is quite different when hitting either the primary ADFS server or any secondary ADFS server

You might think to also execute the PowerShell commands on any secondary ADFS server. However, that’s not possible because secondary ADFS servers are not writable and therefore the PowerShell commands do not work. It will work however, if you temporarily configure a secondary to be a primary, execute the commands, then reconfigure it back to a secondary. If you have multiple WID based ADFS servers, that can be some extensive work, which is also subject to mistakes ending up in inconsistencies.

So, how to solve this?

I wrote a script, which is available here that helps in configuring the web content on secondary ADFS servers.

WARNING: I do not know if this is supported or not by Microsoft. However, it does solve the problem as currently unfortunately there is no hotfix that fixes this issue in ADFS v3.0. Make sure to test this FIRST in a test lab before using it in production!

Please provide feedback through the comments section OR you the contact page

DISCLAIMER (READ THIS!):

  • I wrote this script, therefore I own it. Anyone asking money for it, should NOT be doing that and is basically ripping you off!
  • The script is freeware, you are free to use it and distribute it, but always refer to this website (https://jorgequestforknowledge.wordpress.com/) as the location where you got it.
  • This script is furnished "AS IS". No warranty is expressed or implied!
  • I have NOT tested it in every scenario nor have I tested it against every Windows and/or AD version
  • Always test first in lab environment to see if it meets your needs!
  • Use this script at your own risk!
  • I do not warrant this script to be fit for any purpose, use or environment!
  • I have tried to check everything that needed to be checked, but I do not guarantee the script does not have bugs!
  • I do not guarantee the script will not damage or destroy your system(s), environment or whatever!
  • I do not accept liability in any way if you screw up, use the script wrong or in any other way where damage is caused to your environment/systems!
  • If you do not accept these terms do not use the script in any way and delete it immediately!

SYNTAX:

  • <PoSH Script File> [-adfsServers <FQDN ADFS server 1>,<FQDN ADFS server 2>,etc ] [-scriptBlock <PowerShell Command>] [-scriptFile <Path To Text File Containing PowerShell Commands>] [-showScriptOutput]

This script is well documented (look inside the script) or execute:

Get-help .\Process-Web-Content-On-WID-Based-ADFS-Servers.ps1 -full

….but I’ll explain the parameters that can be used

Parameter “adfsServers”

When this parameter is specified, the XML config file is NOT read and a separated list of FQDNs must be specified through this parameter listing the ADFS servers that must be targeted. This may for example be used when you have just one or more new secondary ADFS server to update after those have been installed in addition to the existing ones

However, if you have a new configuration that must be applied to all ADFS servers, you may still use this parameter, but you can also create an XML file that contains all ADFS servers. This can be handy when you must apply changes to all existing ADFS servers. When you want to use the XML config file, do not use this parameter and the script will look for the XML config file which must be in the same folder as the script itself. By default the script will look for the XML file! It will abort if it does not find the XML config file!

EXAMPLE Contents of “ADFS-STS-SCRIPT-CONFIG.XML”

<?xml version="1.0" encoding="utf-8"?>
<adfsScriptConfig xmlns:xsi="
http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <adfsServers>
        <adfsServer serverName="R1FSRWDC1.IAMTEC.NET" />
        <adfsServer serverName="R1FSRWDC2.IAMTEC.NET" />
        <adfsServer serverName="R1FSRWDC3.IAMTEC.NET" />
        <adfsServer serverName="R1FSRWDC4.IAMTEC.NET" />
    </adfsServers>
</adfsScriptConfig>

Parameter “scriptBlock”

With this parameter one PowerShell command can be specified as a value for this parameter. Pay very special attention to the quotes used!

Example value: "Set-AdfsRelyingPartyWebContent -Name ‘SALESFORCE dot COM’ -ErrorPageAuthorizationErrorMessage `"<B><Font size=’4′ color=’red’>Authorization Has Been Denied For ‘SALESFORCE.COM’.</Font></B><BR><BR>You Either Do Not Have The Correct Authorization Or You Have Been Assigned More Than One Profile ID.<BR><BR>Please Contact <A HREF=’mailto:ADM.ROOT@IAMTEC.NL?subject=Access Request For Application 'SALESFORCE.COM'’>ADM.ROOT</A> To Resolve This If You Require Access.`""

Parameter “scriptFile”

With this parameter one or more PowerShell commands can be specified in a text file. The complete path of the text is then used as a value for this parameter.

Example value: "C:\TEMP\ScriptBlock.txt"

Parameter “showScriptOutput”

This parameter tells the script to display the output of the commands on screen, if there is anything to display at all.

Another thing to be aware of is that the script logs everything into an event log called “Custom – Support”. If the event log does not exist it will create it and also register the source. If you do not want this, scan through the script and remove or out comment those parts!

image

Figure 5: Example XML Config File

image

Figure 6: Example Script Block File Containing Multiple PowerShell Commands To Execute

image

Figure 7: Example Output – General Info

image

Figure 8: Example Output – Performing Checks

image

Figure 9: Example Output – Processing Commands Against An ADFS Server (R1FSRWDC1.IAMTEC.NET)

image

Figure 10: Example Output – Processing Commands Against An ADFS Server (R1FSRWDC2.IAMTEC.NET)

image

Figure 11: Example Output – Processing Commands Against An ADFS Server (R1FSRWDC3.IAMTEC.NET)

image

Figure 12: Example Output – Processing Commands Against An ADFS Server (R1FSRWDC4.IAMTEC.NET)

Cheers,
Jorge
———————————————————————————————
* This posting is provided "AS IS" with no warranties and confers no rights!
* Always evaluate/test yourself before using/implementing this!
* DISCLAIMER:
https://jorgequestforknowledge.wordpress.com/disclaimer/
———————————————————————————————
############### Jorge’s Quest For Knowledge #############
#########
http://JorgeQuestForKnowledge.wordpress.com/ ########
———————————————————————————————

Posted in Active Directory Federation Services (ADFS), DB On WID, PowerShell, Tooling/Scripting | Leave a Comment »

(2016-07-02) The Export Script “Export-FederationConfiguration.ps1” Throws An Error in ADFS v3

Posted by Jorge on 2016-07-02


With ADFS v3.0 (ADFS in Windows Server 2012 R2), Microsoft has provided an export script (“Export-FederationConfiguration.ps1”) and an import script (“Import-FederationConfiguration.ps1”). Both scripts can be found in the folder “C:\Windows\ADFS”. The scripts are to be used to support the migration from ADFS v2.x to ADFS v3.0. The export script should be used on ADFS v2.x and the import script should be used on ADFS 3.0

As you may have read in this blog post you need to export and import the configuration when moving from SQL-based ADFS to WID-based ADFS.

However, when running the export script on ADFS v3.0, you will see the following error

image

Figure 1: Error When Running The Export Script “Export-FederationConfiguration.ps1” On ADFS v3.0

Unable to find type [Microsoft.IdentityServer.PowerShell.Resources.RelyingPartyTrust]. Make sure that the assembly that contains this type
is loaded.
At C:\Windows\ADFS\Export-FederationConfiguration.ps1:1220 char:9
+         $rpTrusts = [Microsoft.IdentityServer.PowerShell.Resources.RelyingPartyT …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (Microsoft.Ident…lyingPartyTrust:TypeName) [], RuntimeException
    + FullyQualifiedErrorId : TypeNotFound

Unable to find type [Microsoft.IdentityServer.PowerShell.Resources.RelyingPartyTrust]. Make sure that the assembly that contains this type
is loaded.
At C:\Windows\ADFS\Export-FederationConfiguration.ps1:1220 char:9
+         $rpTrusts = [Microsoft.IdentityServer.PowerShell.Resources.RelyingPartyT …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (Microsoft.Ident…lyingPartyTrust:TypeName) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : TypeNotFound

The reason for not working?

The reason the export script does not work in ADFS v3 has to do with a reference to an assembly that does not exist in ADFS 3.0, but only ADFS v2.x

The solution?

A guy named Jeff Patton, already published a fixed version of the script. You can find it on Github: https://gist.github.com/jeffpatton1971/12f9e00dbca27abf8b59

Cheers,
Jorge
———————————————————————————————
* This posting is provided "AS IS" with no warranties and confers no rights!
* Always evaluate/test yourself before using/implementing this!
* DISCLAIMER:
https://jorgequestforknowledge.wordpress.com/disclaimer/
———————————————————————————————
############### Jorge’s Quest For Knowledge #############
#########
http://JorgeQuestForKnowledge.wordpress.com/ ########
———————————————————————————————

Posted in Active Directory Federation Services (ADFS), Configuration, PowerShell, Tooling/Scripting | Leave a Comment »

(2016-05-29) FIM PowerShell CMDlets Have Been Updated

Posted by Jorge on 2016-05-29


Brain Desmond has updated the FIM PowerShell Modules on Codeplex.

Download:

Documentation:

Significant number of enhancements and fixes, including:

New Cmdlets

  • New-FimEmailTemplate
  • New-FimWorkflowDefinition
  • New-FimManagementPolicyRule
  • New-FimSearchScope
  • New-FimNavigationBarLink
  • Get-FimSynchronizationRuleDependencyTree

Enhancements

  • Add Manual Members Support to New-FimSet
  • Add PassThru Switch Schema Cmdlets
  • AllowAuthorizationException switch on New-FimImportObject
  • Add Mode to Get-FimRequestParameter
  • Support null values for New-FimImportChange
  • Support Constants in Sync Rules in Get-ExportAttributeFlow

Bugs Fixed

  • 1850: FimSyncPowerShellModule Should Not Export Internal Functions
  • 1851: Create-ImportFileFromCSEntry Uses an Unapproved Verb
  • 1852: Update Help Text in FimSyncPowerShellModule to use Approved Names, Domains
  • 1885: Objects Emitted by Module Should have a Custom Type Name
  • 1886: Change New-FimImportChange, New-FimImportObject to use ValidateSet
  • 1888: Decorate New-FimSchemaAttribute with Cmdlet Attributes
  • 1889: Decorate New-FimSchemaObject with Parameter Attributes
  • 1939: Improve PSSnapin Load Error Handling
  • 1941: Update Comment Based Help
  • 1894: New-FimImportChange Does Not Support Numeric Inputs
  • 1896: New-FimImportChange Should Support Guid Values
  • 1897: Hide Progress Bar on Module Calls
  • 1948: Fix ‘DateTime’ $DataType casing on New-FimSchemaAttribute
  • 1951: Output Correct Data Type from New-FimSchemaBinding
  • 1968: SynchronizationRuleParameters typing issue
  • 1973: E2E attribute flow: constant sync rule values
  • 1982: Fixed a bug with OSR-Expression export flows. Added the following info to the output:
    • Allow nulls
    • Initial flow only
    • Is existence test
  • 1991: Get-MetaverseSchema Does Not Triage Boolean Attributes Correctly
  • 1997: If the msidmOutboundIsFilterBased is false, no value are submit to FIM and when you edit the SR with the form, you get a change for this attribute (no intial value to false)
  • 2083: Missing Error Handling in New-FimImportObject

Cheers,
Jorge
———————————————————————————————
* This posting is provided "AS IS" with no warranties and confers no rights!
* Always evaluate/test yourself before using/implementing this!
* DISCLAIMER:
https://jorgequestforknowledge.wordpress.com/disclaimer/
———————————————————————————————
############### Jorge’s Quest For Knowledge #############
#########
http://JorgeQuestForKnowledge.wordpress.com/ ########
———————————————————————————————

Posted in Forefront Identity Manager (FIM) Portal, Forefront Identity Manager (FIM) Sync, PowerShell, Tooling/Scripting, Tools, Tools | Leave a Comment »

(2016-05-09) Notifying Users By E-mail Their Password Is Going To Expire (Update 5)

Posted by Jorge on 2016-05-09


Around 2009 I wrote a blog post about and also wrote a tool to notify users through e-mail when their password was going to expire. You can read all the details about the idea here. Now that tool was very inflexible and because of that I received numerous requests to make it more flexible such as the ability to customize the e-mail message. With this blog post I’m sharing a brand new tool, based upon PowerShell, that will notify users through e-mail when their password is going to expire. So let’s get started in explaining on this works! I did not test all combinations! However, I do expect it to run on any Windows version as long as PowerShell is available. It should also work against any AD version and there is NO dependency on using the AD PowerShell CMDlets!. Everything is done through ADSI to be independent of Windows versions! It will also support PSOs if the DFL is high enough and PSOs are configured!

SYNTAX:

  • <PoSH Script File> –> Runs The Script In Test Mode While NOT Sending Any E-Mails
  • <PoSH Script File> -force:$true –> Runs The Script In DEV (One Mail To Configured Admin) Or TEST (All Mails To Configured Admin) Or PROD (Mails To Users) Mode While Sending Any E-Mails

Please provide feedback through the comments section OR you the contact page

DISCLAIMER (READ THIS!):

  • I wrote this script, therefore I own it. Anyone asking money for it, should NOT be doing that and is basically ripping you off!
  • The script is freeware, you are free to use it and distribute it, but always refer to this website (https://jorgequestforknowledge.wordpress.com/) as the location where you got it.
  • This script is furnished "AS IS". No warranty is expressed or implied!
  • I have NOT tested it in every scenario nor have I tested it against every Windows and/or AD version
  • Always test first in lab environment to see if it meets your needs!
  • Use this script at your own risk!
  • I do not warrant this script to be fit for any purpose, use or environment!
  • I have tried to check everything that needed to be checked, but I do not guarantee the script does not have bugs!
  • I do not guarantee the script will not damage or destroy your system(s), environment or whatever!
  • I do not accept liability in any way if you screw up, use the script wrong or in any other way where damage is caused to your environment/systems!
  • If you do not accept these terms do not use the script in any way and delete it immediately!

REMARKS (READ THIS!):

  • The script requires PowerShell v2.0 at a minimum
  • This script must be able to read the contents of the PSO container in every AD domain the script will target!. By default only Domain Admins can read this.
  • It is therefore needed to delegate those permissions to the account executing this PoSH script.
  • For more information about this see the blog post: https://jorgequestforknowledge.wordpress.com/2007/08/09/windows-server-2008-fine-grained-password-policies/
  • DSACLS "\\<Some RWDC>\CN=Password Settings Container,CN=System,<Your AD domain DN>" /G "<Some Security Principal>:GR" /I:T
    • This assign <Some Security Principal> with Allow:Read on the Password Settings Container including its descendant objects

The tool uses an XML called "AD-Pwd-Exp-Notify.xml". It is pre-filled with examples from my test/demo environment. Make sure to change as needed to accommodate your own environment and requirements!

The script has four execution modes. When NOT running the PowerShell script with the ‘-force’ parameter, it will by default run in TEST mode without sending any e-mail to users regarding password expiry ("TEST (NO MAILINGS)"), no matter what the configuration in the XML files specifies. When running the PowerShell script with the ‘-force’ parameter, it will look in the XML file to see which execution mode to run in. When "DEV" is specified it will only send 1 mail to the SMTP address of the admin user specified in the "toSMTPAddressInTestMode" configuration field. This mode allows you to develop the solution being swamped in e-mails or impacting your users. When "TEST" is specified it will only send all mails to the SMTP address of the admin user specified in the "toSMTPAddressInTestMode" configuration field. This mode allows you to see/experience what your scoped/targeted users would see/experience without actually impacting them. When "PROD" is specified it will only send all mails to the SMTP address of the individual users. This really sends the e-mails to all the scoped/targeted individual users.

<!– Execution Mode: DEV (1 Mail To Admin User) or TEST (All Mails To Admin User) or PROD (All Mails To Individual Users) –>

<executionMode>DEV</executionMode>

The PowerShell script sends e-mail, therefore it requires a FROM e-mail address

<!– The SMTP Address Used In The FROM Field –>

<mailFromSender>general.DO-NOT-REPLY@iamtec.nl</mailFromSender>

To develop the solution and test it you can specify an SMTP address that will be used to send e-mails to, without impacting the real user community. That SMTP address will also be used for notifications is the SMTP server or DC is unavailable.

<!– The SMTP Address Used When Running In DEV/TEST Mode And Also Used For Notifications –>

<toSMTPAddressInTestMode>adm.root@iamtec.nl</toSMTPAddressInTestMode>

The PowerShell script sends e-mail, therefore it requires an SMTP server. A test connection to the SMTP server is made. If it fails the script aborts!

<!– FQDN Of The Mail Server Or Mail Relay –>

<smtpServer>MAIL.IAMTEC.NET</smtpServer>

The priority of the mail send can be configured as Low, Normal or High

<!– The Priority Of The Message: Low, Normal, High –>

<mailPriority>High</mailPriority>

The script supports multi-lingual messages. You must always specify a default language and for each language you must also specify a mail subject and the full path to HTML body file that contains the text in a specific language. Both the subject and the body support variables that can be replaced by the actual values. The script contains an example for US (English) and the same example for NL (Dutch).

<!– The File With The HTML Body Text For A Specific Language And The Subject. Supported Variables: FIRST_NAME, LAST_NAME, DISPLAY_NAME, FQDN_DOMAIN, PWD_EXPIRE_IN_NUM_DAYS, PWD_EXPIRY_DATE, PWD_MIN_LENGTH, PWD_MIN_AGE, PWD_MAX_AGE, PWD_HISTORY, PWD_COMPLEX, PWD_CHANGE_RESET_URL  –>
<htmlBodyFiles>
    <htmlBodyFile language="default" mailSubject="Expiring Password In Approx. PWD_EXPIRE_IN_NUM_DAYS Days – Change Your Password As Soon As Possible!" fullPath="D:\TEMP\ADPwdExpNotifyMessageBody_US.html" />
    <htmlBodyFile language="US" mailSubject="Expiring Password In Approx. PWD_EXPIRE_IN_NUM_DAYS Days – Change Your Password As Soon As Possible!" fullPath="D:\TEMP\ADPwdExpNotifyMessageBody_US.html" />
    <htmlBodyFile language="NL" mailSubject="Verlopen Wachtwoord In Ongeveer PWD_EXPIRE_IN_NUM_DAYS Dagen – Wijzig Uw Wachtwoord Zo Snel Als Mogelijk!" fullPath="D:\TEMP\ADPwdExpNotifyMessageBody_NL.html" />
</htmlBodyFiles>

If you have a web portal (e.g. FIM SSPR or Exchange Change Password) to change and/or reset the password, then you can specify it here

<!– The URL Where The Users Can Change Or Reset Their Password –>

<pwdChangeOrResetURL>https://ssprportal.iamtec.net:447/</pwdChangeOrResetURL>

Logging to screen can be enabled (ON) or disabled (OFF).

<!– Enable/Disable Logging To Screen: ON or OFF –>

<logToScreen>ON</logToScreen>

Logging tofile can be enabled (ON) or disabled (OFF).

<!– Enable/Disable Logging To A Log File: ON or OFF –>

<logToFile>ON</logToFile>

If logging is enabled, then you must specify the full path to the log file that will be used. The script itself will take date and time into account

<!– Full Path Of The Log File (.LOG Extension!) –>

<fullPathToLogFile>D:\TEMP\ADPwdExpNotify.log</fullPathToLogFile>

To make sure the disk is not swamped with a huge number of log files, you can specify for how many days the script will keep log files. Every log file older than the specified number will be deleted

<!– Number Of Days To Keep LOG Files –>

<numDaysLOGToKeep>30</numDaysLOGToKeep>

When enabled (ON), the script will export the information of users to a CSV file for troubleshooting and analyses. When disabled (OFF) nothing is exported.

<!– Enable/Disable Export Of Notified Accounts To A CSV File: ON or OFF –>

<exportToCSV>ON</exportToCSV>

If exporting is enabled, then you must specify the full path to the CSV file that will be used. The script itself will take date and time into account

<!– Full Path Of The CSV File (.CSV Extension!) –>

<fullPathToCSVFile>D:\TEMP\ADPwdExpNotify.csv</fullPathToCSVFile>

To make sure the disk is not swamped with a huge number of CSV files, you can specify for how many days the script will keep CSV files. Every CSV file older than the specified number will be deleted

<!– Number Of Days To Keep CSV Files –>

<numDaysCSVToKeep>30</numDaysCSVToKeep>

In the XML config file you can specify the date/time format to be used on screen, in the log, in the CSV and in the E-mail message

<!– Date And Time Format To Use On Screen, In Logs And In E-mail Message –>
<formatDateTime>yyyy-MM-dd HH:mm:ss</formatDateTime>

In this section you specify every AD domain in the AD forest, including trusting AD domains, for which its scoped/targeted users must be notified. For every AD domain, you then need to specify if an RWDC needs to be discovered (specifiy: DISCOVER) or you can specifically mention an RWDC that must be targeted. A test connection to the RWDC is made. If it fails the AD domain for that RWDC is fully skipped! Then for every AD domain, specify one or more search bases to scope/target users in the LDAP query. For every search base configure the language for the scoped/targeted users. Make sure that if you specify specific languages, that you also have configured the HTML Body File for that language

<!– Targeted Domains, Specify DISCOVER To Discover A DC Or Use Specific DC And Search Bases Per Domain –>
<!– WARNING: Make Sure The Search Bases DO NOT Overlap Each Other!!! –>
<domains>
    <domain FQDN="IAMTEC.NET" DC="DISCOVER">
        <searchBase nr="1" language="default">OU=Users,OU=EMPLOYEES,OU=Org-Users,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="2" language="US">OU=Users,OU=CONTRACTORS,OU=Org-Users,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="3" language="US">OU=Users,OU=CONTRACTORZZZ,OU=Org-Users,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="4" language="NL">OU=Users,OU=HISTORY1,OU=Org-Users,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="5" language="NL">OU=Users,OU=HISTORY2,OU=Org-Users,DC=IAMTEC,DC=NET</searchBase>
    </domain>
    <domain FQDN="CHILD.IAMTEC.NET" DC="C1FSRWDC1.CHILD.IAMTEC.NET">
        <searchBase nr="1" language="default">OU=Users,OU=EMPLOYEES,OU=Org-Users,DC=CHILD,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="2" language="US">OU=Users,OU=CONTRACTORS,OU=Org-Users,DC=CHILD,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="3" language="US">OU=Users,OU=CONTRACTORZZZ,OU=Org-Users,DC=CHILD,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="4" language="NL">OU=Users,OU=HISTORY1,OU=Org-Users,DC=CHILD,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="5" language="NL">OU=Users,OU=HISTORY2,OU=Org-Users,DC=CHILD,DC=IAMTEC,DC=NET</searchBase>
    </domain>
</domains>

In this section you can specify one or multiple periods of notifications. Make sure that none of the periods overlaps any other period!. In the example shown, the scoped/targeted user will receive 4 notifications assuming the script executes once every day. Also make sure the periods configured are in balance with the maximum password age!

<!– Number Of Days Before The Password Expires To Send Notifications –>
<!– WARNING: Make Sure The Periods DO NOT Overlap Each Other!!! –>
<daysBeforeWarn>
    <period nr="1" Max="10" Min="9" />
    <period nr="2" Max="5" Min="4" />
    <period nr="3" Max="2" Min="0" />
</daysBeforeWarn>

image_thumb26_thumb_thumb_thumb

Figure 1a: XML Configuration File

image_thumb29_thumb_thumb_thumb

Figure 1b: XML Configuration File

Executing The Script

Executing the script to run in "DEV", "TEST" or "PROD" mode (whatever is configured in the XML configuration file) while using the default location of the XML configuration file (same folder as script)

.\AD-Pwd-Exp-Notify_v018.ps1 -force

Executing the script to run in "TEST (NO MAILINGS)" mode while using the default location of the XML configuration file (same folder as script)

.\AD-Pwd-Exp-Notify_v018.ps1

Executing the script to run in "TEST (NO MAILINGS)" mode while using a custom location of the XML configuration file

.\AD-Pwd-Exp-Notify_v018.ps1 -xmlconfigfilepath D:\TEMP\AD-Pwd-Exp-Notify.xml

Executing the script to run in "DEV", "TEST" or "PROD" mode (whatever is configured in the XML configuration file) while using custom location of the XML configuration file

.\AD-Pwd-Exp-Notify_v018.ps1 -xmlconfigfilepath D:\TEMP\AD-Pwd-Exp-Notify.xml -force

Example Output Of The Script (On Screen)

image_thumb32_thumb_thumb_thumb

Figure 2a: Output To Screen

image_thumb35_thumb_thumb_thumb

Figure 2b: Output To Screen

image_thumb38_thumb_thumb_thumb

Figure 2c: Output To Screen

image_thumb41_thumb_thumb_thumb

Figure 2d: Output To Screen

image_thumb44_thumb_thumb_thumb

Figure 2e: Output To Screen

Example Output Of The Script (Log File)

See zip file

Example Output Of The Script (CSV file)

See zip file

E-mail Message For US English Language

image_thumb48_thumb_thumb_thumb

Figure 3a: E-Mail Notification In English

image_thumb51_thumb_thumb_thumb

Figure 3b: E-Mail Notification In English

E-mail Message For Dutch Language

image_thumb55_thumb_thumb_thumb

Figure 4a: E-Mail Notification In Dutch

image_thumb58_thumb_thumb_thumb

Figure 4b: E-Mail Notification In Dutch

And Finally….The PowerShell Script Itself

Version of version: v0.19

Date of script: 2016-05-09

### Abstract: This PoSH Script Notifies Mailbox Enabled Users For Which The Password Will Expires Within A Specific Number Of Days ### Written by: Jorge de Almeida Pinto [MVP-DS] ### BLOG: https://jorgequestforknowledge.wordpress.com/ ### ### 2015-03-21: Initial version of the script in PowerShell (v0.13) ### 2015-03-26: Bug fixes regarding some attributes not having values (v0.14) ### 2015-03-27: Supporting date/time format in XML and incorrect variable being used to get the correct password policy ### settings (v0.15) ### 2015-04-29: Bug fixes regarding the default domain GPO getting no name when no PSOs are used or inheriting the name ### of the last processed PSO, the displayName of the development user not being processed correctly, and more ### enhanced error detection when discovering a DC for non-existing AD domain, and better explanation and ### information about the parameters and the script itself (v0.16) ### 2015-09-22: Bug fixes regarding specifying the script version and date after the parameter section (v0.17) ### 2015-10-13: Resolved a bug with regards to paging when searching (v0.18) ### 2016-05-08: Resolved a bug with regards to array definition of the password policies in a domain ($pwdPolicyInDomain), ### and testing the existence of the language files (v0.19) ### <# .SYNOPSIS This PoSH Script Notifies Mailbox Enabled Users For Which The Password Will Expires Within A Specific Number Of Days .DESCRIPTION This PoSH script notifies mailbox enabled users for which the password will expires within a specific number of days. The configuration of the script is done through an XML file. The tool uses an XML called "AD-Pwd-Exp-Notify.xml". It is pre-filled with examples from my test/demo environment. Make sure to change as needed to accommodate your own environment and requirements! For detailed information about all configurable options see the sample XML file or browse to: https://jorgequestforknowledge.wordpress.com/2015/03/24/notifying-users-by-e-mail-their-password-is-going-to-expire-update-1/ .PARAMETER force Runs the script in whatever mode is configured in the XML file (e.g. "DEV", "TEST" or "PROD" mode) .PARAMETER xmlconfigfilepath Allows to use a custom location and custom XML file instead of the default XML file in the default location (same folder as the script) .EXAMPLE Executing the script to run in "TEST (NO MAILINGS)" mode while using the default location of the XML configuration file (same folder as script) AD-Pwd-Exp-Notify_vXXX.ps1 .EXAMPLE Executing the script to run in "DEV", "TEST" or "PROD" mode (whatever is configured in the XML configuration file) while using the default location of the XML configuration file (same folder as script) AD-Pwd-Exp-Notify_vXXX.ps1 -force .EXAMPLE Executing the script to run in "TEST (NO MAILINGS)" mode while using a custom location of the XML configuration file AD-Pwd-Exp-Notify_vXXX.ps1 -xmlconfigfilepath D:\TEMP\AD-Pwd-Exp-Notify.xml .EXAMPLE Executing the script to run in "DEV", "TEST" or "PROD" mode (whatever is configured in the XML configuration file) while using custom location of the XML configuration file AD-Pwd-Exp-Notify_vXXX.ps1 -xmlconfigfilepath D:\TEMP\AD-Pwd-Exp-Notify.xml -force .NOTES -->> DISCLAIMER <<-- * I wrote this script, therefore I own it. Anyone asking money for it, should NOT be doing that and is basically ripping you off! * The script is freeware, you are free to use it and distribute it, but always refer to this website (https://jorgequestforknowledge.wordpress.com/) as the location where you got it. * This script is furnished "AS IS". No warranty is expressed or implied! * I have NOT tested it in every scenario nor have I tested it against every Windows and/or AD version * Always test first in lab environment to see if it meets your needs! * Use this script at your own risk! * I do not warrant this script to be fit for any purpose, use or environment! * I have tried to check everything that needed to be checked, but I do not guarantee the script does not have bugs! * I do not guarantee the script will not damage or destroy your system(s), environment or whatever! * I do not accept liability in any way if you screw up, use the script wrong or in any other way where damage is caused to your environment/systems! * If you do not accept these terms do not use the script in any way and delete it immediately! -->> REMARKS <<-- * The script requires PowerShell v2.0 at a minimum * This script must be able to read the contents of the PSO container in every AD domain the script will target!. By default only Domain Admins can read this. * It is therefore needed to delegate those permissions to the account executing this PoSH script. * For more information about this see the blog post: https://jorgequestforknowledge.wordpress.com/2007/08/09/windows-server-2008-fine-grained-password-policies/ * DSACLS "\\<Some RWDC>\CN=Password Settings Container,CN=System,<Your AD domain DN>" /G "<Some Security Principal>:GR" /I:T >> This assigns <Some Security Principal> with Allow:Read on the Password Settings Container including its descendant objects #> Param( [Parameter(Mandatory=$false)] [string]$xmlconfigfilepath, [Parameter(Mandatory=$false)] [switch]$force ) $scriptVersion = "v0.19" $scriptDate = "2016-05-08" ################################################################################################## #################################### SCRIPT FUNCTIONS START ###################################### ################################################################################################## ################################################################################################## ### FUNCTION: Logging Data To The Log File Function Logging($dataToLog) { $datetimeLogLine = "[" + $(Get-Date -format $formatDateTime) + "] : " If ($logToFile.ToUpper() -eq "ON") { Out-File -filepath "$fullPathToLogFile" -append -inputObject "$datetimeLogLine$dataToLog" } If ($logToScreen.ToUpper() -eq "ON") { Write-Output($datetimeLogLine + $dataToLog) } } ################################################################################################## ### FUNCTION: Cleaning Up Old Log Files Function CleanUpLOGFiles($numDaysLOGToKeep) { $regExPatternLogFile = '^.*_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}.log$' $oldLogFiles = Get-ChildItem -Path $folderLogFile\*.log | ?{$_.Name -match $regExPatternLogFile} $oldLogFilesToDelete = $oldLogFiles | ?{$_.lastwritetime -lt (Get-Date $execStartDateTime).addDays(-$numDaysLOGToKeep) -and -not $_.psiscontainer} $oldLogFilesToDelete | %{Remove-Item $_.FullName -force} Return ($oldLogFilesToDelete | Measure-Object).Count } ################################################################################################## ### FUNCTION: Cleaning Up Old Csv Files Function CleanUpCSVFiles($numDaysCSVToKeep) { $regExPatternCsvFile = '^.*_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}.csv$' $oldCsvFiles = Get-ChildItem -Path $folderCsvFile\*.csv | ?{$_.Name -match $regExPatternCsvFile} $oldCsvFilesToDelete = $oldCsvFiles | ?{$_.lastwritetime -lt (Get-Date $execStartDateTime).addDays(-$numDaysCSVToKeep) -and -not $_.psiscontainer} $oldCsvFilesToDelete | %{Remove-Item $_.FullName -force} Return ($oldCsvFilesToDelete | Measure-Object).Count } ################################################################################################## ### FUNCTION: Discover An RWDC From An AD Domain Function DiscoverRWDC($fqdnADdomain) { $contextADDomain = $NULL $contextADDomain = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext("Domain",$fqdnADdomain) $dnsHostNameRWDC = $NULL $ErrorActionPreference = "SilentlyContinue" $dnsHostNameRWDC = ([System.DirectoryServices.ActiveDirectory.DomainController]::findone($contextADDomain)).Name $ErrorActionPreference = "Continue" If ($dnsHostNameRWDC -eq $null) { Return "DOMAIN_DOES_NOT_EXIST_OR_CANNOT_FIND_DC" } Else { Return $dnsHostNameRWDC } } ################################################################################################## ### FUNCTION: Check If An OU/Container Exists Function CheckDNExistence($dnsHostNameRWDC,$DN) { Try { If([ADSI]::Exists("LDAP://$dnsHostNameRWDC/$DN")) { Return "SUCCESS" } Else { Return "ERROR" } ` } Catch { Return "ERROR" } } ################################################################################################## ### FUNCTION: Test Connection To A Server Function TestConnectionToServer($dnsHostName,$port) { $tcpPortSocket = New-Object System.Net.Sockets.TcpClient $timeOut = "500" $portConnect = $tcpPortSocket.BeginConnect($dnsHostName,$port,$null,$null) $tcpPortWait = $portConnect.AsyncWaitHandle.WaitOne($timeOut,$false) If(!$tcpPortWait) { $tcpPortSocket.Close() Return "ERROR" } Else { $ErrorActionPreference = "SilentlyContinue" $tcpPortSocket.EndConnect($portConnect) | Out-Null If (!$?) { Return "ERROR" } Else { Return "SUCCESS" } $tcpPortSocket.Close() $ErrorActionPreference = "Continue" } } ################################################################################################## ### FUNCTION: Decode Functional Level Function DecodeFunctionalLevel($dfl) { Switch ($dfl) { 0 {"Windows 2000"} 1 {"Windows 2003 Interim"} 2 {"Windows 2003"} 3 {"Windows 2008"} 4 {"Windows 2008 R2"} 5 {"Windows 2012"} 6 {"Windows 2012 R2"} #7 {"TBD"} #8 {"TBD"} #9 {"TBD"} #10 {"TBD"} Default {"If You See This, Something Is Wrong!"} } } ################################################################################################## ##################################### SCRIPT FUNCTIONS END ####################################### ################################################################################################## ### Clear The Screen Clear-Host ### Configure The Appropriate Screen And Buffer Size To Make Sure Everything Fits Nicely $uiConfig = (Get-Host).UI.RawUI $uiConfig.WindowTitle = "+++ AD PASSWORD EXPIRY NOTIFICATION +++" $uiConfig.ForegroundColor = "Yellow" $uiConfigBufferSize = $uiConfig.BufferSize $uiConfigBufferSize.Width = 500 $uiConfigBufferSize.Height = 9999 $uiConfigScreenSizeMax = $uiConfig.MaxPhysicalWindowSize $uiConfigScreenSizeMaxWidth = $uiConfigScreenSizeMax.Width $uiConfigScreenSizeMaxHeight = $uiConfigScreenSizeMax.Height $uiConfigScreenSize = $uiConfig.WindowSize If ($uiConfigScreenSizeMaxWidth -lt 160) { $uiConfigScreenSize.Width = $uiConfigScreenSizeMaxWidth } Else { $uiConfigScreenSize.Width = 160 } If ($uiConfigScreenSizeMaxHeight -lt 75) { $uiConfigScreenSize.Height = $uiConfigScreenSizeMaxHeight - 5 } Else { $uiConfigScreenSize.Height = 75 } $uiConfig.BufferSize = $uiConfigBufferSize $uiConfig.WindowSize = $uiConfigScreenSize ### Script Configuration File If ($xmlconfigfilepath -eq $null -or $xmlconfigfilepath -eq "") { $currentScriptFolderPath = Split-Path $MyInvocation.MyCommand.Definition [string]$scriptXMLConfigFilePath = Join-Path $currentScriptFolderPath "AD-Pwd-Exp-Notify.xml" } Else { [string]$scriptXMLConfigFilePath = $xmlconfigfilepath } ### Start Time Of Script In UTC $execStartDateTime = (Get-Date -format $formatDateTime) $execStartDateTimeForFileSystem = (Get-Date $execStartDateTime -format "yyyy-MM-dd_HH-mm-ss") ### Read The Config File If (!(Test-Path $scriptXMLConfigFilePath)) { Write-Host "The XML Config File '$scriptXMLConfigFilePath' CANNOT Be Found!..." -ForeGroundColor Red Write-Host "Aborting Script..." -ForeGroundColor Red EXIT } Else { [XML]$global:configADPwdExpNotify = Get-Content $scriptXMLConfigFilePath #Write-Host "The XML Config File '$scriptXMLConfigFilePath' Has Been Found!..." -ForeGroundColor Green #Write-Host "Continuing Script..." -ForeGroundColor Green #Write-Host "" } ### Read The Properties From The XML Config File $executionMode = $configADPwdExpNotify.ADPwdExpNotifyConfig.executionMode $mailFromSender = $configADPwdExpNotify.ADPwdExpNotifyConfig.mailFromSender $toSMTPAddressInTestMode = $configADPwdExpNotify.ADPwdExpNotifyConfig.toSMTPAddressInTestMode $smtpServer = $configADPwdExpNotify.ADPwdExpNotifyConfig.smtpServer $mailPriority = $configADPwdExpNotify.ADPwdExpNotifyConfig.mailPriority $mailSubject = $configADPwdExpNotify.ADPwdExpNotifyConfig.mailSubject $htmlBodyFiles = $configADPwdExpNotify.ADPwdExpNotifyConfig.htmlBodyFiles.htmlBodyFile $pwdChangeOrResetURL = $configADPwdExpNotify.ADPwdExpNotifyConfig.pwdChangeOrResetURL $logToScreen = $configADPwdExpNotify.ADPwdExpNotifyConfig.logToScreen $logToFile = $configADPwdExpNotify.ADPwdExpNotifyConfig.logToFile $fullPathToLogFile = $configADPwdExpNotify.ADPwdExpNotifyConfig.fullPathToLogFile -replace ".log","_$execStartDateTimeForFileSystem.log" $folderLogFile = Split-Path $fullPathToLogFile $numDaysLOGToKeep = $configADPwdExpNotify.ADPwdExpNotifyConfig.numDaysLOGToKeep $exportToCSV = $configADPwdExpNotify.ADPwdExpNotifyConfig.exportToCSV $fullPathToCSVFile = $configADPwdExpNotify.ADPwdExpNotifyConfig.fullPathToCSVFile -replace ".csv","_$execStartDateTimeForFileSystem.csv" $folderCsvFile = Split-Path $fullPathToCSVFile $numDaysCSVToKeep = $configADPwdExpNotify.ADPwdExpNotifyConfig.numDaysCSVToKeep $formatDateTime = $configADPwdExpNotify.ADPwdExpNotifyConfig.formatDateTime $domains = $configADPwdExpNotify.ADPwdExpNotifyConfig.domains.domain $daysBeforeWarn = $configADPwdExpNotify.ADPwdExpNotifyConfig.daysBeforeWarn.Period Logging "#######################################################################################" Logging " *********************************************************" Logging " * *" Logging " * --> ACTIVE DIRECTORY PASSWORD EXPIRY NOTIFICATION <-- *" Logging " * *" Logging " * ($scriptVersion) ($scriptDate) *" Logging " * *" Logging " * Written By: Jorge de Almeida Pinto [MVP-DS] *" Logging " * *" Logging " * BLOG: 'Jorge's Quest For Knowledge' *" Logging " * (https://jorgequestforknowledge.wordpress.com/) *" Logging " * *" Logging " *********************************************************" Logging "" Logging "Starting Date And Time...........: $execStartDateTime" Logging "" $htmlBodyFiles | %{ $htmlBodyFileFullPath = $_.fullPath If (!(Test-Path $htmlBodyFileFullPath)) { Write-Host "" Write-Host "The Language File '$htmlBodyFileFullPath' Does Not Exist!..." -Foregroundcolor Red Write-Host "Aborting Script!..." -Foregroundcolor Red Write-Host "" EXIT } } Logging "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" ### Logging All Configured Settings Logging "" Logging "XML Config File Path.............: $scriptXMLConfigFilePath" Logging "" If (!$force) { $executionMode = "TEST (NO MAILINGS)" } Logging "Execution Mode...................: $executionMode" Logging "" Logging "Log To Screen....................: $logToScreen" Logging "" Logging "Log To File......................: $logToFile" Logging "" Logging "Log File Full Path...............: $fullPathToLogFile" Logging "" Logging "Log Files Folder.................: $folderLogFile" Logging "" Logging "Number Of Days Of Logs To Keep...: $numDaysLOGToKeep" Logging "" Logging "Export List Of User To CSV.......: $exportToCSV" Logging "" Logging "CSV File Full Path...............: $fullPathToCSVFile" Logging "" Logging "CSV Files Folder.................: $folderCsvFile" Logging "" Logging "Number Of Days Of CSVs To Keep...: $numDaysCSVToKeep" Logging "" $smtpServerStatus = $NULL $smtpServerStatus = TestConnectionToServer $smtpServer "25" Logging "SMTP Server......................: $smtpServer (Status: $smtpServerStatus)" Logging "" If ($smtpServerStatus.ToUpper() -eq "ERROR") { EXIT } Logging "Sender Address...................: $mailFromSender" Logging "" If ($executionMode.ToUpper() -eq "TEST (NO MAILINGS)") { Logging "Recipient Address................: None" } If ($executionMode.ToUpper() -eq "DEV" -Or $executionMode.ToUpper() -eq "TEST") { Logging "Recipient Address................: $toSMTPAddressInTestMode" } If ($executionMode.ToUpper() -eq "PROD") { Logging "Recipient Address................: Individual Users" } Logging "" Logging "Message Priority.................: $mailPriority" $htmlBodyFiles | %{ $language = $_.language $mailSubject = $_.mailSubject $fullPath = $_.fullPath Logging "" Logging "Message Subject..................: ($language) $mailSubject" Logging "" Logging "HTML Body File...................: ($language) $fullPath" } Logging "" Logging "Change/Reset PWD URL.............: $pwdChangeOrResetURL" Logging "" Logging "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" ### Cleaning Up Old Log and Csv Files Logging "" Logging "Cleaning Up Old Log Files. Keeping Log Files From Last $numDaysLOGToKeep Days..." $oldLogFilesToDeleteCount = CleanUpLOGFiles $numDaysLOGToKeep Logging " --> Number Of Old Log Files Deleted...: $oldLogFilesToDeleteCount" Logging "" Logging "Cleaning Up Old Csv Files. Keeping Csv Files From Last $numDaysCSVToKeep Days..." $oldCsvFilesToDeleteCount = CleanUpCSVFiles $numDaysCSVToKeep Logging " --> Number Of Csv Log Files Deleted...: $oldCsvFilesToDeleteCount" Logging "" Logging "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" ### Creating An Empty Array For All Queried Users (Not Necessarily The Same List Of Users That Will Be Notified!) $listOfQueriedUsers = @() ### Processing Each Configured AD Domain In The XML Config File Logging "" Logging "Processing Configured AD Domains..." ### Go Through Every Configured AD Domain $domains | %{ ### The FQDN Of The AD Domain From The XML Config File $fqdnADdomain = $_.FQDN Logging "" Logging "** AD Domain: $fqdnADdomain **" ### The FQDN Of The DC From The XML Config File $fqdnDC = $_.DC ### If DISCOVER Was Specified Instead Of A Specific (Static) DC, Then Discover The Nearest RWDC And Use That One If ($fqdnDC.ToUpper() -eq "DISCOVER") { $fqdnDC = DiscoverRWDC $fqdnADdomain ### Check If The RWDC Is Available If ($fqdnDC -eq "DOMAIN_DOES_NOT_EXIST_OR_CANNOT_FIND_DC") { $dcStatus = "ERROR" Logging "" Logging " --> FQDN DC: $fqdnDC (Discovered) (Status: $dcStatus)" } Else { $dcStatus = $NULL $dcStatus = TestConnectionToServer $fqdnDC "389" Logging "" Logging " --> FQDN DC: $fqdnDC (Discovered) (Status: $dcStatus)" } } Else { ### Check If The RWDC Is Available $dcStatus = $NULL $dcStatus = TestConnectionToServer $fqdnDC "389" Logging "" Logging " --> FQDN DC: $fqdnDC (Static) (Status: $dcStatus)" } ### If There Is Something Wrong With The RWDC, Then Abort Processing For This AD Domain And Send Mail About It If ($dcStatus -eq "ERROR" -And $fqdnDC -ne "DOMAIN_DOES_NOT_EXIST_OR_CANNOT_FIND_DC") { Logging " --> SKIPPED DUE TO ERROR - UNABLE TO CONTACT DC!" Logging "" Send-MailMessage -SmtpServer $smtpServer -From $mailFromSender -To $toSMTPAddressInTestMode -Priority High -Subject "Error With DC From AD Domain!" -Body "There Are Connectivity Issues With The DC '$fqdnDC' From The AD Domain '$fqdnADdomain'!" -BodyAsHtml } If ($dcStatus -eq "ERROR" -And $fqdnDC -eq "DOMAIN_DOES_NOT_EXIST_OR_CANNOT_FIND_DC") { Logging " --> SKIPPED DUE TO ERROR - DOMAIN DOES NOT EXIST OR CANNOT FIND DC!" Logging "" Send-MailMessage -SmtpServer $smtpServer -From $mailFromSender -To $toSMTPAddressInTestMode -Priority High -Subject "Error With DC From AD Domain!" -Body "The AD Domain '$fqdnADdomain' Does Not Exist Or Unable To Discover A DC For The AD Domain '$fqdnADdomain'!" -BodyAsHtml } ### If The AD Domain Does Exist And The DC Can Be Discovered And It Can Be Contacted If ($dcStatus -eq "SUCCESS") { ### Array Definition Of All Password Policies In A Domain $pwdPolicyInDomain = @() ### Connect To The RootDSE Of The RWDC And Get Info From It $RootDSE = [ADSI]"LDAP://$fqdnDC/RootDSE" $dfl = $RootDSE.domainFunctionality $defaultNC = $RootDSE.defaultNamingContext Logging "" Logging " --> DFL: $dfl ($(DecodeFunctionalLevel $dfl))" Logging "" Logging " --> Default NC: $defaultNC" ### PWD Policies From The AD Domain ### If Domain Functional Level Is At Least 3 (Windows 2008) Or Higher Then Check For Any Configured Password Settings Object (PSO) And Get The Settings For Each PSO If ($dfl -ge 3) { Logging "" Logging " --> PSOs In AD Domain" ### PSO Container (REMEMBER: The Account Running This Script Must Have Allow:Read Permissions On The PSO Container Itself And Sub Objects $psoContainerDN = "CN=Password Settings Container,CN=System,$defaultNC" ### Setup The LDAP Query To Get All PSOs And Execute The Query $searchRoot = $NULL $searchRoot = [ADSI]"LDAP://$fqdnDC/$psoContainerDN" $searcher = New-Object System.DirectoryServices.DirectorySearcher($searchRoot) $searcher.Filter = "(objectClass=msDS-PasswordSettings)" $searcher.SearchScope = "Subtree" $searcher.PageSize = 1000 $propertyList = "distinguishedName","name","msDS-MaximumPasswordAge","msDS-MinimumPasswordAge","msDS-MinimumPasswordLength","msDS-PasswordComplexityEnabled","msDS-PasswordHistoryLength" ForEach ($property in $propertyList){ $searcher.PropertiesToLoad.Add($property) | Out-Null } $results = $NULL $results = $searcher.FindAll() ### For Every Discovered PSO Get Its Properties (REMEMBER: The Account Running This Script Must Have Allow:Read Permissions On The PSO Container Itself And Sub Objects If ($results -ne $null) { $results | %{ $pwdPolicyPSOInDomainObj = "" | Select DN,name,MaxPwdAge,MinPwdAge,MinPwdLength,PwdComplexity,PwdHistoryLength $pwdPolicyPSOInDomainObj.DN = $_.Properties.distinguishedname[0] $psoName = $_.Properties.name[0] $pwdPolicyPSOInDomainObj.name = $($psoName + " (" + $fqdnADdomain + ")") Logging "" Logging " --> Name............: $psoName" $psoMaxPwdAge = [System.TimeSpan]::FromTicks([System.Math]::ABS($_.Properties."msds-maximumpasswordage"[0])).Days $pwdPolicyPSOInDomainObj.MaxPwdAge = $psoMaxPwdAge Logging " --> Max Pwd Age.....: $psoMaxPwdAge" $psoMinPwdAge = [System.TimeSpan]::FromTicks([System.Math]::ABS($_.Properties."msds-minimumpasswordage"[0])).Days $pwdPolicyPSOInDomainObj.MinPwdAge = $psoMinPwdAge Logging " --> Min Pwd Age.....: $psoMinPwdAge" $psoMinPwdLength = $_.Properties."msds-minimumpasswordlength"[0] $pwdPolicyPSOInDomainObj.MinPwdLength = $psoMinPwdLength Logging " --> Min Pwd Length..: $psoMinPwdLength" If ($_.Properties."msds-passwordcomplexityenabled"[0]) { $pwdPolicyPSOInDomainObj.PwdComplexity = "TRUE" Logging " --> Pwd Complexity..: TRUE" } Else { $pwdPolicyPSOInDomainObj.PwdComplexity = "FALSE" Logging " --> Pwd Complexity..: FALSE" } $psoPwdHistoryLength = $_.Properties."msds-passwordhistorylength"[0] $pwdPolicyPSOInDomainObj.PwdHistoryLength = $psoPwdHistoryLength Logging " --> Pwd Complexity..: $psoPwdHistoryLength" $pwdPolicyInDomain += $pwdPolicyPSOInDomainObj } } $searcher = $NULL $results = $NULL } ### Get The Password Policy Settings From The Default Domain GPO Which Are Also Registered On The AD Domain NC Head Logging "" Logging " --> Default Domain GPO Password Settings" ### Setup The LDAP Query To Get The Password Policy Settings From The Default Domain GPO And Execute The Query $searchRoot = $NULL $searchRoot = [ADSI]"LDAP://$fqdnDC/$defaultNC" $searcher = New-Object System.DirectoryServices.DirectorySearcher($searchRoot) $searcher.SearchScope = "Base" $propertyList = "maxPwdAge","minPwdAge","minPwdLength","pwdProperties","pwdHistoryLength" ForEach ($property in $propertyList){ $searcher.PropertiesToLoad.Add($property) | Out-Null } $results = $NULL $results = $searcher.FindOne() ### Get The Properties And Process Them $results | %{ $pwdPolicyGPOInDomainObj = "" | Select DN,name,MaxPwdAge,MinPwdAge,MinPwdLength,PwdComplexity,PwdHistoryLength $pwdPolicyGPOInDomainObj.DN = $defaultNC[0] $gpoName = "DefaultDomainGPO (" + $fqdnADdomain + ")" $pwdPolicyGPOInDomainObj.name = $gpoName Logging "" Logging " --> Name............: $gpoName" $gpoMaxPwdAge = [System.TimeSpan]::FromTicks([System.Math]::ABS($_.Properties.maxpwdage[0])).Days $pwdPolicyGPOInDomainObj.MaxPwdAge = $gpoMaxPwdAge Logging " --> Max Pwd Age.....: $gpoMaxPwdAge" $gpoMinPwdAge = [System.TimeSpan]::FromTicks([System.Math]::ABS($_.Properties.minpwdage[0])).Days $pwdPolicyGPOInDomainObj.MinPwdAge = $gpoMinPwdAge Logging " --> Min Pwd Age.....: $gpoMinPwdAge" $psoMinPwdLength = $_.Properties.minpwdlength[0] $pwdPolicyGPOInDomainObj.MinPwdLength = $psoMinPwdLength Logging " --> Min Pwd Length..: $psoMinPwdLength" If (($results.Properties.pwdproperties[0] -band 0x1) -Eq 1) { $pwdPolicyGPOInDomainObj.PwdComplexity = "TRUE" Logging " --> Pwd Complexity..: TRUE" } Else { $pwdPolicyGPOInDomainObj.PwdComplexity = "FALSE" Logging " --> Pwd Complexity..: FALSE" } $gpoPwdHistoryLength = $_.Properties.pwdhistorylength[0] $pwdPolicyGPOInDomainObj.PwdHistoryLength = $gpoPwdHistoryLength Logging " --> Pwd Complexity..: $gpoPwdHistoryLength" $pwdPolicyInDomain += $pwdPolicyGPOInDomainObj } $searcher = $NULL $results = $NULL Logging "" Logging " --> Search Bases In AD Domain" ### Processing Each Configured Search Base Within An AD Domain In The XML Config File $searchBases = $_.searchBase $searchBases | %{ $searchBase = $_."#text" $languageForUser = $_.language $mailSubjectForUser = ($htmlBodyFiles | ?{$_.language -eq $languageForUser}).mailSubject $htmlBodyFileForUser = ($htmlBodyFiles | ?{$_.language -eq $languageForUser}).fullPath Logging "" ### Let's Make Sure The Configured Search Base Does Exist $searchBaseStatus = $NULL $searchBaseStatus = CheckDNExistence $fqdnDC $searchBase Logging " --> Search Base..........: $searchBase (Status: $searchBaseStatus)" ### If The Search Base Does Exist Then Continue If ($searchBaseStatus -eq "SUCCESS") { ### Setup The LDAP Query To Get The User Objects And Execute The Query $searchRoot = $NULL $searchRoot = [ADSI]"LDAP://$fqdnDC/$searchBase" $searcher = New-Object System.DirectoryServices.DirectorySearcher($searchRoot) $searcher.Filter = "(&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(userAccountControl:1.2.840.113556.1.4.803:=65536))(mail=*)(!(pwdLastSet=0)))" $searcher.SearchScope = "Subtree" $searcher.PageSize = 1000 $propertyList = "distinguishedName","givenName","sn","displayName","mail","pwdLastSet","msDS-UserPasswordExpiryTimeComputed","accountExpires","msDS-ResultantPSO" ForEach ($property in $propertyList){ $searcher.PropertiesToLoad.Add($property) | Out-Null } $results = $NULL $results = $searcher.FindAll() $userCountInSearchBase = ($results | Measure-Object).Count Logging " --> Queried User Count...: $userCountInSearchBase" Logging " --> Specified Language...: $languageForUser" ### Get The Properties And Process Them $results | %{ $listOfQueriedUsersObj = "" | Select "FQDN AD Domain",DN,"Given Name","Last Name","Display Name","E-Mail Address","PWD Last Set","PWD Expire Date","Days Until PWD Expiry","Account Expiry Date","Days Until Account Expiry","Effective PWD Policy","Language","Mail Subject","HTML Body File" $listOfQueriedUsersObj."FQDN AD Domain" = $fqdnADdomain $listOfQueriedUsersObj.DN = $_.Properties.distinguishedname[0] If ($_.Properties.givenname -ne $null) { $listOfQueriedUsersObj."Given Name" = $_.Properties.givenname[0] } Else { $listOfQueriedUsersObj."Given Name" = $null } If ($_.Properties.sn -ne $null) { $listOfQueriedUsersObj."Last Name" = $_.Properties.sn[0] } Else { $listOfQueriedUsersObj."Last Name" = $null } If ($_.Properties.displayname -ne $null) { $listOfQueriedUsersObj."Display Name" = $_.Properties.displayname[0] } Else { $listOfQueriedUsersObj."Display Name" = $null } $listOfQueriedUsersObj."E-Mail Address" = $_.Properties.mail[0] $adUserPwdLastSet = Get-Date -Date ([DateTime]::FromFileTime([Int64]::Parse($_.Properties.pwdlastset[0]))) -Format $formatDateTime $listOfQueriedUsersObj."PWD Last Set" = $adUserPwdLastSet $adUserPwdExpires = Get-Date -Date ([DateTime]::FromFileTime([Int64]::Parse($_.Properties."msds-userpasswordexpirytimecomputed"[0]))) -Format $formatDateTime $listOfQueriedUsersObj."PWD Expire Date" = $adUserPwdExpires $timeDiffPwdExpiryInDays = (New-TimeSpan -Start $execStartDateTime -end $adUserPwdExpires).TotalDays $listOfQueriedUsersObj."Days Until PWD Expiry" = $timeDiffPwdExpiryInDays $adUserAccountExpires = $_.Properties.accountexpires[0] ### If An Account Is Configured With Never Expires, Then Assign An Insane End Date To Be Able To Perform Calculations If ($adUserAccountExpires -eq 9223372036854775807 -Or $adUserAccountExpires -eq 0) { $adUserAccountExpires = Get-Date "9999-12-31 23:59:59" -format $formatDateTime } Else { $adUserAccountExpires = Get-Date -Date ([DateTime]::FromFileTime([Int64]::Parse($adUserAccountExpires))) -Format $formatDateTime } $listOfQueriedUsersObj."Account Expiry Date" = $adUserAccountExpires $timeDiffAccountExpiryInDays = (New-TimeSpan -Start $execStartDateTime -end $adUserAccountExpires).TotalDays $listOfQueriedUsersObj."Days Until Account Expiry" = $timeDiffAccountExpiryInDays If ($_.Properties."msds-resultantpso" -ne $null) { $effectivePWDPolicyDN = $_.Properties."msds-resultantpso"[0] } Else { $effectivePWDPolicyDN = $defaultNC } $effectivePWDPolicyName = ($pwdPolicyInDomain | ?{$_.DN -eq $effectivePWDPolicyDN}).name $listOfQueriedUsersObj."Effective PWD Policy" = $effectivePWDPolicyName $listOfQueriedUsersObj."Language" = $languageForUser $listOfQueriedUsersObj."Mail Subject" = $mailSubjectForUser $listOfQueriedUsersObj."HTML Body File" = $htmlBodyFileForUser $listOfQueriedUsers += $listOfQueriedUsersObj } $searcher = $NULL $results = $NULL } Else { ### If The Search Base Does NOT Exist Then Skip That Search Base And Send Mail About It Logging " --> SKIPPED DUE TO ERROR!" Send-MailMessage -SmtpServer $smtpServer -From $mailFromSender -To $toSMTPAddressInTestMode -Priority High -Subject "Error With Defined SearchBase!" -Body "The Search Base '$searchBase' Does Not Exist!" -BodyAsHtml } } } } Logging "" Logging "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" ### Now Having The List Of Queried Users, Determine Which Of Those Users Require E-mail Notification Based Upon The Configured Warning Periods Logging "" Logging "Processing Warning Periods..." ### From The List Of Queried Users, Get Those Users For Which The AD User Account Has Not Expired Yet $listOfUsersWithNonExpiredAccounts = $listOfQueriedUsers | ?{$_."Days Until Account Expiry" -gt 0} ### Creating An Empty Array For Users That Will Be Notified! $listOfUsersWithExpiringPWDToNotify = @() ### Process Every Configured Warning Period. Make Sure In The XML NOT To Have Overlapping Periods! $daysBeforeWarn | %{ $max = $_.max $min = $_.min Logging "" Logging "** Period: Max: $max Days | Min: $min Days **" $listOfUsersWithinWarningPeriod = $listOfUsersWithNonExpiredAccounts | ?{$_."Days Until PWD Expiry" -lt $max -And $_."Days Until PWD Expiry" -gt $min} $userCountInWarningPeriod = ($listOfUsersWithinWarningPeriod | Measure-Object).Count Logging " --> User Count Within Warning Period...: $userCountInWarningPeriod" $listOfUsersWithExpiringPWDToNotify += $listOfUsersWithinWarningPeriod } ### If If Was Configured To Export The List Of Users That Will Be Notified, Than Do So! If ($exportToCSV.ToUpper() -eq "ON") { $listOfUsersWithExpiringPWDToNotify | Export-Csv -Path $fullPathToCSVFile -NoTypeInformation } #Logging "" #Logging "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" ### Show On Screen The List Of Queried Users Including Some Details #Logging "" #Logging "List Of Queried Users..." #$listOfUsersWithNonExpiredAccounts | FT "FQDN AD Domain",DN,"Given Name","Last Name","Display Name","E-Mail Address","PWD Last Set","PWD Expire Date","Days Until PWD Expiry","Account Expiry Date","Days Until Account Expiry","Effective PWD Policy","Language","Mail Subject","HTML Body File" -Autosize #$listOfUsersWithNonExpiredAccounts | FT "FQDN AD Domain","Display Name","E-Mail Address","PWD Expire Date","Days Until PWD Expiry","Effective PWD Policy","Language" -Autosize #$userCountQueried = ($listOfUsersWithNonExpiredAccounts | Measure-Object).Count #Logging "--> User Count To Be Queried....: $userCountQueried" Logging "" Logging "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" ### Show On Screen The List Of Users That Will Be Notified, Including Some Details Logging "" Logging "List Of Notified Users..." #$listOfUsersWithExpiringPWDToNotify | FT "FQDN AD Domain",DN,"Given Name","Last Name","Display Name","E-Mail Address","PWD Last Set","PWD Expire Date","Days Until PWD Expiry","Account Expiry Date","Days Until Account Expiry","Effective PWD Policy","Language","Mail Subject","HTML Body File" -Autosize $listOfUsersWithExpiringPWDToNotify | FT "FQDN AD Domain","Display Name","E-Mail Address","PWD Expire Date","Days Until PWD Expiry","Effective PWD Policy","Language" -Autosize $userCountNotified = ($listOfUsersWithExpiringPWDToNotify | Measure-Object).Count Logging "--> User Count To Be Notified...: $userCountNotified" ### If The FORCE Parameter Was NOT Specified With TRUE Then DO NOT Send Any E-Mail If ($executionMode.ToUpper() -eq "TEST (NO MAILINGS)") { Logging "" Logging " --> No Notifications Have Been Send!" } Else { ### When Running In DEV Mode Execute This Part If ($executionMode.ToUpper() -eq "DEV") { Logging "" Logging "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" Logging "" Logging "Displaying Information Of The Development User..." ### Get The Current AD Domain $ThisADDomain = [DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() $fqdnThisADDomain = $ThisADDomain.Name ### Discover An RWDC For That Current AD Domain $fqdnDC = DiscoverRWDC $fqdnThisADDomain ### Setup The LDAP Query To Get The Information Of The User And Execute The Query $RootDSE = [ADSI]"LDAP://$fqdnDC/RootDSE" $defaultNC = $RootDSE.defaultNamingContext $searchRoot = $NULL $searchRoot = [ADSI]"LDAP://$fqdnDC/$defaultNC" $searcher = New-Object System.DirectoryServices.DirectorySearcher($searchRoot) $searcher.Filter = "(&(objectCategory=person)(objectClass=user)(|(proxyAddresses=smtp:$toSMTPAddressInTestMode)(proxyAddresses=SMTP:$toSMTPAddressInTestMode)))" $searcher.SearchScope = "Subtree" $searcher.PageSize = 1000 $propertyList = "distinguishedName","givenName","sn","displayName","pwdLastSet","msDS-UserPasswordExpiryTimeComputed","msDS-ResultantPSO" ForEach ($property in $propertyList){ $searcher.PropertiesToLoad.Add($property) | Out-Null } $results = $NULL $results = $searcher.FindOne() ### Get The Properties Of The User If ($results.Properties.givenname -ne $null) { $adUserGivenName = $results.Properties.givenname[0] } Else { $adUserGivenName = "NO-VALUE" } If ($results.Properties.sn -ne $null) { $adUserSn = $results.Properties.sn[0] } Else { $adUserSn = "NO-VALUE" } If ($results.Properties.displayname -ne $null) { $adUserDisplayName = $results.Properties.displayname[0] } Else { $adUserDisplayName = "NO-VALUE" } $adUserPwdLastSet = Get-Date -Date ([DateTime]::FromFileTime([Int64]::Parse($results.Properties.pwdlastset[0]))) -Format $formatDateTime $adUserPwdExpires = $results.Properties."msds-userpasswordexpirytimecomputed"[0] ### If A Password Is Configured With Never Expires, Then Assign An Insane End Date To Be Able To Perform Calculations If ($adUserPwdExpires -eq 9223372036854775807 -Or $adUserAccountExpires -eq 0) { $adUserPwdExpires = Get-Date "9999-12-31 23:59:59" -format $formatDateTime } Else { $adUserPwdExpires = Get-Date -Date ([DateTime]::FromFileTime([Int64]::Parse($adUserPwdExpires))) -Format $formatDateTime } $timeDiffPwdExpiryInDays = (New-TimeSpan -Start $execStartDateTime -end $adUserPwdExpires).TotalDays If ($results.Properties."msds-resultantpso" -ne $null) { $effectivePWDPolicyDN = $results.Properties."msds-resultantpso"[0] } Else { $effectivePWDPolicyDN = $defaultNC } $effectivePWDPolicyOnUser = $pwdPolicyInDomain | ?{$_.DN -eq $effectivePWDPolicyDN} ### Get The Settings Of The Effective PWD Policy On The User $policyPWDName = $effectivePWDPolicyOnUser.Name $policyPWDMinLength = $effectivePWDPolicyOnUser.MinPwdLength $policyPWDMinAge = $effectivePWDPolicyOnUser.MinPwdAge $policyPWDMaxAge = $effectivePWDPolicyOnUser.MaxPwdAge $policyPWDHistory = $effectivePWDPolicyOnUser.PwdHistoryLength $policyPWDComplexity = $effectivePWDPolicyOnUser.PwdComplexity ### Get The Content Of The HTML File That Will Be Used For The E-Mails $languageForUser = "default" $mailSubjectForUser = ($htmlBodyFiles | ?{$_.language -eq $languageForUser}).mailSubject $htmlBodyFileForUser = ($htmlBodyFiles | ?{$_.language -eq $languageForUser}).fullPath $mailBodyForUser = Get-Content $htmlBodyFileForUser Logging "" Logging "Display Name................: $adUserDisplayName" Logging "First Name..................: $adUserGivenName" Logging "Last Name...................: $adUserSn" Logging "PWD Last Set................: $adUserPwdLastSet" Logging "PWD Expiry Date.............: $adUserPwdExpires" Logging "Days Until PWD Expiry Date..: $timeDiffPwdExpiryInDays ($([math]::Round($timeDiffPwdExpiryInDays)))" Logging "Effective PWD Policy Name...: $policyPWDName" Logging "Effective PWD Min Length....: $policyPWDMinLength" Logging "Effective PWD Min Age.......: $policyPWDMinAge" Logging "Effective PWD Max Age.......: $policyPWDMaxAge" Logging "Effective PWD History.......: $policyPWDHistory" Logging "Effective PWD Complexity....: $policyPWDComplexity" Logging "Language....................: $languageForUser" Logging "Mail Subject................: $mailSubjectForUser" Logging "HTML Body File..............: $htmlBodyFileForUser" ### Replace Any Variables In The SUBJECT With The Actual Values $mailSubject = $mailSubjectForUser -replace "FIRST_NAME",$adUserGivenName $mailSubject = $mailSubject -replace "LAST_NAME",$adUserSn $mailSubject = $mailSubject -replace "DISPLAY_NAME",$adUserDisplayName $mailSubject = $mailSubject -replace "FQDN_DOMAIN",$fqdnThisADDomain $mailSubject = $mailSubject -replace "PWD_EXPIRY_DATE",$adUserPwdExpires $mailSubject = $mailSubject -replace "PWD_EXPIRE_IN_NUM_DAYS",[math]::Round($timeDiffPwdExpiryInDays) $mailSubject = $mailSubject -replace "PWD_MIN_LENGTH",$policyPWDMinLength $mailSubject = $mailSubject -replace "PWD_MIN_AGE",$policyPWDMinAge $mailSubject = $mailSubject -replace "PWD_MAX_AGE",$policyPWDMaxAge $mailSubject = $mailSubject -replace "PWD_HISTORY",$policyPWDHistory $mailSubject = $mailSubject -replace "PWD_COMPLEX",$policyPWDComplexity $mailSubject = $mailSubject -replace "PWD_CHANGE_RESET_URL",$pwdChangeOrResetURL ### Replace Any Variables In The BODY With The Actual Values $mailBody = $mailBodyForUser -replace "FIRST_NAME",$adUserGivenName $mailBody = $mailBody -replace "LAST_NAME",$adUserSn $mailBody = $mailBody -replace "DISPLAY_NAME",$adUserDisplayName $mailBody = $mailBody -replace "FQDN_DOMAIN",$fqdnThisADDomain $mailBody = $mailBody -replace "PWD_EXPIRY_DATE",$adUserPwdExpires $mailBody = $mailBody -replace "PWD_EXPIRE_IN_NUM_DAYS",[math]::Round($timeDiffPwdExpiryInDays) $mailBody = $mailBody -replace "PWD_MIN_LENGTH",$policyPWDMinLength $mailBody = $mailBody -replace "PWD_MIN_AGE",$policyPWDMinAge $mailBody = $mailBody -replace "PWD_MAX_AGE",$policyPWDMaxAge $mailBody = $mailBody -replace "PWD_HISTORY",$policyPWDHistory $mailBody = $mailBody -replace "PWD_COMPLEX",$policyPWDComplexity $mailBody = $mailBody -replace "PWD_CHANGE_RESET_URL",$pwdChangeOrResetURL ### Send A Notification E-Mail About The Expiring Password Send-MailMessage -SmtpServer $smtpServer -From $mailFromSender -To $toSMTPAddressInTestMode -Priority $mailPriority -Subject $mailSubject -Body $($mailBody | Out-String) -BodyAsHtml Logging "" Logging " --> Notifying '$adUserDisplayName' by sending an e-mail to '$toSMTPAddressInTestMode'" } Else { ### When Running In TEST Or PROD Mode Execute This Part ### Process Any User With Expiring Password And Send E-Mail Notification About The Expiring Password $listOfUsersWithExpiringPWDToNotify | %{ ### If The FORCE Parameter Was Specified With TRUE Then Send E-Mail Based On The Configured Execution Mode If ($executionMode.ToUpper() -eq "TEST") { ### For All Users Send E-Mail Notifications To The Configured Admin Mail Address $mailToRecipient = $toSMTPAddressInTestMode } If ($executionMode.ToUpper() -eq "PROD") { ### For All Users Send E-Mail Notifications To The E-Mail Address Of Each User $mailToRecipient = $_."E-Mail Address" } ### Get The Display Name Of The User $displayNameUser = $_."Display Name" ### Get The Effective PWD Policy On The User $effectivePWDPolicyNameOnUser = $_."Effective PWD Policy" $effectivePWDPolicyOnUser = $pwdPolicyInDomain | ?{$_.Name -eq $effectivePWDPolicyNameOnUser} ### Get The Settings Of The Effective PWD Policy On The User $policyPWDMinLength = $effectivePWDPolicyOnUser.MinPwdLength $policyPWDMinAge = $effectivePWDPolicyOnUser.MinPwdAge $policyPWDMaxAge = $effectivePWDPolicyOnUser.MaxPwdAge $policyPWDHistory = $effectivePWDPolicyOnUser.PwdHistoryLength $policyPWDComplexity = $effectivePWDPolicyOnUser.PwdComplexity ### Get The Content Of The HTML File That Will Be Used For The E-Mails $languageForUser = $_.Language $mailSubjectForUser = ($htmlBodyFiles | ?{$_.language -eq $languageForUser}).mailSubject $htmlBodyFileForUser = ($htmlBodyFiles | ?{$_.language -eq $languageForUser}).fullPath $mailBodyForUser = Get-Content $htmlBodyFileForUser ### Replace Any Variables In The SUBJECT With The Actual Values $mailSubject = $mailSubjectForUser -replace "FIRST_NAME",$_."Given Name" $mailSubject = $mailSubject -replace "LAST_NAME",$_."Last Name" $mailSubject = $mailSubject -replace "DISPLAY_NAME",$_."Display Name" $mailSubject = $mailSubject -replace "FQDN_DOMAIN",$_."FQDN AD Domain" $mailSubject = $mailSubject -replace "PWD_EXPIRY_DATE",$_."PWD Expire Date" $mailSubject = $mailSubject -replace "PWD_EXPIRE_IN_NUM_DAYS",[math]::Round($_."Days Until PWD Expiry") $mailSubject = $mailSubject -replace "PWD_MIN_LENGTH",$policyPWDMinLength $mailSubject = $mailSubject -replace "PWD_MIN_AGE",$policyPWDMinAge $mailSubject = $mailSubject -replace "PWD_MAX_AGE",$policyPWDMaxAge $mailSubject = $mailSubject -replace "PWD_HISTORY",$policyPWDHistory $mailSubject = $mailSubject -replace "PWD_COMPLEX",$policyPWDComplexity $mailSubject = $mailSubject -replace "PWD_CHANGE_RESET_URL",$pwdChangeOrResetURL ### Replace Any Variables In The BODY With The Actual Values $mailBody = $mailBodyForUser -replace "FIRST_NAME",$_."Given Name" $mailBody = $mailBody -replace "LAST_NAME",$_."Last Name" $mailBody = $mailBody -replace "DISPLAY_NAME",$_."Display Name" $mailBody = $mailBody -replace "FQDN_DOMAIN",$_."FQDN AD Domain" $mailBody = $mailBody -replace "PWD_EXPIRY_DATE",$_."PWD Expire Date" $mailBody = $mailBody -replace "PWD_EXPIRE_IN_NUM_DAYS",[math]::Round($_."Days Until PWD Expiry") $mailBody = $mailBody -replace "PWD_MIN_LENGTH",$policyPWDMinLength $mailBody = $mailBody -replace "PWD_MIN_AGE",$policyPWDMinAge $mailBody = $mailBody -replace "PWD_MAX_AGE",$policyPWDMaxAge $mailBody = $mailBody -replace "PWD_HISTORY",$policyPWDHistory $mailBody = $mailBody -replace "PWD_COMPLEX",$policyPWDComplexity $mailBody = $mailBody -replace "PWD_CHANGE_RESET_URL",$pwdChangeOrResetURL ### Send A Notification E-Mail About The Expiring Password Send-MailMessage -SmtpServer $smtpServer -From $mailFromSender -To $mailToRecipient -Priority $mailPriority -Subject $mailSubject -Body $($mailBody | Out-String) -BodyAsHtml Logging "" Logging " --> Notifying '$displayNameUser' by sending an e-mail to '$mailToRecipient'" } } } Logging "" Logging "#######################################################################################"

You can download the most recent PowerShell version from HERE.

I HAVE NOT TESTED EVERY POSSIBLE SCENARIO! Please provide feedback through the comments section OR you the contact page

DISCLAIMER (READ THIS!):

  • I wrote this script, therefore I own it. Anyone asking money for it, should NOT be doing that and is basically ripping you off!
  • The script is freeware, you are free to use it and distribute it, but always refer to this website (https://jorgequestforknowledge.wordpress.com/) as the location where you got it.
  • This script is furnished "AS IS". No warranty is expressed or implied!
  • I have NOT tested it in every scenario nor have I tested it against every Windows and/or AD version
  • Always test first in lab environment to see if it meets your needs!
  • Use this script at your own risk!
  • I do not warrant this script to be fit for any purpose, use or environment!
  • I have tried to check everything that needed to be checked, but I do not guarantee the script does not have bugs!
  • I do not guarantee the script will not damage or destroy your system(s), environment or whatever!
  • I do not accept liability in any way if you screw up, use the script wrong or in any other way where damage is caused to your environment/systems!
  • If you do not accept these terms do not use the script in any way and delete it immediately!

Cheers,

Jorge

———————————————————————————————

* This posting is provided "AS IS" with no warranties and confers no rights!

* Always evaluate/test yourself before using/implementing this!

* DISCLAIMER: https://jorgequestforknowledge.wordpress.com/disclaimer/

———————————————————————————————

############### Jorge’s Quest For Knowledge #############

######### http://JorgeQuestForKnowledge.wordpress.com/ ########

———————————————————————————————

Posted in Active Directory Domain Services (ADDS), Fine Grained Password Policies, Password Expiration Notification, PowerShell, Tooling/Scripting | Leave a Comment »

(2016-05-08) Copying OUs, Users, Groups And GPOs From One AD To A Different AD

Posted by Jorge on 2016-05-08


Almost 11 years ago I wrote the following blog posts about creating a test environment.

Those blog posts mention the use of the legacy GPMC scripts to copy OUs, user, groups and GPOs from one AD to another AD.

Ian Farr, working at Microsoft, wrote a PowerShell module to do exactly the same, copying OUs, user, groups and GPOs from one AD to another AD. Wanna try it out?

Go and get the PowerShell module from the script gallery here

Cheers,
Jorge
———————————————————————————————
* This posting is provided "AS IS" with no warranties and confers no rights!
* Always evaluate/test yourself before using/implementing this!
* DISCLAIMER:
https://jorgequestforknowledge.wordpress.com/disclaimer/
———————————————————————————————
############### Jorge’s Quest For Knowledge #############
#########
http://JorgeQuestForKnowledge.wordpress.com/ ########
———————————————————————————————

Posted in PowerShell, Tooling/Scripting | Leave a Comment »

(2015-10-18) Notifying Users By E-mail Their Password Is Going To Expire (Update 4)

Posted by Jorge on 2015-10-18


UPDATE 2016-05-09: see https://jorgequestforknowledge.wordpress.com/2016/05/09/notifying-users-by-e-mail-their-password-is-going-to-expire-update-5/

Almost 6 years ago I wrote a blog post about and also wrote a tool to notify users through e-mail when their password was going to expire. You can read all the details about the idea here. Now that tool was very inflexible and because of that I received numerous requests to make it more flexible such as the ability to customize the e-mail message. With this blog post I’m sharing a brand new tool, based upon PowerShell, that will notify users through e-mail when their password is going to expire. So let’s get started in explaining on this works! I did not test all combinations! However, I do expect it to run on any Windows version as long as PowerShell is available. It should also work against any AD version and there is NO dependency on using the AD PowerShell CMDlets!. Everything is done through ADSI to be independent of Windows versions! It will also support PSOs if the DFL is high enough and PSOs are configured!

SYNTAX:

  • <PoSH Script File> –> Runs The Script In Test Mode While NOT Sending Any E-Mails
  • <PoSH Script File> -force:$true –> Runs The Script In DEV (One Mail To Configured Admin) Or TEST (All Mails To Configured Admin) Or PROD (Mails To Users) Mode While Sending Any E-Mails

Please provide feedback through the comments section OR you the contact page

DISCLAIMER (READ THIS!):

  • I wrote this script, therefore I own it. Anyone asking money for it, should NOT be doing that and is basically ripping you off!
  • The script is freeware, you are free to use it and distribute it, but always refer to this website (https://jorgequestforknowledge.wordpress.com/) as the location where you got it.
  • This script is furnished "AS IS". No warranty is expressed or implied!
  • I have NOT tested it in every scenario nor have I tested it against every Windows and/or AD version
  • Always test first in lab environment to see if it meets your needs!
  • Use this script at your own risk!
  • I do not warrant this script to be fit for any purpose, use or environment!
  • I have tried to check everything that needed to be checked, but I do not guarantee the script does not have bugs!
  • I do not guarantee the script will not damage or destroy your system(s), environment or whatever!
  • I do not accept liability in any way if you screw up, use the script wrong or in any other way where damage is caused to your environment/systems!
  • If you do not accept these terms do not use the script in any way and delete it immediately!

REMARKS (READ THIS!):

  • The script requires PowerShell v2.0 at a minimum
  • This script must be able to read the contents of the PSO container in every AD domain the script will target!. By default only Domain Admins can read this.
  • It is therefore needed to delegate those permissions to the account executing this PoSH script.
  • For more information about this see the blog post: https://jorgequestforknowledge.wordpress.com/2007/08/09/windows-server-2008-fine-grained-password-policies/
  • DSACLS "\\<Some RWDC>\CN=Password Settings Container,CN=System,<Your AD domain DN>" /G "<Some Security Principal>:GR" /I:T
    • This assign <Some Security Principal> with Allow:Read on the Password Settings Container including its descendant objects

The tool uses an XML called "AD-Pwd-Exp-Notify.xml". It is pre-filled with examples from my test/demo environment. Make sure to change as needed to accommodate your own environment and requirements!

The script has four execution modes. When NOT running the PowerShell script with the ‘-force’ parameter, it will by default run in TEST mode without sending any e-mail to users regarding password expiry ("TEST (NO MAILINGS)"), no matter what the configuration in the XML files specifies. When running the PowerShell script with the ‘-force’ parameter, it will look in the XML file to see which execution mode to run in. When "DEV" is specified it will only send 1 mail to the SMTP address of the admin user specified in the "toSMTPAddressInTestMode" configuration field. This mode allows you to develop the solution being swamped in e-mails or impacting your users. When "TEST" is specified it will only send all mails to the SMTP address of the admin user specified in the "toSMTPAddressInTestMode" configuration field. This mode allows you to see/experience what your scoped/targeted users would see/experience without actually impacting them. When "PROD" is specified it will only send all mails to the SMTP address of the individual users. This really sends the e-mails to all the scoped/targeted individual users.

<!– Execution Mode: DEV (1 Mail To Admin User) or TEST (All Mails To Admin User) or PROD (All Mails To Individual Users) –>

<executionMode>DEV</executionMode>

The PowerShell script sends e-mail, therefore it requires a FROM e-mail address

<!– The SMTP Address Used In The FROM Field –>

<mailFromSender>general.DO-NOT-REPLY@iamtec.nl</mailFromSender>

To develop the solution and test it you can specify an SMTP address that will be used to send e-mails to, without impacting the real user community. That SMTP address will also be used for notifications is the SMTP server or DC is unavailable.

<!– The SMTP Address Used When Running In DEV/TEST Mode And Also Used For Notifications –>

<toSMTPAddressInTestMode>adm.root@iamtec.nl</toSMTPAddressInTestMode>

The PowerShell script sends e-mail, therefore it requires an SMTP server. A test connection to the SMTP server is made. If it fails the script aborts!

<!– FQDN Of The Mail Server Or Mail Relay –>

<smtpServer>MAIL.IAMTEC.NET</smtpServer>

The priority of the mail send can be configured as Low, Normal or High

<!– The Priority Of The Message: Low, Normal, High –>

<mailPriority>High</mailPriority>

The script supports multi-lingual messages. You must always specify a default language and for each language you must also specify a mail subject and the full path to HTML body file that contains the text in a specific language. Both the subject and the body support variables that can be replaced by the actual values. The script contains an example for US (English) and the same example for NL (Dutch).

<!– The File With The HTML Body Text For A Specific Language And The Subject. Supported Variables: FIRST_NAME, LAST_NAME, DISPLAY_NAME, FQDN_DOMAIN, PWD_EXPIRE_IN_NUM_DAYS, PWD_EXPIRY_DATE, PWD_MIN_LENGTH, PWD_MIN_AGE, PWD_MAX_AGE, PWD_HISTORY, PWD_COMPLEX, PWD_CHANGE_RESET_URL  –>
<htmlBodyFiles>
    <htmlBodyFile language="default" mailSubject="Expiring Password In Approx. PWD_EXPIRE_IN_NUM_DAYS Days – Change Your Password As Soon As Possible!" fullPath="D:\TEMP\ADPwdExpNotifyMessageBody_US.html" />
    <htmlBodyFile language="US" mailSubject="Expiring Password In Approx. PWD_EXPIRE_IN_NUM_DAYS Days – Change Your Password As Soon As Possible!" fullPath="D:\TEMP\ADPwdExpNotifyMessageBody_US.html" />
    <htmlBodyFile language="NL" mailSubject="Verlopen Wachtwoord In Ongeveer PWD_EXPIRE_IN_NUM_DAYS Dagen – Wijzig Uw Wachtwoord Zo Snel Als Mogelijk!" fullPath="D:\TEMP\ADPwdExpNotifyMessageBody_NL.html" />
</htmlBodyFiles>

If you have a web portal (e.g. FIM SSPR or Exchange Change Password) to change and/or reset the password, then you can specify it here

<!– The URL Where The Users Can Change Or Reset Their Password –>

<pwdChangeOrResetURL>https://ssprportal.iamtec.net:447/</pwdChangeOrResetURL>

Logging to screen can be enabled (ON) or disabled (OFF).

<!– Enable/Disable Logging To Screen: ON or OFF –>

<logToScreen>ON</logToScreen>

Logging tofile can be enabled (ON) or disabled (OFF).

<!– Enable/Disable Logging To A Log File: ON or OFF –>

<logToFile>ON</logToFile>

If logging is enabled, then you must specify the full path to the log file that will be used. The script itself will take date and time into account

<!– Full Path Of The Log File (.LOG Extension!) –>

<fullPathToLogFile>D:\TEMP\ADPwdExpNotify.log</fullPathToLogFile>

To make sure the disk is not swamped with a huge number of log files, you can specify for how many days the script will keep log files. Every log file older than the specified number will be deleted

<!– Number Of Days To Keep LOG Files –>

<numDaysLOGToKeep>30</numDaysLOGToKeep>

When enabled (ON), the script will export the information of users to a CSV file for troubleshooting and analyses. When disabled (OFF) nothing is exported.

<!– Enable/Disable Export Of Notified Accounts To A CSV File: ON or OFF –>

<exportToCSV>ON</exportToCSV>

If exporting is enabled, then you must specify the full path to the CSV file that will be used. The script itself will take date and time into account

<!– Full Path Of The CSV File (.CSV Extension!) –>

<fullPathToCSVFile>D:\TEMP\ADPwdExpNotify.csv</fullPathToCSVFile>

To make sure the disk is not swamped with a huge number of CSV files, you can specify for how many days the script will keep CSV files. Every CSV file older than the specified number will be deleted

<!– Number Of Days To Keep CSV Files –>

<numDaysCSVToKeep>30</numDaysCSVToKeep>

In the XML config file you can specify the date/time format to be used on screen, in the log, in the CSV and in the E-mail message

<!– Date And Time Format To Use On Screen, In Logs And In E-mail Message –>
<formatDateTime>yyyy-MM-dd HH:mm:ss</formatDateTime>

In this section you specify every AD domain in the AD forest, including trusting AD domains, for which its scoped/targeted users must be notified. For every AD domain, you then need to specify if an RWDC needs to be discovered (specifiy: DISCOVER) or you can specifically mention an RWDC that must be targeted. A test connection to the RWDC is made. If it fails the AD domain for that RWDC is fully skipped! Then for every AD domain, specify one or more search bases to scope/target users in the LDAP query. For every search base configure the language for the scoped/targeted users. Make sure that if you specify specific languages, that you also have configured the HTML Body File for that language

<!– Targeted Domains, Specify DISCOVER To Discover A DC Or Use Specific DC And Search Bases Per Domain –>
<!– WARNING: Make Sure The Search Bases DO NOT Overlap Each Other!!! –>
<domains>
    <domain FQDN="IAMTEC.NET" DC="DISCOVER">
        <searchBase nr="1" language="default">OU=Users,OU=EMPLOYEES,OU=Org-Users,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="2" language="US">OU=Users,OU=CONTRACTORS,OU=Org-Users,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="3" language="US">OU=Users,OU=CONTRACTORZZZ,OU=Org-Users,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="4" language="NL">OU=Users,OU=HISTORY1,OU=Org-Users,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="5" language="NL">OU=Users,OU=HISTORY2,OU=Org-Users,DC=IAMTEC,DC=NET</searchBase>
    </domain>
    <domain FQDN="CHILD.IAMTEC.NET" DC="C1FSRWDC1.CHILD.IAMTEC.NET">
        <searchBase nr="1" language="default">OU=Users,OU=EMPLOYEES,OU=Org-Users,DC=CHILD,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="2" language="US">OU=Users,OU=CONTRACTORS,OU=Org-Users,DC=CHILD,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="3" language="US">OU=Users,OU=CONTRACTORZZZ,OU=Org-Users,DC=CHILD,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="4" language="NL">OU=Users,OU=HISTORY1,OU=Org-Users,DC=CHILD,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="5" language="NL">OU=Users,OU=HISTORY2,OU=Org-Users,DC=CHILD,DC=IAMTEC,DC=NET</searchBase>
    </domain>
</domains>

In this section you can specify one or multiple periods of notifications. Make sure that none of the periods overlaps any other period!. In the example shown, the scoped/targeted user will receive 4 notifications assuming the script executes once every day. Also make sure the periods configured are in balance with the maximum password age!

<!– Number Of Days Before The Password Expires To Send Notifications –>
<!– WARNING: Make Sure The Periods DO NOT Overlap Each Other!!! –>
<daysBeforeWarn>
    <period nr="1" Max="10" Min="9" />
    <period nr="2" Max="5" Min="4" />
    <period nr="3" Max="2" Min="0" />
</daysBeforeWarn>

image_thumb26_thumb_thumb

Figure 1a: XML Configuration File

image_thumb29_thumb_thumb

Figure 1b: XML Configuration File

Executing The Script

Executing the script to run in "DEV", "TEST" or "PROD" mode (whatever is configured in the XML configuration file) while using the default location of the XML configuration file (same folder as script)

.\AD-Pwd-Exp-Notify_v018.ps1 -force

Executing the script to run in "TEST (NO MAILINGS)" mode while using the default location of the XML configuration file (same folder as script)

.\AD-Pwd-Exp-Notify_v018.ps1

Executing the script to run in "TEST (NO MAILINGS)" mode while using a custom location of the XML configuration file

.\AD-Pwd-Exp-Notify_v018.ps1 -xmlconfigfilepath D:\TEMP\AD-Pwd-Exp-Notify.xml

Executing the script to run in "DEV", "TEST" or "PROD" mode (whatever is configured in the XML configuration file) while using custom location of the XML configuration file

.\AD-Pwd-Exp-Notify_v018.ps1 -xmlconfigfilepath D:\TEMP\AD-Pwd-Exp-Notify.xml -force

Example Output Of The Script (On Screen)

image_thumb32_thumb_thumb

Figure 2a: Output To Screen

image_thumb35_thumb_thumb

Figure 2b: Output To Screen

image_thumb38_thumb_thumb

Figure 2c: Output To Screen

image_thumb41_thumb_thumb

Figure 2d: Output To Screen

image_thumb44_thumb_thumb

Figure 2e: Output To Screen

Example Output Of The Script (Log File)

See zip file

Example Output Of The Script (CSV file)

See zip file

E-mail Message For US English Language

image_thumb48_thumb_thumb

Figure 3a: E-Mail Notification In English

image_thumb51_thumb_thumb

Figure 3b: E-Mail Notification In English

E-mail Message For Dutch Language

image_thumb55_thumb_thumb

Figure 4a: E-Mail Notification In Dutch

image_thumb58_thumb_thumb

Figure 4b: E-Mail Notification In Dutch

And Finally….The PowerShell Script Itself

Version of version: v0.18

Date of script: 2015-10-13

### Abstract: This PoSH Script Notifies Mailbox Enabled Users For Which The Password Will Expires Within A Specific Number Of Days ### Written by: Jorge de Almeida Pinto [MVP-DS] ### BLOG: https://jorgequestforknowledge.wordpress.com/ ### ### 2015-03-21: Initial version of the script in PowerShell (v0.13) ### 2015-03-26: Bug fixes regarding some attributes not having values (v0.14) ### 2015-03-27: Supporting date/time format in XML and incorrect variable being used to get the correct password policy settings (v0.15) ### 2015-04-29: Bug fixes regarding the default domain GPO getting no name when no PSOs are used or inheriting the name of the last processed PSO, ### the displayName of the development user not being processed correctly, and more enhanced error detection when discovering a DC for ### non-existing AD domain, and better explanation and information about the parameters and the script itself (v0.16) ### 2015-09-22: Bug fixes regarding specifying the script version and date after the parameter section (v0.17) ### 2015-10-13: Resolved a bug with regards to paging when searching (v0.18) ### <# .SYNOPSIS This PoSH Script Notifies Mailbox Enabled Users For Which The Password Will Expires Within A Specific Number Of Days .DESCRIPTION This PoSH script notifies mailbox enabled users for which the password will expires within a specific number of days. The configuration of the script is done through an XML file. The tool uses an XML called "AD-Pwd-Exp-Notify.xml". It is pre-filled with examples from my test/demo environment. Make sure to change as needed to accommodate your own environment and requirements! For detailed information about all configurable options see the sample XML file or browse to: https://jorgequestforknowledge.wordpress.com/2015/03/24/notifying-users-by-e-mail-their-password-is-going-to-expire-update-1/ .PARAMETER force Runs the script in whatever mode is configured in the XML file (e.g. "DEV", "TEST" or "PROD" mode) .PARAMETER xmlconfigfilepath Allows to use a custom location and custom XML file instead of the default XML file in the default location (same folder as the script) .EXAMPLE Executing the script to run in "TEST (NO MAILINGS)" mode while using the default location of the XML configuration file (same folder as script) AD-Pwd-Exp-Notify_vXXX.ps1 .EXAMPLE Executing the script to run in "DEV", "TEST" or "PROD" mode (whatever is configured in the XML configuration file) while using the default location of the XML configuration file (same folder as script) AD-Pwd-Exp-Notify_vXXX.ps1 -force .EXAMPLE Executing the script to run in "TEST (NO MAILINGS)" mode while using a custom location of the XML configuration file AD-Pwd-Exp-Notify_vXXX.ps1 -xmlconfigfilepath D:\TEMP\AD-Pwd-Exp-Notify.xml .EXAMPLE Executing the script to run in "DEV", "TEST" or "PROD" mode (whatever is configured in the XML configuration file) while using custom location of the XML configuration file AD-Pwd-Exp-Notify_vXXX.ps1 -xmlconfigfilepath D:\TEMP\AD-Pwd-Exp-Notify.xml -force .NOTES -->> DISCLAIMER <<-- * I wrote this script, therefore I own it. Anyone asking money for it, should NOT be doing that and is basically ripping you off! * The script is freeware, you are free to use it and distribute it, but always refer to this website (https://jorgequestforknowledge.wordpress.com/) as the location where you got it. * This script is furnished "AS IS". No warranty is expressed or implied! * I have NOT tested it in every scenario nor have I tested it against every Windows and/or AD version * Always test first in lab environment to see if it meets your needs! * Use this script at your own risk! * I do not warrant this script to be fit for any purpose, use or environment! * I have tried to check everything that needed to be checked, but I do not guarantee the script does not have bugs! * I do not guarantee the script will not damage or destroy your system(s), environment or whatever! * I do not accept liability in any way if you screw up, use the script wrong or in any other way where damage is caused to your environment/systems! * If you do not accept these terms do not use the script in any way and delete it immediately! -->> REMARKS <<-- * The script requires PowerShell v2.0 at a minimum * This script must be able to read the contents of the PSO container in every AD domain the script will target!. By default only Domain Admins can read this. * It is therefore needed to delegate those permissions to the account executing this PoSH script. * For more information about this see the blog post: https://jorgequestforknowledge.wordpress.com/2007/08/09/windows-server-2008-fine-grained-password-policies/ * DSACLS "\\<Some RWDC>\CN=Password Settings Container,CN=System,<Your AD domain DN>" /G "<Some Security Principal>:GR" /I:T >> This assigns <Some Security Principal> with Allow:Read on the Password Settings Container including its descendant objects #> Param( [Parameter(Mandatory=$false)] [string]$xmlconfigfilepath, [Parameter(Mandatory=$false)] [switch]$force ) $scriptVersion = "v0.18" $scriptDate = "2015-10-13" ################################################################################################## #################################### SCRIPT FUNCTIONS START ###################################### ################################################################################################## ################################################################################################## ### FUNCTION: Logging Data To The Log File Function Logging($dataToLog) { $datetimeLogLine = "[" + $(Get-Date -format $formatDateTime) + "] : " If ($logToFile.ToUpper() -eq "ON") { Out-File -filepath "$fullPathToLogFile" -append -inputObject "$datetimeLogLine$dataToLog" } If ($logToScreen.ToUpper() -eq "ON") { Write-Output($datetimeLogLine + $dataToLog) } } ################################################################################################## ### FUNCTION: Cleaning Up Old Log Files Function CleanUpLOGFiles($numDaysLOGToKeep) { $regExPatternLogFile = '^.*_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}.log$' $oldLogFiles = Get-ChildItem -Path $folderLogFile\*.log | ?{$_.Name -match $regExPatternLogFile} $oldLogFilesToDelete = $oldLogFiles | ?{$_.lastwritetime -lt (Get-Date $execStartDateTime).addDays(-$numDaysLOGToKeep) -and -not $_.psiscontainer} $oldLogFilesToDelete | %{Remove-Item $_.FullName -force} Return ($oldLogFilesToDelete | Measure-Object).Count } ################################################################################################## ### FUNCTION: Cleaning Up Old Csv Files Function CleanUpCSVFiles($numDaysCSVToKeep) { $regExPatternCsvFile = '^.*_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}.csv$' $oldCsvFiles = Get-ChildItem -Path $folderCsvFile\*.csv | ?{$_.Name -match $regExPatternCsvFile} $oldCsvFilesToDelete = $oldCsvFiles | ?{$_.lastwritetime -lt (Get-Date $execStartDateTime).addDays(-$numDaysCSVToKeep) -and -not $_.psiscontainer} $oldCsvFilesToDelete | %{Remove-Item $_.FullName -force} Return ($oldCsvFilesToDelete | Measure-Object).Count } ################################################################################################## ### FUNCTION: Discover An RWDC From An AD Domain Function DiscoverRWDC($fqdnADdomain) { $contextADDomain = $NULL $contextADDomain = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext("Domain",$fqdnADdomain) $dnsHostNameRWDC = $NULL $ErrorActionPreference = "SilentlyContinue" $dnsHostNameRWDC = ([System.DirectoryServices.ActiveDirectory.DomainController]::findone($contextADDomain)).Name $ErrorActionPreference = "Continue" If ($dnsHostNameRWDC -eq $null) { Return "DOMAIN_DOES_NOT_EXIST_OR_CANNOT_FIND_DC" } Else { Return $dnsHostNameRWDC } } ################################################################################################## ### FUNCTION: Check If An OU/Container Exists Function CheckDNExistence($dnsHostNameRWDC,$DN) { Try { If([ADSI]::Exists("LDAP://$dnsHostNameRWDC/$DN")) { Return "SUCCESS" } Else { Return "ERROR" } ` } Catch { Return "ERROR" } } ################################################################################################## ### FUNCTION: Test Connection To A Server Function TestConnectionToServer($dnsHostName,$port) { $tcpPortSocket = New-Object System.Net.Sockets.TcpClient $timeOut = "500" $portConnect = $tcpPortSocket.BeginConnect($dnsHostName,$port,$null,$null) $tcpPortWait = $portConnect.AsyncWaitHandle.WaitOne($timeOut,$false) If(!$tcpPortWait) { $tcpPortSocket.Close() Return "ERROR" } Else { $ErrorActionPreference = "SilentlyContinue" $tcpPortSocket.EndConnect($portConnect) | Out-Null If (!$?) { Return "ERROR" } Else { Return "SUCCESS" } $tcpPortSocket.Close() $ErrorActionPreference = "Continue" } } ################################################################################################## ### FUNCTION: Decode Functional Level Function DecodeFunctionalLevel($dfl) { Switch ($dfl) { 0 {"Windows 2000"} 1 {"Windows 2003 Interim"} 2 {"Windows 2003"} 3 {"Windows 2008"} 4 {"Windows 2008 R2"} 5 {"Windows 2012"} 6 {"Windows 2012 R2"} #7 {"TBD"} #8 {"TBD"} #9 {"TBD"} #10 {"TBD"} Default {"If You See This, Something Is Wrong!"} } } ################################################################################################## ##################################### SCRIPT FUNCTIONS END ####################################### ################################################################################################## ### Clear The Screen Clear-Host ### Configure The Appropriate Screen And Buffer Size To Make Sure Everything Fits Nicely $uiConfig = (Get-Host).UI.RawUI $uiConfig.WindowTitle = "+++ AD PASSWORD EXPIRY NOTIFICATION +++" $uiConfig.ForegroundColor = "Yellow" $uiConfigBufferSize = $uiConfig.BufferSize $uiConfigBufferSize.Width = 500 $uiConfigBufferSize.Height = 9999 $uiConfigScreenSizeMax = $uiConfig.MaxPhysicalWindowSize $uiConfigScreenSizeMaxWidth = $uiConfigScreenSizeMax.Width $uiConfigScreenSizeMaxHeight = $uiConfigScreenSizeMax.Height $uiConfigScreenSize = $uiConfig.WindowSize If ($uiConfigScreenSizeMaxWidth -lt 160) { $uiConfigScreenSize.Width = $uiConfigScreenSizeMaxWidth } Else { $uiConfigScreenSize.Width = 160 } If ($uiConfigScreenSizeMaxHeight -lt 75) { $uiConfigScreenSize.Height = $uiConfigScreenSizeMaxHeight - 5 } Else { $uiConfigScreenSize.Height = 75 } $uiConfig.BufferSize = $uiConfigBufferSize $uiConfig.WindowSize = $uiConfigScreenSize ### Script Configuration File If ($xmlconfigfilepath -eq $null -or $xmlconfigfilepath -eq "") { $currentScriptFolderPath = Split-Path $MyInvocation.MyCommand.Definition [string]$scriptXMLConfigFilePath = Join-Path $currentScriptFolderPath "AD-Pwd-Exp-Notify.xml" } Else { [string]$scriptXMLConfigFilePath = $xmlconfigfilepath } ### Start Time Of Script In UTC $execStartDateTime = (Get-Date -format $formatDateTime) $execStartDateTimeForFileSystem = (Get-Date $execStartDateTime -format "yyyy-MM-dd_HH-mm-ss") ### Read The Config File If (!(Test-Path $scriptXMLConfigFilePath)) { Write-Host "The XML Config File '$scriptXMLConfigFilePath' CANNOT Be Found!..." -ForeGroundColor Red Write-Host "Aborting Script..." -ForeGroundColor Red EXIT } Else { [XML]$global:configADPwdExpNotify = Get-Content $scriptXMLConfigFilePath #Write-Host "The XML Config File '$scriptXMLConfigFilePath' Has Been Found!..." -ForeGroundColor Green #Write-Host "Continuing Script..." -ForeGroundColor Green #Write-Host "" } ### Read The Properties From The XML Config File $executionMode = $configADPwdExpNotify.ADPwdExpNotifyConfig.executionMode $mailFromSender = $configADPwdExpNotify.ADPwdExpNotifyConfig.mailFromSender $toSMTPAddressInTestMode = $configADPwdExpNotify.ADPwdExpNotifyConfig.toSMTPAddressInTestMode $smtpServer = $configADPwdExpNotify.ADPwdExpNotifyConfig.smtpServer $mailPriority = $configADPwdExpNotify.ADPwdExpNotifyConfig.mailPriority $mailSubject = $configADPwdExpNotify.ADPwdExpNotifyConfig.mailSubject $htmlBodyFiles = $configADPwdExpNotify.ADPwdExpNotifyConfig.htmlBodyFiles.htmlBodyFile $pwdChangeOrResetURL = $configADPwdExpNotify.ADPwdExpNotifyConfig.pwdChangeOrResetURL $logToScreen = $configADPwdExpNotify.ADPwdExpNotifyConfig.logToScreen $logToFile = $configADPwdExpNotify.ADPwdExpNotifyConfig.logToFile $fullPathToLogFile = $configADPwdExpNotify.ADPwdExpNotifyConfig.fullPathToLogFile -replace ".log","_$execStartDateTimeForFileSystem.log" $folderLogFile = Split-Path $fullPathToLogFile $numDaysLOGToKeep = $configADPwdExpNotify.ADPwdExpNotifyConfig.numDaysLOGToKeep $exportToCSV = $configADPwdExpNotify.ADPwdExpNotifyConfig.exportToCSV $fullPathToCSVFile = $configADPwdExpNotify.ADPwdExpNotifyConfig.fullPathToCSVFile -replace ".csv","_$execStartDateTimeForFileSystem.csv" $folderCsvFile = Split-Path $fullPathToCSVFile $numDaysCSVToKeep = $configADPwdExpNotify.ADPwdExpNotifyConfig.numDaysCSVToKeep $formatDateTime = $configADPwdExpNotify.ADPwdExpNotifyConfig.formatDateTime $domains = $configADPwdExpNotify.ADPwdExpNotifyConfig.domains.domain $daysBeforeWarn = $configADPwdExpNotify.ADPwdExpNotifyConfig.daysBeforeWarn.Period Logging "#######################################################################################" Logging " *********************************************************" Logging " * *" Logging " * --> ACTIVE DIRECTORY PASSWORD EXPIRY NOTIFICATION <-- *" Logging " * *" Logging " * ($scriptVersion) ($scriptDate) *" Logging " * *" Logging " * Written By: Jorge de Almeida Pinto [MVP-DS] *" Logging " * *" Logging " * BLOG: 'Jorge's Quest For Knowledge' *" Logging " * (https://jorgequestforknowledge.wordpress.com/) *" Logging " * *" Logging " *********************************************************" Logging "" Logging "Starting Date And Time...........: $execStartDateTime" Logging "" Logging "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" ### Logging All Configured Settings Logging "" Logging "XML Config File Path.............: $scriptXMLConfigFilePath" Logging "" If (!$force) { $executionMode = "TEST (NO MAILINGS)" } Logging "Execution Mode...................: $executionMode" Logging "" Logging "Log To Screen....................: $logToScreen" Logging "" Logging "Log To File......................: $logToFile" Logging "" Logging "Log File Full Path...............: $fullPathToLogFile" Logging "" Logging "Log Files Folder.................: $folderLogFile" Logging "" Logging "Number Of Days Of Logs To Keep...: $numDaysLOGToKeep" Logging "" Logging "Export List Of User To CSV.......: $exportToCSV" Logging "" Logging "CSV File Full Path...............: $fullPathToCSVFile" Logging "" Logging "CSV Files Folder.................: $folderCsvFile" Logging "" Logging "Number Of Days Of CSVs To Keep...: $numDaysCSVToKeep" Logging "" $smtpServerStatus = $NULL $smtpServerStatus = TestConnectionToServer $smtpServer "25" Logging "SMTP Server......................: $smtpServer (Status: $smtpServerStatus)" Logging "" If ($smtpServerStatus.ToUpper() -eq "ERROR") { EXIT } Logging "Sender Address...................: $mailFromSender" Logging "" If ($executionMode.ToUpper() -eq "TEST (NO MAILINGS)") { Logging "Recipient Address................: None" } If ($executionMode.ToUpper() -eq "DEV" -Or $executionMode.ToUpper() -eq "TEST") { Logging "Recipient Address................: $toSMTPAddressInTestMode" } If ($executionMode.ToUpper() -eq "PROD") { Logging "Recipient Address................: Individual Users" } Logging "" Logging "Message Priority.................: $mailPriority" $htmlBodyFiles | %{ $language = $_.language $mailSubject = $_.mailSubject $fullPath = $_.fullPath Logging "" Logging "Message Subject..................: ($language) $mailSubject" Logging "" Logging "HTML Body File...................: ($language) $fullPath" } Logging "" Logging "Change/Reset PWD URL.............: $pwdChangeOrResetURL" Logging "" Logging "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" ### Cleaning Up Old Log and Csv Files Logging "" Logging "Cleaning Up Old Log Files. Keeping Log Files From Last $numDaysLOGToKeep Days..." $oldLogFilesToDeleteCount = CleanUpLOGFiles $numDaysLOGToKeep Logging " --> Number Of Old Log Files Deleted...: $oldLogFilesToDeleteCount" Logging "" Logging "Cleaning Up Old Csv Files. Keeping Csv Files From Last $numDaysCSVToKeep Days..." $oldCsvFilesToDeleteCount = CleanUpCSVFiles $numDaysCSVToKeep Logging " --> Number Of Csv Log Files Deleted...: $oldCsvFilesToDeleteCount" Logging "" Logging "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" ### Creating An Empty Array For All Queried Users (Not Necessarily The Same List Of Users That Will Be Notified!) $listOfQueriedUsers = @() ### Processing Each Configured AD Domain In The XML Config File Logging "" Logging "Processing Configured AD Domains..." ### Go Through Every Configured AD Domain $domains | %{ ### The FQDN Of The AD Domain From The XML Config File $fqdnADdomain = $_.FQDN Logging "" Logging "** AD Domain: $fqdnADdomain **" ### The FQDN Of The DC From The XML Config File $fqdnDC = $_.DC ### If DISCOVER Was Specified Instead Of A Specific (Static) DC, Then Discover The Nearest RWDC And Use That One If ($fqdnDC.ToUpper() -eq "DISCOVER") { $fqdnDC = DiscoverRWDC $fqdnADdomain ### Check If The RWDC Is Available If ($fqdnDC -eq "DOMAIN_DOES_NOT_EXIST_OR_CANNOT_FIND_DC") { $dcStatus = "ERROR" Logging "" Logging " --> FQDN DC: $fqdnDC (Discovered) (Status: $dcStatus)" } Else { $dcStatus = $NULL $dcStatus = TestConnectionToServer $fqdnDC "389" Logging "" Logging " --> FQDN DC: $fqdnDC (Discovered) (Status: $dcStatus)" } } Else { ### Check If The RWDC Is Available $dcStatus = $NULL $dcStatus = TestConnectionToServer $fqdnDC "389" Logging "" Logging " --> FQDN DC: $fqdnDC (Static) (Status: $dcStatus)" } ### If There Is Something Wrong With The RWDC, Then Abort Processing For This AD Domain And Send Mail About It If ($dcStatus -eq "ERROR" -And $fqdnDC -ne "DOMAIN_DOES_NOT_EXIST_OR_CANNOT_FIND_DC") { Logging " --> SKIPPED DUE TO ERROR - UNABLE TO CONTACT DC!" Logging "" Send-MailMessage -SmtpServer $smtpServer -From $mailFromSender -To $toSMTPAddressInTestMode -Priority High -Subject "Error With DC From AD Domain!" -Body "There Are Connectivity Issues With The DC '$fqdnDC' From The AD Domain '$fqdnADdomain'!" -BodyAsHtml } If ($dcStatus -eq "ERROR" -And $fqdnDC -eq "DOMAIN_DOES_NOT_EXIST_OR_CANNOT_FIND_DC") { Logging " --> SKIPPED DUE TO ERROR - DOMAIN DOES NOT EXIST OR CANNOT FIND DC!" Logging "" Send-MailMessage -SmtpServer $smtpServer -From $mailFromSender -To $toSMTPAddressInTestMode -Priority High -Subject "Error With DC From AD Domain!" -Body "The AD Domain '$fqdnADdomain' Does Not Exist Or Unable To Discover A DC For The AD Domain '$fqdnADdomain'!" -BodyAsHtml } ### If The AD Domain Does Exist And The DC Can Be Discovered And It Can Be Contacted If ($dcStatus -eq "SUCCESS") { ### Connect To The RootDSE Of The RWDC And Get Info From It $RootDSE = [ADSI]"LDAP://$fqdnDC/RootDSE" $dfl = $RootDSE.domainFunctionality $defaultNC = $RootDSE.defaultNamingContext Logging "" Logging " --> DFL: $dfl ($(DecodeFunctionalLevel $dfl))" Logging "" Logging " --> Default NC: $defaultNC" ### PWD Policies From The AD Domain ### If Domain Functional Level Is At Least 3 (Windows 2008) Or Higher Then Check For Any Configured Password Settings Object (PSO) And Get The Settings For Each PSO If ($dfl -ge 3) { Logging "" Logging " --> PSOs In AD Domain" ### PSO Container (REMEMBER: The Account Running This Script Must Have Allow:Read Permissions On The PSO Container Itself And Sub Objects $psoContainerDN = "CN=Password Settings Container,CN=System,$defaultNC" ### Setup The LDAP Query To Get All PSOs And Execute The Query $searchRoot = $NULL $searchRoot = [ADSI]"LDAP://$fqdnDC/$psoContainerDN" $searcher = New-Object System.DirectoryServices.DirectorySearcher($searchRoot) $searcher.Filter = "(objectClass=msDS-PasswordSettings)" $searcher.SearchScope = "Subtree" $searcher.PageSize = 1000 $propertyList = "distinguishedName","name","msDS-MaximumPasswordAge","msDS-MinimumPasswordAge","msDS-MinimumPasswordLength","msDS-PasswordComplexityEnabled","msDS-PasswordHistoryLength" ForEach ($property in $propertyList){ $searcher.PropertiesToLoad.Add($property) | Out-Null } $results = $NULL $results = $searcher.FindAll() ### For Every Discovered PSO Get Its Properties (REMEMBER: The Account Running This Script Must Have Allow:Read Permissions On The PSO Container Itself And Sub Objects If ($results -ne $null) { $pwdPolicyInDomain = @() $results | %{ $pwdPolicyPSOInDomainObj = "" | Select DN,name,MaxPwdAge,MinPwdAge,MinPwdLength,PwdComplexity,PwdHistoryLength $pwdPolicyPSOInDomainObj.DN = $_.Properties.distinguishedname[0] $psoName = $_.Properties.name[0] $pwdPolicyPSOInDomainObj.name = $($psoName + " (" + $fqdnADdomain + ")") Logging "" Logging " --> Name............: $psoName" $psoMaxPwdAge = [System.TimeSpan]::FromTicks([System.Math]::ABS($_.Properties."msds-maximumpasswordage"[0])).Days $pwdPolicyPSOInDomainObj.MaxPwdAge = $psoMaxPwdAge Logging " --> Max Pwd Age.....: $psoMaxPwdAge" $psoMinPwdAge = [System.TimeSpan]::FromTicks([System.Math]::ABS($_.Properties."msds-minimumpasswordage"[0])).Days $pwdPolicyPSOInDomainObj.MinPwdAge = $psoMinPwdAge Logging " --> Min Pwd Age.....: $psoMinPwdAge" $psoMinPwdLength = $_.Properties."msds-minimumpasswordlength"[0] $pwdPolicyPSOInDomainObj.MinPwdLength = $psoMinPwdLength Logging " --> Min Pwd Length..: $psoMinPwdLength" If ($_.Properties."msds-passwordcomplexityenabled"[0]) { $pwdPolicyPSOInDomainObj.PwdComplexity = "TRUE" Logging " --> Pwd Complexity..: TRUE" } Else { $pwdPolicyPSOInDomainObj.PwdComplexity = "FALSE" Logging " --> Pwd Complexity..: FALSE" } $psoPwdHistoryLength = $_.Properties."msds-passwordhistorylength"[0] $pwdPolicyPSOInDomainObj.PwdHistoryLength = $psoPwdHistoryLength Logging " --> Pwd Complexity..: $psoPwdHistoryLength" $pwdPolicyInDomain += $pwdPolicyPSOInDomainObj } } $searcher = $NULL $results = $NULL } ### Get The Password Policy Settings From The Default Domain GPO Which Are Also Registered On The AD Domain NC Head Logging "" Logging " --> Default Domain GPO Password Settings" ### Setup The LDAP Query To Get The Password Policy Settings From The Default Domain GPO And Execute The Query $searchRoot = $NULL $searchRoot = [ADSI]"LDAP://$fqdnDC/$defaultNC" $searcher = New-Object System.DirectoryServices.DirectorySearcher($searchRoot) $searcher.SearchScope = "Base" $propertyList = "maxPwdAge","minPwdAge","minPwdLength","pwdProperties","pwdHistoryLength" ForEach ($property in $propertyList){ $searcher.PropertiesToLoad.Add($property) | Out-Null } $results = $NULL $results = $searcher.FindOne() ### Get The Properties And Process Them $results | %{ $pwdPolicyGPOInDomainObj = "" | Select DN,name,MaxPwdAge,MinPwdAge,MinPwdLength,PwdComplexity,PwdHistoryLength $pwdPolicyGPOInDomainObj.DN = $defaultNC[0] $gpoName = "DefaultDomainGPO (" + $fqdnADdomain + ")" $pwdPolicyGPOInDomainObj.name = $gpoName Logging "" Logging " --> Name............: $gpoName" $gpoMaxPwdAge = [System.TimeSpan]::FromTicks([System.Math]::ABS($_.Properties.maxpwdage[0])).Days $pwdPolicyGPOInDomainObj.MaxPwdAge = $gpoMaxPwdAge Logging " --> Max Pwd Age.....: $gpoMaxPwdAge" $gpoMinPwdAge = [System.TimeSpan]::FromTicks([System.Math]::ABS($_.Properties.minpwdage[0])).Days $pwdPolicyGPOInDomainObj.MinPwdAge = $gpoMinPwdAge Logging " --> Min Pwd Age.....: $gpoMinPwdAge" $psoMinPwdLength = $_.Properties.minpwdlength[0] $pwdPolicyGPOInDomainObj.MinPwdLength = $psoMinPwdLength Logging " --> Min Pwd Length..: $psoMinPwdLength" If (($results.Properties.pwdproperties[0] -band 0x1) -Eq 1) { $pwdPolicyGPOInDomainObj.PwdComplexity = "TRUE" Logging " --> Pwd Complexity..: TRUE" } Else { $pwdPolicyGPOInDomainObj.PwdComplexity = "FALSE" Logging " --> Pwd Complexity..: FALSE" } $gpoPwdHistoryLength = $_.Properties.pwdhistorylength[0] $pwdPolicyGPOInDomainObj.PwdHistoryLength = $gpoPwdHistoryLength Logging " --> Pwd Complexity..: $gpoPwdHistoryLength" $pwdPolicyInDomain += $pwdPolicyGPOInDomainObj } $searcher = $NULL $results = $NULL Logging "" Logging " --> Search Bases In AD Domain" ### Processing Each Configured Search Base Within An AD Domain In The XML Config File $searchBases = $_.searchBase $searchBases | %{ $searchBase = $_."#text" $languageForUser = $_.language $mailSubjectForUser = ($htmlBodyFiles | ?{$_.language -eq $languageForUser}).mailSubject $htmlBodyFileForUser = ($htmlBodyFiles | ?{$_.language -eq $languageForUser}).fullPath Logging "" ### Let's Make Sure The Configured Search Base Does Exist $searchBaseStatus = $NULL $searchBaseStatus = CheckDNExistence $fqdnDC $searchBase Logging " --> Search Base..........: $searchBase (Status: $searchBaseStatus)" ### If The Search Base Does Exist Then Continue If ($searchBaseStatus -eq "SUCCESS") { ### Setup The LDAP Query To Get The User Objects And Execute The Query $searchRoot = $NULL $searchRoot = [ADSI]"LDAP://$fqdnDC/$searchBase" $searcher = New-Object System.DirectoryServices.DirectorySearcher($searchRoot) $searcher.Filter = "(&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(userAccountControl:1.2.840.113556.1.4.803:=65536))(mail=*)(!(pwdLastSet=0)))" $searcher.SearchScope = "Subtree" $searcher.PageSize = 1000 $propertyList = "distinguishedName","givenName","sn","displayName","mail","pwdLastSet","msDS-UserPasswordExpiryTimeComputed","accountExpires","msDS-ResultantPSO" ForEach ($property in $propertyList){ $searcher.PropertiesToLoad.Add($property) | Out-Null } $results = $NULL $results = $searcher.FindAll() $userCountInSearchBase = ($results | Measure-Object).Count Logging " --> Queried User Count...: $userCountInSearchBase" Logging " --> Specified Language...: $languageForUser" ### Get The Properties And Process Them $results | %{ $listOfQueriedUsersObj = "" | Select "FQDN AD Domain",DN,"Given Name","Last Name","Display Name","E-Mail Address","PWD Last Set","PWD Expire Date","Days Until PWD Expiry","Account Expiry Date","Days Until Account Expiry","Effective PWD Policy","Language","Mail Subject","HTML Body File" $listOfQueriedUsersObj."FQDN AD Domain" = $fqdnADdomain $listOfQueriedUsersObj.DN = $_.Properties.distinguishedname[0] If ($_.Properties.givenname -ne $null) { $listOfQueriedUsersObj."Given Name" = $_.Properties.givenname[0] } Else { $listOfQueriedUsersObj."Given Name" = $null } If ($_.Properties.sn -ne $null) { $listOfQueriedUsersObj."Last Name" = $_.Properties.sn[0] } Else { $listOfQueriedUsersObj."Last Name" = $null } If ($_.Properties.displayname -ne $null) { $listOfQueriedUsersObj."Display Name" = $_.Properties.displayname[0] } Else { $listOfQueriedUsersObj."Display Name" = $null } $listOfQueriedUsersObj."E-Mail Address" = $_.Properties.mail[0] $adUserPwdLastSet = Get-Date -Date ([DateTime]::FromFileTime([Int64]::Parse($_.Properties.pwdlastset[0]))) -Format $formatDateTime $listOfQueriedUsersObj."PWD Last Set" = $adUserPwdLastSet $adUserPwdExpires = Get-Date -Date ([DateTime]::FromFileTime([Int64]::Parse($_.Properties."msds-userpasswordexpirytimecomputed"[0]))) -Format $formatDateTime $listOfQueriedUsersObj."PWD Expire Date" = $adUserPwdExpires $timeDiffPwdExpiryInDays = (New-TimeSpan -Start $execStartDateTime -end $adUserPwdExpires).TotalDays $listOfQueriedUsersObj."Days Until PWD Expiry" = $timeDiffPwdExpiryInDays $adUserAccountExpires = $_.Properties.accountexpires[0] ### If An Account Is Configured With Never Expires, Then Assign An Insane End Date To Be Able To Perform Calculations If ($adUserAccountExpires -eq 9223372036854775807 -Or $adUserAccountExpires -eq 0) { $adUserAccountExpires = Get-Date "9999-12-31 23:59:59" -format $formatDateTime } Else { $adUserAccountExpires = Get-Date -Date ([DateTime]::FromFileTime([Int64]::Parse($adUserAccountExpires))) -Format $formatDateTime } $listOfQueriedUsersObj."Account Expiry Date" = $adUserAccountExpires $timeDiffAccountExpiryInDays = (New-TimeSpan -Start $execStartDateTime -end $adUserAccountExpires).TotalDays $listOfQueriedUsersObj."Days Until Account Expiry" = $timeDiffAccountExpiryInDays If ($_.Properties."msds-resultantpso" -ne $null) { $effectivePWDPolicyDN = $_.Properties."msds-resultantpso"[0] } Else { $effectivePWDPolicyDN = $defaultNC } $effectivePWDPolicyName = ($pwdPolicyInDomain | ?{$_.DN -eq $effectivePWDPolicyDN}).name $listOfQueriedUsersObj."Effective PWD Policy" = $effectivePWDPolicyName $listOfQueriedUsersObj."Language" = $languageForUser $listOfQueriedUsersObj."Mail Subject" = $mailSubjectForUser $listOfQueriedUsersObj."HTML Body File" = $htmlBodyFileForUser $listOfQueriedUsers += $listOfQueriedUsersObj } $searcher = $NULL $results = $NULL } Else { ### If The Search Base Does NOT Exist Then Skip That Search Base And Send Mail About It Logging " --> SKIPPED DUE TO ERROR!" Send-MailMessage -SmtpServer $smtpServer -From $mailFromSender -To $toSMTPAddressInTestMode -Priority High -Subject "Error With Defined SearchBase!" -Body "The Search Base '$searchBase' Does Not Exist!" -BodyAsHtml } } } } Logging "" Logging "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" ### Now Having The List Of Queried Users, Determine Which Of Those Users Require E-mail Notification Based Upon The Configured Warning Periods Logging "" Logging "Processing Warning Periods..." ### From The List Of Queried Users, Get Those Users For Which The AD User Account Has Not Expired Yet $listOfUsersWithNonExpiredAccounts = $listOfQueriedUsers | ?{$_."Days Until Account Expiry" -gt 0} ### Creating An Empty Array For Users That Will Be Notified! $listOfUsersWithExpiringPWDToNotify = @() ### Process Every Configured Warning Period. Make Sure In The XML NOT To Have Overlapping Periods! $daysBeforeWarn | %{ $max = $_.max $min = $_.min Logging "" Logging "** Period: Max: $max Days | Min: $min Days **" $listOfUsersWithinWarningPeriod = $listOfUsersWithNonExpiredAccounts | ?{$_."Days Until PWD Expiry" -lt $max -And $_."Days Until PWD Expiry" -gt $min} $userCountInWarningPeriod = ($listOfUsersWithinWarningPeriod | Measure-Object).Count Logging " --> User Count Within Warning Period...: $userCountInWarningPeriod" $listOfUsersWithExpiringPWDToNotify += $listOfUsersWithinWarningPeriod } ### If If Was Configured To Export The List Of Users That Will Be Notified, Than Do So! If ($exportToCSV.ToUpper() -eq "ON") { $listOfUsersWithExpiringPWDToNotify | Export-Csv -Path $fullPathToCSVFile -NoTypeInformation } #Logging "" #Logging "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" ### Show On Screen The List Of Queried Users Including Some Details #Logging "" #Logging "List Of Queried Users..." #$listOfUsersWithNonExpiredAccounts | FT "FQDN AD Domain",DN,"Given Name","Last Name","Display Name","E-Mail Address","PWD Last Set","PWD Expire Date","Days Until PWD Expiry","Account Expiry Date","Days Until Account Expiry","Effective PWD Policy","Language","Mail Subject","HTML Body File" -Autosize #$listOfUsersWithNonExpiredAccounts | FT "FQDN AD Domain","Display Name","E-Mail Address","PWD Expire Date","Days Until PWD Expiry","Effective PWD Policy","Language" -Autosize #$userCountQueried = ($listOfUsersWithNonExpiredAccounts | Measure-Object).Count #Logging "--> User Count To Be Queried....: $userCountQueried" Logging "" Logging "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" ### Show On Screen The List Of Users That Will Be Notified, Including Some Details Logging "" Logging "List Of Notified Users..." #$listOfUsersWithExpiringPWDToNotify | FT "FQDN AD Domain",DN,"Given Name","Last Name","Display Name","E-Mail Address","PWD Last Set","PWD Expire Date","Days Until PWD Expiry","Account Expiry Date","Days Until Account Expiry","Effective PWD Policy","Language","Mail Subject","HTML Body File" -Autosize $listOfUsersWithExpiringPWDToNotify | FT "FQDN AD Domain","Display Name","E-Mail Address","PWD Expire Date","Days Until PWD Expiry","Effective PWD Policy","Language" -Autosize $userCountNotified = ($listOfUsersWithExpiringPWDToNotify | Measure-Object).Count Logging "--> User Count To Be Notified...: $userCountNotified" ### If The FORCE Parameter Was NOT Specified With TRUE Then DO NOT Send Any E-Mail If ($executionMode.ToUpper() -eq "TEST (NO MAILINGS)") { Logging "" Logging " --> No Notifications Have Been Send!" } Else { ### When Running In DEV Mode Execute This Part If ($executionMode.ToUpper() -eq "DEV") { Logging "" Logging "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" Logging "" Logging "Displaying Information Of The Development User..." ### Get The Current AD Domain $ThisADDomain = [DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() $fqdnThisADDomain = $ThisADDomain.Name ### Discover An RWDC For That Current AD Domain $fqdnDC = DiscoverRWDC $fqdnThisADDomain ### Setup The LDAP Query To Get The Information Of The User And Execute The Query $RootDSE = [ADSI]"LDAP://$fqdnDC/RootDSE" $defaultNC = $RootDSE.defaultNamingContext $searchRoot = $NULL $searchRoot = [ADSI]"LDAP://$fqdnDC/$defaultNC" $searcher = New-Object System.DirectoryServices.DirectorySearcher($searchRoot) $searcher.Filter = "(&(objectCategory=person)(objectClass=user)(|(proxyAddresses=smtp:$toSMTPAddressInTestMode)(proxyAddresses=SMTP:$toSMTPAddressInTestMode)))" $searcher.SearchScope = "Subtree" $searcher.PageSize = 1000 $propertyList = "distinguishedName","givenName","sn","displayName","pwdLastSet","msDS-UserPasswordExpiryTimeComputed","msDS-ResultantPSO" ForEach ($property in $propertyList){ $searcher.PropertiesToLoad.Add($property) | Out-Null } $results = $NULL $results = $searcher.FindOne() ### Get The Properties Of The User If ($results.Properties.givenname -ne $null) { $adUserGivenName = $results.Properties.givenname[0] } Else { $adUserGivenName = "NO-VALUE" } If ($results.Properties.sn -ne $null) { $adUserSn = $results.Properties.sn[0] } Else { $adUserSn = "NO-VALUE" } If ($results.Properties.displayname -ne $null) { $adUserDisplayName = $results.Properties.displayname[0] } Else { $adUserDisplayName = "NO-VALUE" } $adUserPwdLastSet = Get-Date -Date ([DateTime]::FromFileTime([Int64]::Parse($results.Properties.pwdlastset[0]))) -Format $formatDateTime $adUserPwdExpires = $results.Properties."msds-userpasswordexpirytimecomputed"[0] ### If A Password Is Configured With Never Expires, Then Assign An Insane End Date To Be Able To Perform Calculations If ($adUserPwdExpires -eq 9223372036854775807 -Or $adUserAccountExpires -eq 0) { $adUserPwdExpires = Get-Date "9999-12-31 23:59:59" -format $formatDateTime } Else { $adUserPwdExpires = Get-Date -Date ([DateTime]::FromFileTime([Int64]::Parse($adUserPwdExpires))) -Format $formatDateTime } $timeDiffPwdExpiryInDays = (New-TimeSpan -Start $execStartDateTime -end $adUserPwdExpires).TotalDays If ($results.Properties."msds-resultantpso" -ne $null) { $effectivePWDPolicyDN = $results.Properties."msds-resultantpso"[0] } Else { $effectivePWDPolicyDN = $defaultNC } $effectivePWDPolicyOnUser = $pwdPolicyInDomain | ?{$_.DN -eq $effectivePWDPolicyDN} ### Get The Settings Of The Effective PWD Policy On The User $policyPWDName = $effectivePWDPolicyOnUser.Name $policyPWDMinLength = $effectivePWDPolicyOnUser.MinPwdLength $policyPWDMinAge = $effectivePWDPolicyOnUser.MinPwdAge $policyPWDMaxAge = $effectivePWDPolicyOnUser.MaxPwdAge $policyPWDHistory = $effectivePWDPolicyOnUser.PwdHistoryLength $policyPWDComplexity = $effectivePWDPolicyOnUser.PwdComplexity ### Get The Content Of The HTML File That Will Be Used For The E-Mails $languageForUser = "default" $mailSubjectForUser = ($htmlBodyFiles | ?{$_.language -eq $languageForUser}).mailSubject $htmlBodyFileForUser = ($htmlBodyFiles | ?{$_.language -eq $languageForUser}).fullPath $mailBodyForUser = Get-Content $htmlBodyFileForUser Logging "" Logging "Display Name................: $adUserDisplayName" Logging "First Name..................: $adUserGivenName" Logging "Last Name...................: $adUserSn" Logging "PWD Last Set................: $adUserPwdLastSet" Logging "PWD Expiry Date.............: $adUserPwdExpires" Logging "Days Until PWD Expiry Date..: $timeDiffPwdExpiryInDays ($([math]::Round($timeDiffPwdExpiryInDays)))" Logging "Effective PWD Policy Name...: $policyPWDName" Logging "Effective PWD Min Length....: $policyPWDMinLength" Logging "Effective PWD Min Age.......: $policyPWDMinAge" Logging "Effective PWD Max Age.......: $policyPWDMaxAge" Logging "Effective PWD History.......: $policyPWDHistory" Logging "Effective PWD Complexity....: $policyPWDComplexity" Logging "Language....................: $languageForUser" Logging "Mail Subject................: $mailSubjectForUser" Logging "HTML Body File..............: $htmlBodyFileForUser" ### Replace Any Variables In The SUBJECT With The Actual Values $mailSubject = $mailSubjectForUser -replace "FIRST_NAME",$adUserGivenName $mailSubject = $mailSubject -replace "LAST_NAME",$adUserSn $mailSubject = $mailSubject -replace "DISPLAY_NAME",$adUserDisplayName $mailSubject = $mailSubject -replace "FQDN_DOMAIN",$fqdnThisADDomain $mailSubject = $mailSubject -replace "PWD_EXPIRY_DATE",$adUserPwdExpires $mailSubject = $mailSubject -replace "PWD_EXPIRE_IN_NUM_DAYS",[math]::Round($timeDiffPwdExpiryInDays) $mailSubject = $mailSubject -replace "PWD_MIN_LENGTH",$policyPWDMinLength $mailSubject = $mailSubject -replace "PWD_MIN_AGE",$policyPWDMinAge $mailSubject = $mailSubject -replace "PWD_MAX_AGE",$policyPWDMaxAge $mailSubject = $mailSubject -replace "PWD_HISTORY",$policyPWDHistory $mailSubject = $mailSubject -replace "PWD_COMPLEX",$policyPWDComplexity $mailSubject = $mailSubject -replace "PWD_CHANGE_RESET_URL",$pwdChangeOrResetURL ### Replace Any Variables In The BODY With The Actual Values $mailBody = $mailBodyForUser -replace "FIRST_NAME",$adUserGivenName $mailBody = $mailBody -replace "LAST_NAME",$adUserSn $mailBody = $mailBody -replace "DISPLAY_NAME",$adUserDisplayName $mailBody = $mailBody -replace "FQDN_DOMAIN",$fqdnThisADDomain $mailBody = $mailBody -replace "PWD_EXPIRY_DATE",$adUserPwdExpires $mailBody = $mailBody -replace "PWD_EXPIRE_IN_NUM_DAYS",[math]::Round($timeDiffPwdExpiryInDays) $mailBody = $mailBody -replace "PWD_MIN_LENGTH",$policyPWDMinLength $mailBody = $mailBody -replace "PWD_MIN_AGE",$policyPWDMinAge $mailBody = $mailBody -replace "PWD_MAX_AGE",$policyPWDMaxAge $mailBody = $mailBody -replace "PWD_HISTORY",$policyPWDHistory $mailBody = $mailBody -replace "PWD_COMPLEX",$policyPWDComplexity $mailBody = $mailBody -replace "PWD_CHANGE_RESET_URL",$pwdChangeOrResetURL ### Send A Notification E-Mail About The Expiring Password Send-MailMessage -SmtpServer $smtpServer -From $mailFromSender -To $toSMTPAddressInTestMode -Priority $mailPriority -Subject $mailSubject -Body $($mailBody | Out-String) -BodyAsHtml Logging "" Logging " --> Notifying '$adUserDisplayName' by sending an e-mail to '$toSMTPAddressInTestMode'" } Else { ### When Running In TEST Or PROD Mode Execute This Part ### Process Any User With Expiring Password And Send E-Mail Notification About The Expiring Password $listOfUsersWithExpiringPWDToNotify | %{ ### If The FORCE Parameter Was Specified With TRUE Then Send E-Mail Based On The Configured Execution Mode If ($executionMode.ToUpper() -eq "TEST") { ### For All Users Send E-Mail Notifications To The Configured Admin Mail Address $mailToRecipient = $toSMTPAddressInTestMode } If ($executionMode.ToUpper() -eq "PROD") { ### For All Users Send E-Mail Notifications To The E-Mail Address Of Each User $mailToRecipient = $_."E-Mail Address" } ### Get The Display Name Of The User $displayNameUser = $_."Display Name" ### Get The Effective PWD Policy On The User $effectivePWDPolicyNameOnUser = $_."Effective PWD Policy" $effectivePWDPolicyOnUser = $pwdPolicyInDomain | ?{$_.Name -eq $effectivePWDPolicyNameOnUser} ### Get The Settings Of The Effective PWD Policy On The User $policyPWDMinLength = $effectivePWDPolicyOnUser.MinPwdLength $policyPWDMinAge = $effectivePWDPolicyOnUser.MinPwdAge $policyPWDMaxAge = $effectivePWDPolicyOnUser.MaxPwdAge $policyPWDHistory = $effectivePWDPolicyOnUser.PwdHistoryLength $policyPWDComplexity = $effectivePWDPolicyOnUser.PwdComplexity ### Get The Content Of The HTML File That Will Be Used For The E-Mails $languageForUser = $_.Language $mailSubjectForUser = ($htmlBodyFiles | ?{$_.language -eq $languageForUser}).mailSubject $htmlBodyFileForUser = ($htmlBodyFiles | ?{$_.language -eq $languageForUser}).fullPath $mailBodyForUser = Get-Content $htmlBodyFileForUser ### Replace Any Variables In The SUBJECT With The Actual Values $mailSubject = $mailSubjectForUser -replace "FIRST_NAME",$_."Given Name" $mailSubject = $mailSubject -replace "LAST_NAME",$_."Last Name" $mailSubject = $mailSubject -replace "DISPLAY_NAME",$_."Display Name" $mailSubject = $mailSubject -replace "FQDN_DOMAIN",$_."FQDN AD Domain" $mailSubject = $mailSubject -replace "PWD_EXPIRY_DATE",$_."PWD Expire Date" $mailSubject = $mailSubject -replace "PWD_EXPIRE_IN_NUM_DAYS",[math]::Round($_."Days Until PWD Expiry") $mailSubject = $mailSubject -replace "PWD_MIN_LENGTH",$policyPWDMinLength $mailSubject = $mailSubject -replace "PWD_MIN_AGE",$policyPWDMinAge $mailSubject = $mailSubject -replace "PWD_MAX_AGE",$policyPWDMaxAge $mailSubject = $mailSubject -replace "PWD_HISTORY",$policyPWDHistory $mailSubject = $mailSubject -replace "PWD_COMPLEX",$policyPWDComplexity $mailSubject = $mailSubject -replace "PWD_CHANGE_RESET_URL",$pwdChangeOrResetURL ### Replace Any Variables In The BODY With The Actual Values $mailBody = $mailBodyForUser -replace "FIRST_NAME",$_."Given Name" $mailBody = $mailBody -replace "LAST_NAME",$_."Last Name" $mailBody = $mailBody -replace "DISPLAY_NAME",$_."Display Name" $mailBody = $mailBody -replace "FQDN_DOMAIN",$_."FQDN AD Domain" $mailBody = $mailBody -replace "PWD_EXPIRY_DATE",$_."PWD Expire Date" $mailBody = $mailBody -replace "PWD_EXPIRE_IN_NUM_DAYS",[math]::Round($_."Days Until PWD Expiry") $mailBody = $mailBody -replace "PWD_MIN_LENGTH",$policyPWDMinLength $mailBody = $mailBody -replace "PWD_MIN_AGE",$policyPWDMinAge $mailBody = $mailBody -replace "PWD_MAX_AGE",$policyPWDMaxAge $mailBody = $mailBody -replace "PWD_HISTORY",$policyPWDHistory $mailBody = $mailBody -replace "PWD_COMPLEX",$policyPWDComplexity $mailBody = $mailBody -replace "PWD_CHANGE_RESET_URL",$pwdChangeOrResetURL ### Send A Notification E-Mail About The Expiring Password Send-MailMessage -SmtpServer $smtpServer -From $mailFromSender -To $mailToRecipient -Priority $mailPriority -Subject $mailSubject -Body $($mailBody | Out-String) -BodyAsHtml Logging "" Logging " --> Notifying '$displayNameUser' by sending an e-mail to '$mailToRecipient'" } } } Logging "" Logging "#######################################################################################"

You can download the most recent PowerShell version from HERE.

I HAVE NOT TESTED EVERY POSSIBLE SCENARIO! Please provide feedback through the comments section OR you the contact page

DISCLAIMER (READ THIS!):

  • I wrote this script, therefore I own it. Anyone asking money for it, should NOT be doing that and is basically ripping you off!
  • The script is freeware, you are free to use it and distribute it, but always refer to this website (https://jorgequestforknowledge.wordpress.com/) as the location where you got it.
  • This script is furnished "AS IS". No warranty is expressed or implied!
  • I have NOT tested it in every scenario nor have I tested it against every Windows and/or AD version
  • Always test first in lab environment to see if it meets your needs!
  • Use this script at your own risk!
  • I do not warrant this script to be fit for any purpose, use or environment!
  • I have tried to check everything that needed to be checked, but I do not guarantee the script does not have bugs!
  • I do not guarantee the script will not damage or destroy your system(s), environment or whatever!
  • I do not accept liability in any way if you screw up, use the script wrong or in any other way where damage is caused to your environment/systems!
  • If you do not accept these terms do not use the script in any way and delete it immediately!

Cheers,

Jorge

———————————————————————————————

* This posting is provided "AS IS" with no warranties and confers no rights!

* Always evaluate/test yourself before using/implementing this!

* DISCLAIMER:

https://jorgequestforknowledge.wordpress.com/disclaimer/

———————————————————————————————

############### Jorge’s Quest For Knowledge #############

#########

http://JorgeQuestForKnowledge.wordpress.com/ ########

———————————————————————————————

Posted in Active Directory Domain Services (ADDS), Fine Grained Password Policies, Password Expiration Notification, PowerShell, Tooling/Scripting | 5 Comments »

(2015-09-22) Notifying Users By E-mail Their Password Is Going To Expire (Update 3)

Posted by Jorge on 2015-09-22


UPDATE 2015-10-18: see https://jorgequestforknowledge.wordpress.com/2015/10/18/notifying-users-by-e-mail-their-password-is-going-to-expire-update-4/

Almost 6 years ago I wrote a blog post about and also wrote a tool to notify users through e-mail when their password was going to expire. You can read all the details about the idea here. Now that tool was very inflexible and because of that I received numerous requests to make it more flexible such as the ability to customize the e-mail message. With this blog post I’m sharing a brand new tool, based upon PowerShell, that will notify users through e-mail when their password is going to expire. So let’s get started in explaining on this works! I did not test all combinations! However, I do expect it to run on any Windows version as long as PowerShell is available. It should also work against any AD version and there is NO dependency on using the AD PowerShell CMDlets!. Everything is done through ADSI to be independent of Windows versions! It will also support PSOs if the DFL is high enough and PSOs are configured!

SYNTAX:

  • <PoSH Script File> –> Runs The Script In Test Mode While NOT Sending Any E-Mails
  • <PoSH Script File> -force:$true –> Runs The Script In DEV (One Mail To Configured Admin) Or TEST (All Mails To Configured Admin) Or PROD (Mails To Users) Mode While Sending Any E-Mails

Please provide feedback through the comments section OR you the contact page

DISCLAIMER (READ THIS!):

  • I wrote this script, therefore I own it. Anyone asking money for it, should NOT be doing that and is basically ripping you off!
  • The script is freeware, you are free to use it and distribute it, but always refer to this website (https://jorgequestforknowledge.wordpress.com/) as the location where you got it.
  • This script is furnished "AS IS". No warranty is expressed or implied!
  • I have NOT tested it in every scenario nor have I tested it against every Windows and/or AD version
  • Always test first in lab environment to see if it meets your needs!
  • Use this script at your own risk!
  • I do not warrant this script to be fit for any purpose, use or environment!
  • I have tried to check everything that needed to be checked, but I do not guarantee the script does not have bugs!
  • I do not guarantee the script will not damage or destroy your system(s), environment or whatever!
  • I do not accept liability in any way if you screw up, use the script wrong or in any other way where damage is caused to your environment/systems!
  • If you do not accept these terms do not use the script in any way and delete it immediately!

REMARKS (READ THIS!):

  • The script requires PowerShell v2.0 at a minimum
  • This script must be able to read the contents of the PSO container in every AD domain the script will target!. By default only Domain Admins can read this.
  • It is therefore needed to delegate those permissions to the account executing this PoSH script.
  • For more information about this see the blog post: https://jorgequestforknowledge.wordpress.com/2007/08/09/windows-server-2008-fine-grained-password-policies/
  • DSACLS "\\<Some RWDC>\CN=Password Settings Container,CN=System,<Your AD domain DN>" /G "<Some Security Principal>:GR" /I:T
    • This assign <Some Security Principal> with Allow:Read on the Password Settings Container including its descendant objects

The tool uses an XML called "AD-Pwd-Exp-Notify.xml". It is pre-filled with examples from my test/demo environment. Make sure to change as needed to accommodate your own environment and requirements!

The script has four execution modes. When NOT running the PowerShell script with the ‘-force’ parameter, it will by default run in TEST mode without sending any e-mail to users regarding password expiry ("TEST (NO MAILINGS)"), no matter what the configuration in the XML files specifies. When running the PowerShell script with the ‘-force’ parameter, it will look in the XML file to see which execution mode to run in. When "DEV" is specified it will only send 1 mail to the SMTP address of the admin user specified in the "toSMTPAddressInTestMode" configuration field. This mode allows you to develop the solution being swamped in e-mails or impacting your users. When "TEST" is specified it will only send all mails to the SMTP address of the admin user specified in the "toSMTPAddressInTestMode" configuration field. This mode allows you to see/experience what your scoped/targeted users would see/experience without actually impacting them. When "PROD" is specified it will only send all mails to the SMTP address of the individual users. This really sends the e-mails to all the scoped/targeted individual users.

<!– Execution Mode: DEV (1 Mail To Admin User) or TEST (All Mails To Admin User) or PROD (All Mails To Individual Users) –>

<executionMode>DEV</executionMode>

The PowerShell script sends e-mail, therefore it requires a FROM e-mail address

<!– The SMTP Address Used In The FROM Field –>

<mailFromSender>general.DO-NOT-REPLY@iamtec.nl</mailFromSender>

To develop the solution and test it you can specify an SMTP address that will be used to send e-mails to, without impacting the real user community. That SMTP address will also be used for notifications is the SMTP server or DC is unavailable.

<!– The SMTP Address Used When Running In DEV/TEST Mode And Also Used For Notifications –>

<toSMTPAddressInTestMode>adm.root@iamtec.nl</toSMTPAddressInTestMode>

The PowerShell script sends e-mail, therefore it requires an SMTP server. A test connection to the SMTP server is made. If it fails the script aborts!

<!– FQDN Of The Mail Server Or Mail Relay –>

<smtpServer>MAIL.IAMTEC.NET</smtpServer>

The priority of the mail send can be configured as Low, Normal or High

<!– The Priority Of The Message: Low, Normal, High –>

<mailPriority>High</mailPriority>

The script supports multi-lingual messages. You must always specify a default language and for each language you must also specify a mail subject and the full path to HTML body file that contains the text in a specific language. Both the subject and the body support variables that can be replaced by the actual values. The script contains an example for US (English) and the same example for NL (Dutch).

<!– The File With The HTML Body Text For A Specific Language And The Subject. Supported Variables: FIRST_NAME, LAST_NAME, DISPLAY_NAME, FQDN_DOMAIN, PWD_EXPIRE_IN_NUM_DAYS, PWD_EXPIRY_DATE, PWD_MIN_LENGTH, PWD_MIN_AGE, PWD_MAX_AGE, PWD_HISTORY, PWD_COMPLEX, PWD_CHANGE_RESET_URL  –>
<htmlBodyFiles>
    <htmlBodyFile language="default" mailSubject="Expiring Password In Approx. PWD_EXPIRE_IN_NUM_DAYS Days – Change Your Password As Soon As Possible!" fullPath="D:\TEMP\ADPwdExpNotifyMessageBody_US.html" />
    <htmlBodyFile language="US" mailSubject="Expiring Password In Approx. PWD_EXPIRE_IN_NUM_DAYS Days – Change Your Password As Soon As Possible!" fullPath="D:\TEMP\ADPwdExpNotifyMessageBody_US.html" />
    <htmlBodyFile language="NL" mailSubject="Verlopen Wachtwoord In Ongeveer PWD_EXPIRE_IN_NUM_DAYS Dagen – Wijzig Uw Wachtwoord Zo Snel Als Mogelijk!" fullPath="D:\TEMP\ADPwdExpNotifyMessageBody_NL.html" />
</htmlBodyFiles>

If you have a web portal (e.g. FIM SSPR or Exchange Change Password) to change and/or reset the password, then you can specify it here

<!– The URL Where The Users Can Change Or Reset Their Password –>

<pwdChangeOrResetURL>https://ssprportal.iamtec.net:447/</pwdChangeOrResetURL>

Logging to screen can be enabled (ON) or disabled (OFF).

<!– Enable/Disable Logging To Screen: ON or OFF –>

<logToScreen>ON</logToScreen>

Logging tofile can be enabled (ON) or disabled (OFF).

<!– Enable/Disable Logging To A Log File: ON or OFF –>

<logToFile>ON</logToFile>

If logging is enabled, then you must specify the full path to the log file that will be used. The script itself will take date and time into account

<!– Full Path Of The Log File (.LOG Extension!) –>

<fullPathToLogFile>D:\TEMP\ADPwdExpNotify.log</fullPathToLogFile>

To make sure the disk is not swamped with a huge number of log files, you can specify for how many days the script will keep log files. Every log file older than the specified number will be deleted

<!– Number Of Days To Keep LOG Files –>

<numDaysLOGToKeep>30</numDaysLOGToKeep>

When enabled (ON), the script will export the information of users to a CSV file for troubleshooting and analyses. When disabled (OFF) nothing is exported.

<!– Enable/Disable Export Of Notified Accounts To A CSV File: ON or OFF –>

<exportToCSV>ON</exportToCSV>

If exporting is enabled, then you must specify the full path to the CSV file that will be used. The script itself will take date and time into account

<!– Full Path Of The CSV File (.CSV Extension!) –>

<fullPathToCSVFile>D:\TEMP\ADPwdExpNotify.csv</fullPathToCSVFile>

To make sure the disk is not swamped with a huge number of CSV files, you can specify for how many days the script will keep CSV files. Every CSV file older than the specified number will be deleted

<!– Number Of Days To Keep CSV Files –>

<numDaysCSVToKeep>30</numDaysCSVToKeep>

In the XML config file you can specify the date/time format to be used on screen, in the log, in the CSV and in the E-mail message

<!– Date And Time Format To Use On Screen, In Logs And In E-mail Message –>
<formatDateTime>yyyy-MM-dd HH:mm:ss</formatDateTime>

In this section you specify every AD domain in the AD forest, including trusting AD domains, for which its scoped/targeted users must be notified. For every AD domain, you then need to specify if an RWDC needs to be discovered (specifiy: DISCOVER) or you can specifically mention an RWDC that must be targeted. A test connection to the RWDC is made. If it fails the AD domain for that RWDC is fully skipped! Then for every AD domain, specify one or more search bases to scope/target users in the LDAP query. For every search base configure the language for the scoped/targeted users. Make sure that if you specify specific languages, that you also have configured the HTML Body File for that language

<!– Targeted Domains, Specify DISCOVER To Discover A DC Or Use Specific DC And Search Bases Per Domain –>
<!– WARNING: Make Sure The Search Bases DO NOT Overlap Each Other!!! –>
<domains>
    <domain FQDN="IAMTEC.NET" DC="DISCOVER">
        <searchBase nr="1" language="default">OU=Users,OU=EMPLOYEES,OU=Org-Users,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="2" language="US">OU=Users,OU=CONTRACTORS,OU=Org-Users,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="3" language="US">OU=Users,OU=CONTRACTORZZZ,OU=Org-Users,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="4" language="NL">OU=Users,OU=HISTORY1,OU=Org-Users,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="5" language="NL">OU=Users,OU=HISTORY2,OU=Org-Users,DC=IAMTEC,DC=NET</searchBase>
    </domain>
    <domain FQDN="CHILD.IAMTEC.NET" DC="C1FSRWDC1.CHILD.IAMTEC.NET">
        <searchBase nr="1" language="default">OU=Users,OU=EMPLOYEES,OU=Org-Users,DC=CHILD,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="2" language="US">OU=Users,OU=CONTRACTORS,OU=Org-Users,DC=CHILD,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="3" language="US">OU=Users,OU=CONTRACTORZZZ,OU=Org-Users,DC=CHILD,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="4" language="NL">OU=Users,OU=HISTORY1,OU=Org-Users,DC=CHILD,DC=IAMTEC,DC=NET</searchBase>
        <searchBase nr="5" language="NL">OU=Users,OU=HISTORY2,OU=Org-Users,DC=CHILD,DC=IAMTEC,DC=NET</searchBase>
    </domain>
</domains>

In this section you can specify one or multiple periods of notifications. Make sure that none of the periods overlaps any other period!. In the example shown, the scoped/targeted user will receive 4 notifications assuming the script executes once every day. Also make sure the periods configured are in balance with the maximum password age!

<!– Number Of Days Before The Password Expires To Send Notifications –>
<!– WARNING: Make Sure The Periods DO NOT Overlap Each Other!!! –>
<daysBeforeWarn>
    <period nr="1" Max="10" Min="9" />
    <period nr="2" Max="5" Min="4" />
    <period nr="3" Max="2" Min="0" />
</daysBeforeWarn>

image_thumb26_thumb

Figure 1a: XML Configuration File

image_thumb29_thumb

Figure 1b: XML Configuration File

Executing The Script

Executing the script to run in "DEV", "TEST" or "PROD" mode (whatever is configured in the XML configuration file) while using the default location of the XML configuration file (same folder as script)

.\AD-Pwd-Exp-Notify_v017.ps1 -force

Executing the script to run in "TEST (NO MAILINGS)" mode while using the default location of the XML configuration file (same folder as script)

.\AD-Pwd-Exp-Notify_v017.ps1

Executing the script to run in "TEST (NO MAILINGS)" mode while using a custom location of the XML configuration file

.\AD-Pwd-Exp-Notify_v017.ps1 -xmlconfigfilepath D:\TEMP\AD-Pwd-Exp-Notify.xml

Executing the script to run in "DEV", "TEST" or "PROD" mode (whatever is configured in the XML configuration file) while using custom location of the XML configuration file

.\AD-Pwd-Exp-Notify_v017.ps1 -xmlconfigfilepath D:\TEMP\AD-Pwd-Exp-Notify.xml -force

Example Output Of The Script (On Screen)

image_thumb32_thumb

Figure 2a: Output To Screen

image_thumb35_thumb

Figure 2b: Output To Screen

image_thumb38_thumb

Figure 2c: Output To Screen

image_thumb41_thumb

Figure 2d: Output To Screen

image_thumb44_thumb

Figure 2e: Output To Screen

Example Output Of The Script (Log File)

See zip file

Example Output Of The Script (CSV file)

See zip file

E-mail Message For US English Language

image_thumb48_thumb

Figure 3a: E-Mail Notification In English

image_thumb51_thumb

Figure 3b: E-Mail Notification In English

E-mail Message For Dutch Language

image_thumb55_thumb

Figure 4a: E-Mail Notification In Dutch

image_thumb58_thumb

Figure 4b: E-Mail Notification In Dutch

And Finally….The PowerShell Script Itself

### Abstract: This PoSH Script Notifies Mailbox Enabled Users For Which The Password Will Expires Within A Specific Number Of Days ### Written by: Jorge de Almeida Pinto [MVP-DS] ### BLOG: https://jorgequestforknowledge.wordpress.com/ ### ### 2015-03-21: Initial version of the script in PowerShell (v0.13) ### 2015-03-26: Bug fixes regarding some attributes not having values (v0.14) ### 2015-03-27: Supporting date/time format in XML and incorrect variable being used to get the correct password policy settings (v0.15) ### 2015-04-29: Bug fixes regarding the default domain GPO getting no name when no PSOs are used or inheriting the name of the last processed PSO, ### the displayName of the development user not being processed correctly, and more enhanced error detection when discovering a DC for ### non-existing AD domain, and better explanation and information about the parameters and the script itself (v0.16) ### 2015-09-22: Bug fixes regarding specifying the script version and date after the parameter section (v0.17) ### <# .SYNOPSIS This PoSH Script Notifies Mailbox Enabled Users For Which The Password Will Expires Within A Specific Number Of Days .DESCRIPTION This PoSH script notifies mailbox enabled users for which the password will expires within a specific number of days. The configuration of the script is done through an XML file. The tool uses an XML called "AD-Pwd-Exp-Notify.xml". It is pre-filled with examples from my test/demo environment. Make sure to change as needed to accommodate your own environment and requirements! For detailed information about all configurable options see the sample XML file or browse to: https://jorgequestforknowledge.wordpress.com/2015/03/24/notifying-users-by-e-mail-their-password-is-going-to-expire-update-1/ .PARAMETER force Runs the script in whatever mode is configured in the XML file (e.g. "DEV", "TEST" or "PROD" mode) .PARAMETER xmlconfigfilepath Allows to use a custom location and custom XML file instead of the default XML file in the default location (same folder as the script) .EXAMPLE Executing the script to run in "TEST (NO MAILINGS)" mode while using the default location of the XML configuration file (same folder as script) AD-Pwd-Exp-Notify_vXXX.ps1 .EXAMPLE Executing the script to run in "DEV", "TEST" or "PROD" mode (whatever is configured in the XML configuration file) while using the default location of the XML configuration file (same folder as script) AD-Pwd-Exp-Notify_vXXX.ps1 -force .EXAMPLE Executing the script to run in "TEST (NO MAILINGS)" mode while using a custom location of the XML configuration file AD-Pwd-Exp-Notify_vXXX.ps1 -xmlconfigfilepath D:\TEMP\AD-Pwd-Exp-Notify.xml .EXAMPLE Executing the script to run in "DEV", "TEST" or "PROD" mode (whatever is configured in the XML configuration file) while using custom location of the XML configuration file AD-Pwd-Exp-Notify_vXXX.ps1 -xmlconfigfilepath D:\TEMP\AD-Pwd-Exp-Notify.xml -force .NOTES -->> DISCLAIMER <<-- * I wrote this script, therefore I own it. Anyone asking money for it, should NOT be doing that and is basically ripping you off! * The script is freeware, you are free to use it and distribute it, but always refer to this website (https://jorgequestforknowledge.wordpress.com/) as the location where you got it. * This script is furnished "AS IS". No warranty is expressed or implied! * I have NOT tested it in every scenario nor have I tested it against every Windows and/or AD version * Always test first in lab environment to see if it meets your needs! * Use this script at your own risk! * I do not warrant this script to be fit for any purpose, use or environment! * I have tried to check everything that needed to be checked, but I do not guarantee the script does not have bugs! * I do not guarantee the script will not damage or destroy your system(s), environment or whatever! * I do not accept liability in any way if you screw up, use the script wrong or in any other way where damage is caused to your environment/systems! * If you do not accept these terms do not use the script in any way and delete it immediately! -->> REMARKS <<-- * The script requires PowerShell v2.0 at a minimum * This script must be able to read the contents of the PSO container in every AD domain the script will target!. By default only Domain Admins can read this. * It is therefore needed to delegate those permissions to the account executing this PoSH script. * For more information about this see the blog post: https://jorgequestforknowledge.wordpress.com/2007/08/09/windows-server-2008-fine-grained-password-policies/ * DSACLS "\\<Some RWDC>\CN=Password Settings Container,CN=System,<Your AD domain DN>" /G "<Some Security Principal>:GR" /I:T >> This assigns <Some Security Principal> with Allow:Read on the Password Settings Container including its descendant objects #> Param( [Parameter(Mandatory=$false)] [string]$xmlconfigfilepath, [Parameter(Mandatory=$false)] [switch]$force ) $scriptVersion = "v0.17" $scriptDate = "2015-09-22" ################################################################################################## #################################### SCRIPT FUNCTIONS START ###################################### ################################################################################################## ################################################################################################## ### FUNCTION: Logging Data To The Log File Function Logging($dataToLog) { $datetimeLogLine = "[" + $(Get-Date -format $formatDateTime) + "] : " If ($logToFile.ToUpper() -eq "ON") { Out-File -filepath "$fullPathToLogFile" -append -inputObject "$datetimeLogLine$dataToLog" } If ($logToScreen.ToUpper() -eq "ON") { Write-Output($datetimeLogLine + $dataToLog) } } ################################################################################################## ### FUNCTION: Cleaning Up Old Log Files Function CleanUpLOGFiles($numDaysLOGToKeep) { $regExPatternLogFile = '^.*_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}.log$' $oldLogFiles = Get-ChildItem -Path $folderLogFile\*.log | ?{$_.Name -match $regExPatternLogFile} $oldLogFilesToDelete = $oldLogFiles | ?{$_.lastwritetime -lt (Get-Date $execStartDateTime).addDays(-$numDaysLOGToKeep) -and -not $_.psiscontainer} $oldLogFilesToDelete | %{Remove-Item $_.FullName -force} Return ($oldLogFilesToDelete | Measure-Object).Count } ################################################################################################## ### FUNCTION: Cleaning Up Old Csv Files Function CleanUpCSVFiles($numDaysCSVToKeep) { $regExPatternCsvFile = '^.*_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}.csv$' $oldCsvFiles = Get-ChildItem -Path $folderCsvFile\*.csv | ?{$_.Name -match $regExPatternCsvFile} $oldCsvFilesToDelete = $oldCsvFiles | ?{$_.lastwritetime -lt (Get-Date $execStartDateTime).addDays(-$numDaysCSVToKeep) -and -not $_.psiscontainer} $oldCsvFilesToDelete | %{Remove-Item $_.FullName -force} Return ($oldCsvFilesToDelete | Measure-Object).Count } ################################################################################################## ### FUNCTION: Discover An RWDC From An AD Domain Function DiscoverRWDC($fqdnADdomain) { $contextADDomain = $NULL $contextADDomain = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext("Domain",$fqdnADdomain) $dnsHostNameRWDC = $NULL $ErrorActionPreference = "SilentlyContinue" $dnsHostNameRWDC = ([System.DirectoryServices.ActiveDirectory.DomainController]::findone($contextADDomain)).Name $ErrorActionPreference = "Continue" If ($dnsHostNameRWDC -eq $null) { Return "DOMAIN_DOES_NOT_EXIST_OR_CANNOT_FIND_DC" } Else { Return $dnsHostNameRWDC } } ################################################################################################## ### FUNCTION: Check If An OU/Container Exists Function CheckDNExistence($dnsHostNameRWDC,$DN) { Try { If([ADSI]::Exists("LDAP://$dnsHostNameRWDC/$DN")) { Return "SUCCESS" } Else { Return "ERROR" } ` } Catch { Return "ERROR" } } ################################################################################################## ### FUNCTION: Test Connection To A Server Function TestConnectionToServer($dnsHostName,$port) { $tcpPortSocket = New-Object System.Net.Sockets.TcpClient $timeOut = "500" $portConnect = $tcpPortSocket.BeginConnect($dnsHostName,$port,$null,$null) $tcpPortWait = $portConnect.AsyncWaitHandle.WaitOne($timeOut,$false) If(!$tcpPortWait) { $tcpPortSocket.Close() Return "ERROR" } Else { $ErrorActionPreference = "SilentlyContinue" $tcpPortSocket.EndConnect($portConnect) | Out-Null If (!$?) { Return "ERROR" } Else { Return "SUCCESS" } $tcpPortSocket.Close() $ErrorActionPreference = "Continue" } } ################################################################################################## ### FUNCTION: Decode Functional Level Function DecodeFunctionalLevel($dfl) { Switch ($dfl) { 0 {"Windows 2000"} 1 {"Windows 2003 Interim"} 2 {"Windows 2003"} 3 {"Windows 2008"} 4 {"Windows 2008 R2"} 5 {"Windows 2012"} 6 {"Windows 2012 R2"} #7 {"TBD"} #8 {"TBD"} #9 {"TBD"} #10 {"TBD"} Default {"If You See This, Something Is Wrong!"} } } ################################################################################################## ##################################### SCRIPT FUNCTIONS END ####################################### ################################################################################################## ### Clear The Screen Clear-Host ### Configure The Appropriate Screen And Buffer Size To Make Sure Everything Fits Nicely $uiConfig = (Get-Host).UI.RawUI $uiConfig.WindowTitle = "+++ AD PASSWORD EXPIRY NOTIFICATION +++" $uiConfig.ForegroundColor = "Yellow" $uiConfigBufferSize = $uiConfig.BufferSize $uiConfigBufferSize.Width = 500 $uiConfigBufferSize.Height = 9999 $uiConfigScreenSizeMax = $uiConfig.MaxPhysicalWindowSize $uiConfigScreenSizeMaxWidth = $uiConfigScreenSizeMax.Width $uiConfigScreenSizeMaxHeight = $uiConfigScreenSizeMax.Height $uiConfigScreenSize = $uiConfig.WindowSize If ($uiConfigScreenSizeMaxWidth -lt 160) { $uiConfigScreenSize.Width = $uiConfigScreenSizeMaxWidth } Else { $uiConfigScreenSize.Width = 160 } If ($uiConfigScreenSizeMaxHeight -lt 75) { $uiConfigScreenSize.Height = $uiConfigScreenSizeMaxHeight - 5 } Else { $uiConfigScreenSize.Height = 75 } $uiConfig.BufferSize = $uiConfigBufferSize $uiConfig.WindowSize = $uiConfigScreenSize ### Script Configuration File If ($xmlconfigfilepath -eq $null -or $xmlconfigfilepath -eq "") { $currentScriptFolderPath = Split-Path $MyInvocation.MyCommand.Definition [string]$scriptXMLConfigFilePath = Join-Path $currentScriptFolderPath "AD-Pwd-Exp-Notify.xml" } Else { [string]$scriptXMLConfigFilePath = $xmlconfigfilepath } ### Start Time Of Script In UTC $execStartDateTime = (Get-Date -format $formatDateTime) $execStartDateTimeForFileSystem = (Get-Date $execStartDateTime -format "yyyy-MM-dd_HH-mm-ss") ### Read The Config File If (!(Test-Path $scriptXMLConfigFilePath)) { Write-Host "The XML Config File '$scriptXMLConfigFilePath' CANNOT Be Found!..." -ForeGroundColor Red Write-Host "Aborting Script..." -ForeGroundColor Red EXIT } Else { [XML]$global:configADPwdExpNotify = Get-Content $scriptXMLConfigFilePath #Write-Host "The XML Config File '$scriptXMLConfigFilePath' Has Been Found!..." -ForeGroundColor Green #Write-Host "Continuing Script..." -ForeGroundColor Green #Write-Host "" } ### Read The Properties From The XML Config File $executionMode = $configADPwdExpNotify.ADPwdExpNotifyConfig.executionMode $mailFromSender = $configADPwdExpNotify.ADPwdExpNotifyConfig.mailFromSender $toSMTPAddressInTestMode = $configADPwdExpNotify.ADPwdExpNotifyConfig.toSMTPAddressInTestMode $smtpServer = $configADPwdExpNotify.ADPwdExpNotifyConfig.smtpServer $mailPriority = $configADPwdExpNotify.ADPwdExpNotifyConfig.mailPriority $mailSubject = $configADPwdExpNotify.ADPwdExpNotifyConfig.mailSubject $htmlBodyFiles = $configADPwdExpNotify.ADPwdExpNotifyConfig.htmlBodyFiles.htmlBodyFile $pwdChangeOrResetURL = $configADPwdExpNotify.ADPwdExpNotifyConfig.pwdChangeOrResetURL $logToScreen = $configADPwdExpNotify.ADPwdExpNotifyConfig.logToScreen $logToFile = $configADPwdExpNotify.ADPwdExpNotifyConfig.logToFile $fullPathToLogFile = $configADPwdExpNotify.ADPwdExpNotifyConfig.fullPathToLogFile -replace ".log","_$execStartDateTimeForFileSystem.log" $folderLogFile = Split-Path $fullPathToLogFile $numDaysLOGToKeep = $configADPwdExpNotify.ADPwdExpNotifyConfig.numDaysLOGToKeep $exportToCSV = $configADPwdExpNotify.ADPwdExpNotifyConfig.exportToCSV $fullPathToCSVFile = $configADPwdExpNotify.ADPwdExpNotifyConfig.fullPathToCSVFile -replace ".csv","_$execStartDateTimeForFileSystem.csv" $folderCsvFile = Split-Path $fullPathToCSVFile $numDaysCSVToKeep = $configADPwdExpNotify.ADPwdExpNotifyConfig.numDaysCSVToKeep $formatDateTime = $configADPwdExpNotify.ADPwdExpNotifyConfig.formatDateTime $domains = $configADPwdExpNotify.ADPwdExpNotifyConfig.domains.domain $daysBeforeWarn = $configADPwdExpNotify.ADPwdExpNotifyConfig.daysBeforeWarn.Period Logging "#######################################################################################" Logging " *********************************************************" Logging " * *" Logging " * --> ACTIVE DIRECTORY PASSWORD EXPIRY NOTIFICATION <-- *" Logging " * *" Logging " * ($scriptVersion) ($scriptDate) *" Logging " * *" Logging " * Written By: Jorge de Almeida Pinto [MVP-DS] *" Logging " * *" Logging " * BLOG: 'Jorge's Quest For Knowledge' *" Logging " * (https://jorgequestforknowledge.wordpress.com/) *" Logging " * *" Logging " *********************************************************" Logging "" Logging "Starting Date And Time...........: $execStartDateTime" Logging "" Logging "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" ### Logging All Configured Settings Logging "" Logging "XML Config File Path.............: $scriptXMLConfigFilePath" Logging "" If (!$force) { $executionMode =