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

Monday, March 17, 2014

Enumerating and Scanning a Large Number of Files recursively with Powershell for a pattern

Useing Powershell 3.0,

I found a large discrepency in terms of performance using the Powershell Commandlets, how you call them, and their execution vs .NET Classes in System. IO namespace.

Scenario:
Scanning a large number of files recursively (approximately 32,000 files across 238 folders) to match filename to a pattern.  (of which 5 files match the pattern for this example)

Code sample using powershell Get-childitem commandlet:

 
$userID = "TestUser"
allContents = (gci \\Server\share -Recurse | where {($_ -like "*$($userID).*")
This command was taking 3-5mins to process, totally unacceptable processing time on something that must be rerun continuously..

Upon examing this code, I am trying to first create a powershell object with 32K+ "FileInfo" Objects items, then filtering the results with a Where-object command on the pipeline.

Re-writing the Powershell commandlet like this increased performance dramatically..
$allContents = gci "\\nw-svc-bu01\vol1\WSINFO" -Filter "*SINESC.*" -Recurse
allContents = gci "\\nw-svc-bu01\vol1\WSINFO" -Filter "*SINESC.*" -Recurse
$allContents = gci "\\server\share" -Filter "*$($userID).*" -Recurse



This command returns results in 10 seconds.  This command 'skims' the file directory structure, and only returns files that match the filter.  The more files that match the filter, the slower the performance will be because it still returns an array of "FileInfo" objects.  So the creating of the "FileInfo" objects are slow point, not the scanning of the folders recursively.

Code sample using .NET Class:

 
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.IO")

$userID = "TestUser"
$allConents = ((([IO.Directory]::EnumerateFiles(\\server\share,"*$($userID).*",[System.IO.SearchOption]::AllDirectories)) | out-string).trim()).Split()


This command returns results in 7 seconds.  The EnumerateFiles Method returns a System.Collections.Generic.IEnumerable Object.  Which only contains the full file path of the files into a collection.

So if you need performance it appears that .NET Classes are slightly superior, if you only need the file path returned for your query.  It also points out that how you call and execute powershell commandlets can impact performance dramatically as well.

Powershell Function to set account enabled for non-AD LDAP (novell in my case)

Function SetEnableLDAPUserAccount {

<#

.DESCRIPTION

Sets an LDAP User object enabled or disabled

.EXAMPLES

SetLDAPUserPassword -LDAPServer "LDAPSERVER" -LDAPPort 636 -SSL $true -targetUserDN "cn=SMITHJO1.ou=test,o=TESTTREE" -AccountDisabled $False -AuthUserName "cn=admin,o=TESTTREE" -SecPassWord $NovellCred.Password


#>

param([string]$LDAPServer,[int]$LDAPPort,[boolean]$SSL,[string]$targetUserDN,[boolean]$AccountDisabled,[string]$AuthUserName,[securestring]$SecPassWord )

#Load the assemblies

[System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.Protocols") | Out-Null

[System.Reflection.Assembly]::LoadWithPartialName("System.Net") | Out-Null

$Error.Clear()

Try {

#Connects to LDAP Server using specified port

$c = New-Object System.DirectoryServices.Protocols.LdapConnection "$($LDAPServer):$($LDAPPort)" -ea Stop

#Set session options

$c.SessionOptions.SecureSocketLayer = $SSL;

$c.SessionOptions.VerifyServerCertificate = { return $true;} #needed for self-signed certificates

# Pick Authentication type:

# Anonymous, Basic, Digest, DPA (Distributed Password Authentication),

# External, Kerberos, Msn, Negotiate, Ntlm, Sicily

$c.AuthType = [System.DirectoryServices.Protocols.AuthType]::Basic



#Creates a credential object to pass to bind to LDAP Connection Object

$NovellCredentials = new-object "System.Net.NetworkCredential" -ArgumentList $AuthUserName,$SecPassWord

# 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);

}

Catch

{

switch -Wildcard ($Error)

{

"*The supplied credential is invalid*" { "The Supplied LDAP Authentication Credentials for User: $($AuthUserName) were invalid." }

"*The LDAP server is unavailable*" {"Error Connecting to LDAP Server! Check that LDAP Server value of: $($LDAPServer) is correct, and available and responding on port: $($LDAPPort)"}

default {"An Unknown Error occured attempting to connect to LDAP Server $($LDAPServer) to $(if ($AccountDisabled -eq $True){"Disable"} Else {"Enable"}) User: $($targetUserDN)'s eDirectory account."}

}

Exit 1

}



#Creating an LDAP request Object

$r = (new-object "System.DirectoryServices.Protocols.ModifyRequest")

$r.DistinguishedName = $targetUserDN;

$m = New-Object "System.DirectoryServices.Protocols.DirectoryAttributeModification"

$m.Name = "loginDisabled"; #Attribute where the User's Password is stored, is a Write only attribute

$m.Operation = [System.DirectoryServices.Protocols.DirectoryAttributeOperation]::Replace

#add value(s) of the attribute

$m.Add($AccountDisabled.ToString().toUpper()) | Out-Null

$r.Modifications.Add($m) | Out-Null

$Error.Clear()

Try

{ #Actually Try to process the request through the server

$re = $c.SendRequest($r);

}

Catch

{

switch -Wildcard ($Error)

{

"*The user has insufficient access rights*" {"The LDAP User $($AuthUserName) doesn't appear to have rights to $(if ($AccountDisabled -eq $True){"Disable"} Else {"Enable"}) the user account: $($targetUserDN)."}

default {"An Unknown Error occured while attempting to $(if ($AccountDisabled -eq $True){"Disable"} Else {"Enable"}) the eDirectory LDAP User: $($targetUserDN)'s account."}

}

Exit 1

}

if ($re.ResultCode -ne [System.directoryServices.Protocols.ResultCode]::Success) {

$LDAPErr = "$(if ($AccountDisabled -eq $True){"Disabling"} Else {"Enabling"}) LDAP User: $($targetUserDN) account failed!

ResultCode: $($re.ResultCode)

Message: $($re.ErrorMessage)"

Return $LDAPErr

}

Else {

Return "$($re.ResultCode)! LDAP User: $($targetUserDN)'s account was $(if ($AccountDisabled -eq $True){"Disabled"} Else {"Enabled"})."

}

 

}
#End Function
#----------------------------------------------------------------------------------

$NovellCred = Get-Credential -Message "Enter your Novell Username and Password. Example username: smithj" # Getting the Novell Credential Information

$EDIRLDAPServer = "LDAPSERVER"
$EDIRBaseDN = "O=TEST"
$userID = $NovellCred.UserName.ToUpper()
$TrgtUserID = "smitha"
$TrgtUserPass = P@ssw0rd

$PRODLDAPObj = GetLDAPObject -LDAPServer $EDIRLDAPServer -LDAPPort 389 -SSL $false -baseDN $EDIRBaseDN -Filter "(uid=$($userID))"

$userDN = ""
$userDN = (($PRODLDAPObj | select "DistinguishedName" | Out-String).Split() | Select-String -Pattern '^(\w+[=]{1}\w+)([,{1}]\w+[=]{1}\w+)*$').ToString().ToUpper()

SetEnableLDAPUserAccount -LDAPServer $EDIRLDAPServer -LDAPPort 389 -SSL $false -targetUserDN $trgtUserDN -AccountDisabled $False -AuthUserName $userDN -SecPassWord $NovellCred.Password