Monday, September 26, 2016

Automated Password Expiration Notification Powershell LDAP (eDirectory)

NETIQ provided some tools to provided automated notifications for password expiration for entire containers /OUs.  However, how do you notify just subset of users?  I wrote this Powershell script to do just that.  Should work with most LDAP implementations, however check if the attribute "passwordexpirationtime" exists, or is called something else and adjust the script accordingly if so.


<#---------------------------------------------------------------------------------
  Password Change Notification
  VERSION 1.0
  DATE: 9/26/2016
  BY: Corey A Sines

  .DESCRIPTION
   Automates sending emails for accounts in specified LDAP Tree, port, and Search Base where account expires is less that 14 days

   .EXAMPLES
   powershell.exe .\Password_Change_Notify.ps1 -LDAPServer lDAP.testdomain.com -LDAPPort 389 -LDAPBase O=RootDN


   Version History:
   1.0     Initial release

---------------------------------------------------------------------------------#>

# Passable Parameters for Script Function
param([string]$LDAPServer="",[int]$LDAPPort=389,[string]$LDAPBase="")

$testing = 0 # 1=true, 0=false  - Sets Script to testing mode to use $testEmailTo instead of true email


Function Usage {
# Displays Usage information if the script is called without Parameters
 "Usage:

  powershell.exe .\Password_Change_Notify.ps1 -LDAPServer ldapserver.testdomain.com -LDAPPort 389 -LDAPBase O=baseDN"

} #End Function Usage
Function GetLDAPObject {
<#
    .DESCRIPTION
    Returns an LDAP object (which contains a collection of attributes), or objects depending on the the search Filter.
    Query uses Anonymous Authentication, Function will need to altered for different Credentials, information contained with Commented out Code.

    .EXAMPLES
    GetLDAPObject -LDAPServer "ldap.testdomain.com" -LDAPPort 389 -SSL $false -baseDN "o=root" -Filter "(uid=john.smith)"
#>
param([string]$LDAPServer,[int]$LDAPPort,[boolean]$SSL,[string]$baseDN, [string]$Filter)

#Load the assemblies
[System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.Protocols") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName("System.Net") | Out-Null


#Connects to Server using SSL on a specificed port
$c = New-Object System.DirectoryServices.Protocols.LdapConnection "$($LDAPServer):$($LDAPPort)"
       
#Set session options
$c.SessionOptions.SecureSocketLayer = $SSL;
         
# Pick Authentication type:
# Anonymous, Basic, Digest, DPA (Distributed Password Authentication),
# External, Kerberos, Msn, Negotiate, Ntlm, Sicily
$c.AuthType = [System.DirectoryServices.Protocols.AuthType]::Anonymous
         
# Gets username and password.  Required for Authentication Types requiring Credentials
#$user = Read-Host -Prompt "Username"
#$pass = Read-Host -AsSecureString "Password"

#Creates a credential object to pass to bind to LDAP Connection Object
#$NovellCredentials = new-object "System.Net.NetworkCredential" -ArgumentList $user,$pass

# Bind with the network credentials. Depending on the type of server,
# the username will take different forms. Authentication type is controlled
# above with the AuthType
#$c.Bind($NovellCredentials);

$scope = [System.DirectoryServices.Protocols.SearchScope]::Subtree
$attrlist = ,"*" #Returns all Attributes
$TimeSpan = New-TimeSpan $(get-Date) $(get-date -Minute 30)

$r = New-Object System.DirectoryServices.Protocols.SearchRequest -ArgumentList `
                $baseDN,$Filter,$scope,$attrlist,$TimeSpan

#$re is a System.DirectoryServices.Protocols.SearchResponse
$re = $c.SendRequest($r);

#How many results do we have?
"A Total of $($re.Entries.Count) Entry(s) found in LDAP Search"

If ($re.Entries.Count -eq 1) #Returns the Only Single Entry
{
    Return $Re.Entries[0]
}
elseIf ($re.Entries.Count -eq 0) #Returns Null, No match found on Filter, or problems with LDAP
{
    Return $null
}
else # Returns the Entire Collection of Entries
{
    #foreach ($i in $re.Entries) { #Do something with each entry here, such as read attributes }
    Return $re.Entries                            
}


} #End Function GetLDAPObject
Function SendEmail {
<#
    .DESCRIPTION
    Sends an SMTP Message with the body as displayed in this Function.
    Parameter "-CountDown" is the number of days till the user's password expires
    Parameter "-PassExpired" (Password Expired) represents someone's password that has reach day 0 (or beyond) and must be changed before logging in.

    .EXAMPLES
    SendEmail -ToEmailAdd "john.smith@testdomain.com" -Fullname "John Smith" -CountDown 3 -PassExpired 0
#>
param([string]$Fullname,[string]$ToEmailAdd,[string]$CountDown,[bool]$PassExpired)

$FROM = "PasswordExpirationNotification@testdomain.com" # Email address that will be listed as the "From"

$SUBJECT = " "
$BODY =" "

IF ($PassExpired -eq $false)
{
"Sending Password Expiration warning (password will expire in $($CountDown) days)"
    $SUBJECT = "UserID Password wille Expire in $($CountDown) days"
    $BODY = "<Enter Body Message Here>"
 
}
ELSEIF ($PassExpired -eq $true)
{
"Sending Password Expiration notice (password has expired)"
    $SUBJECT = "UserID Password Expiration Notice"

    $BODY = "<Enter Body Message Here>"
}

"Trying to Sending Password Expiration Notification to $($ToEmailAdd)"

 Try
 {
    Send-MailMessage -To $ToEmailAdd -From $FROM -Subject $SUBJECT -BodyAsHtml $BODY -SmtpServer "smtp.testdomain.com"
#   "<SENDING EMAIL>"
 }
 Catch
   {"Problem Sending Email to $($ToEmailAdd)..."} # don't display any errors..


} #End Function SendEmail

$scriptpath = $MyInvocation.MyCommand.Path
$Scriptdir = Split-Path $scriptpath
Set-Location $Scriptdir
mkdir "$($Scriptdir)\PWDNTYLOGS" -Force | Out-Null #Creating a Temp folder for ZIP files

Try {Start-Transcript --path "$($Scriptdir)\PWDNTYLOGS\$($TransScriptFile)" -ErrorAction SilentlyContinue} #Logging Transcript to a file
catch {}

$testEmailTo = "john.smith@testdomain.com" # testing Email TO: addresses, must be seperated by "",


If ($LDAPServer -ne "") #checking if script was passed with Parameters or not
{
    $EDIRLDAPServer = $LDAPServer # LDAP Server
    $EDIRBaseDN = $LDAPBase # LDAP Base DN

    $CharsArray= "y","z" # Character Array to parse LDAP,  too big of a query will cause timeouts

    ForEach ($Char in $CharsArray) # Iterativatly parsing the CharsArray
    {
    $LDAPFilter = "(uid=$($char)*)(|(passwordexpirationtime=*)(mail=*)(sn=*)(givenName=*))" #LDAP filter being used,  Pulling 4 attributes per each found UID (user) : passwordexpirationtime,mail,sn,givenname
    $LDAPObj = "" # placeholder

  $error.clear() #clearing error obj just in case
  Try {
  $LDAPObj = GetLDAPObject -LDAPServer $EDIRLDAPServer -LDAPPort $LDAPPort -SSL $false -baseDN $EDIRBaseDN -Filter "$($LDAPFilter)"} # Trying to return LDAP object (or array of objects) from LDAP Query Function "GetLDAPObject"
  Catch {
  "ERROR retrieving LDAP object - ERROR:" + "`n" + ($Error[-1] | Out-String)  } #Problems connecting to LDAP or performing the search


        If ($LDAPObj.Count -ne 0) # making sure the LDAP Object actually contains at least one Item
        {
            "
            Total Users accounts found from Character Array Element:""$($Char)"" is $($LDAPObj.Count)
           
         
            "

            ForEach ($Obj in $LDAPObj) # Stepping through each found item in the LDAP Object Array
            {
                If (($Obj.Attributes.mail) -and ($Obj.Attributes.passwordexpirationtime)) #making sure the LDAP Object Item actually contains the required attributes, or skips over them
                {
                    # getting the value for attribute "passwordexpirationtime", see: https://ldapwiki.willeke.com/wiki/PasswordExpirationTime. I think this might be an eDir only attribute, need to confirm for other LDAPs
                    $passExp = "$($Obj.Attributes.passwordexpirationtime.GetValues([string]))"
                    $passExpDate = [datetime]"$($passExp.substring(4,2))/$($passExp.substring(6,2))/$($passExp.substring(0,4))" #the LDAP Attribute returns a value that must be parsed to get to the desired mm/dd/yyyy format.


                    $tspan = New-TimeSpan $(Get-Date) $($passExpDate) #creating a timespan object to compare the expiration date to today's date

     
                    IF (($tspan.Days -le 14) -and ($tspan.Days -ge 1)) # if the expiration is less that 14 days,  but more than 1 day do this..
                    {
                        $Fullname = "$($Obj.Attributes.givenname.GetValues([string])) $($Obj.Attributes.sn.GetValues([string]))" # getting the fullname of the user by combining givenname + sn, most reliable method
                        "Fullname is: $($Fullname)"
             
                        $UserEmail= $($Obj.Attributes.mail.GetValues([string])) # getting user's email address  -NOTE: not validating it is formatted correctly or valid
                        "Email is: $($UserEmail)"
     
                        "Pass Exp Date: $($passExpDate)"

                        "Password will expire in $($tspan.Days) days"

                        If ($testing -eq $true) #changing destination email for testing to $testEmailTo, so as not bombard users with emails until finished and production ready
                        {
                        "Test Mode enabled, sending to $($testEmailTo) instead of $($UserEmail)"
                        $UserEmail = $testEmailTo
                        }

                        SendEmail -ToEmailAdd $UserEmail -Fullname $Fullname -CountDown $tspan.Days -PassExpired 0 # Calling SendEmail Function with Specified Parameters

                        " "
                    }
                    ELSEIF ($tspan.Days -le 0) # Password has reached the expiration date and has not been changed, do the following..
                    {
                        $Fullname = "$($Obj.Attributes.givenname.GetValues([string])) $($Obj.Attributes.sn.GetValues([string]))" # getting the fullname of the user by combining givenname + sn, most reliable method
                        "Fullname is: $($Fullname)"

                        $UserEmail= $($Obj.Attributes.mail.GetValues([string])) # getting user's email address  -NOTE: not validating it is formatted correctly or valid
                        "Email is: $($UserEmail)"
         
                        "Pass Exp Date: $($passExpDate)"

                        "Password is expired by $($tspan.Days) days"
                     
                        If ($testing -eq $true)  #changing destination email for testing to $testEmailTo, so as not bombard users with emails until finished and production ready
                        {
                        "Test Mode enabled, sending to $($testEmailTo) instead of $($UserEmail)"
                        $UserEmail = $testEmailTo
                        }

                        SendEmail -ToEmailAdd $UserEmail -Fullname $Fullname -CountDown $tspan.Days -PassExpired 1 # Calling SendEmail Function with Specified Parameters
         
                        " "
                    }
               }
            }
        }
        ELSE
        {"No Users accounts found for Character Array Element:$($Char) in LDAP, Count= $($LDAPObj.Count)"} # nothing found in the LDAP Search Filter
    }
}
ELSE
{"Script Exiting, no LDAP Server specified!" ; Usage ; Exit} # Invalid LDAP Server, Port, or SearchBase

Try {Stop-Transcript -ErrorAction SilentlyContinue} # Stoping transcript service
catch {}

#End Script