PowerShell script to disable inactive accounts in Active Directory

In order to improve network security and at the same conform with regulatory requirements, companies have to make sure that they disable stalled accounts in their domains in a timely manner.

The two scripts described in this post show you how you can do this effectively with PowerShell. Both scripts take two parameters:

·         $Subtree: the DN of the container under which this script looks for inactive accounts

·         $NbDays: the maximum number of days of inactivity allowed. This script disables all users who have not logged on for longer that the number of days specified.

The first script uses the lastLogonTimeStamp attribute in Active Directory (introduced in Windows 2003) to determine when a user last logged in. This is the easiest way to detect stalled accounts. However, this attribute is only replicated every 10 to 14 days, based on an interval that is randomly calculated using the domain attribute msDS-LogonTimeSyncInterval. Using this attribute introduces a delay in determining inactive accounts and therefore may not meet some companies' requirements. If that's your case,  you may want to either consider reducing the “msDS-LogonTimeSyncInterval”, which could potentially have undesirable replication impacts, or you may want to use the second script which relies on the lastLogon attribute instead.

The first script is simpler and much more efficient than the second script because it can lookup for all the stalled accounts from Active Directory by just issuing an  LDAP search request with the following LDAP filter:

 (&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(lastLogonTimeStamp<=" + $ lastLogonIntervalLimit + "))

This filter requests for all the accounts that meet the following conditions:

· Are of object class “user”,

· Are enabled,

· Have a “lastLogonTimeStamp” attribute set to a date that is greater than $lastLogonIntervalLimit which is equal to (current-date - $NbDays). 

Here is the code for the first script using the lastLogonTimeStamp attribute:

# Read the input parameters $Subtree and $NbDays

param([string] $Subtree = $(throw write-host `

      "Please specify the DN of the container under which inactive accounts should be queried from." -Foregroundcolor Red),`

      [string] $NbDays = $(throw write-host `

      "Please specify the maximum number of days of inactivity allowed. Users who have not logged on for longer that the number of`

      days specified will get disabled." -Foregroundcolor Red))

 

# Get the current date

$currentDate = [System.DateTime]::Now

# Convert the local time to UTC format because all dates are expressed in UTC (GMT) format in Active Directory

$currentDateUtc = $currentDate.ToUniversalTime()

 

# Set the LDAP URL to the container DN specified on the command line

$LdapURL = "LDAP://" + $Subtree

 

# Initialize a DirectorySearcher object

$searcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]$LdapURL)

 

# Set the attributes that you want to be returned from AD

$searcher.PropertiesToLoad.Add("displayName") >$null

$searcher.PropertiesToLoad.Add("sAMAccountName") >$null

$searcher.PropertiesToLoad.Add("lastLogonTimeStamp") >$null

 

# Calculate the time stamp in Large Integer/Interval format using the $NbDays specified on the command line

$lastLogonTimeStampLimit = $currentDateUtc.AddDays(- $NbDays)

$lastLogonIntervalLimit = $lastLogonTimeStampLimit.ToFileTime()

 

Write-Host "Looking for all users that have not logged on since "$lastLogonTimeStampLimit" ("$lastLogonIntervalLimit")"

 

$searcher.Filter = "(&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(lastLogonTimeStamp<=`

" + $lastLogonIntervalLimit + "))"

 

# Run the LDAP Search request against AD

$users = $searcher.FindAll()

 

if ($users.Count -eq 0)

{

       Write-Host " No account needs to be disabled.”

}

else

{

       foreach ($user in $users)

       {

              # Read the user properties

              [string]$adsPath = $user.Properties.adspath

              [string]$displayName = $user.Properties.displayname

              [string]$samAccountName = $user.Properties.samaccountname

              [string]$lastLogonInterval = $user.Properties.lastlogontimestamp

 

              # Disable the user

              $account=[ADSI]$adsPath

              $account.psbase.invokeset("AccountDisabled", "True")

              $account.setinfo()

 

              # Convert the date and time to the local time zone

              $lastLogon = [System.DateTime]::FromFileTime($lastLogonInterval)

             

              Write-Host " Disabled user "$displayName" ("$samAccountName") who last logged on "$lastLogon" ("$lastLogonInterval")"          

       }

}

 

The second script uses the attribute lastLogon. This attribute is expressed similarly to lastLogonTimeStamp in Large Integer Interval which is the number of 100-nanosecond intervals that have elapsed since the 0 hour on January 1, 1601.  However, the lastLogon attribute is not replicated across DCs and is only set on the domain controller that the user logs on to. For this reason this script has to query all the DCs that compose the domain. If the same user has logged on to multiple DCs and therefore has different lastLogon values recorded in these DCs, the script has to only consider the latest lastLogon value. The script also disables all users that have never logged on (their lastLogon attribute is set to 0) and whose accounts have been created more than 8 days ago (their whenCreate attribute is less than the current-date - 8 days).

Here is how this script operates:

·         It first gets the list of all the domain controllers in the current domain.

·         For each domain, it Issues an LDAP search request with the following LDAP filter:

  (&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(whenCreated<=" + $creationDateStr + "))"

This filter requests for all the accounts that meet the following conditions:

· Are of object class “user”,

· Have a "whenCreated" attribute set to a date that is less than $creationDateStr which is equal to (current-date - 8 days), meaning that the accounts have been created more than 8 days ago.

·      For each user returned by the query, it adds the user lastLogon time stamp into a hashtable. This hashtable is used to record the latest lastLogon time stamp for each user. So if the same user has logged on to another DC and the logon time stamp for the user on this DC is greater than the one previously recorded for the same user in the hashtable, then the user time stamp in the hashtable is overwritten with the latest time stamp.

·      For each user in the hashtable, if the user’s recorded logon time stamp is less than $lastLogonIntervalLimit, which is equal to (current-date - $NbDays), then the user is disabled.

Here is the code for the second script using the lastLogon attribute:

# Read the input parameters $Subtree and $NbDays

param([string] $Subtree = $(throw write-host `

      "Please specify the DN of the container under which inactive accounts should be queried from." -Foregroundcolor Red),`

      [string] $NbDays = $(throw write-host `

      "Please specify the maximum number of days of inactivity allowed. Users who have not logged on for longer that the number of`

      days specified will get disabled." -Foregroundcolor Red))

 

# Get the current date

$currentDate = [System.DateTime]::Now

# Convert the date to UTC format because all dates are expressed in UTC (GMT) format in Active Directory

$currentDateUtc = $currentDate.ToUniversalTime()

 

Write-Host "--------------------------------------------"

Write-Host " Disabling Inactive Accounts "

Write-Host " "$currentDate

Write-Host "--------------------------------------------"

Write-Host  

 

# Initialize a hashtable where we are going to store the users' latest Lastlogon

$inactiveUserList = new-object System.Collections.HashTable

 

# Get the list of all the domain controllers for the current domain

$DCs = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().DomainControllers

foreach ($DC in $DCs)

{

       # Set in the LDAP URL the DC hostname and the container DN specified on the command line

       $LdapURL = "LDAP://" + $DC.Name + "/" + $Subtree

 

        # Initialize a DirectorySearcher object

       $searcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]$LdapURL)

      

       # Set the attributes that you want to be returned from AD

       $searcher.PropertiesToLoad.Add("distinguishedName") >$null

       $searcher.PropertiesToLoad.Add("displayName") >$null  

       $searcher.PropertiesToLoad.Add("lastLogon") >$null

       $searcher.PropertiesToLoad.Add("whenCreated") >$null

 

        # Calculate the time stamp in Large Integer/Interval format using the $NbDays specified on the command line

       $lastLogonTimeStampLimit = $currentDateUtc.AddDays(- $NbDays) # Get the date and time of $NbDays days ago

       $lastLogonIntervalLimit = $lastLogonTimeStampLimit.ToFileTime()

      

       # Construct the $creationDateStr in the format expected by the attribute whenCreated

       $creationDate = $currentDateUtc.AddDays(- 1) # Get the date and time of 8 days ago

       $YYYY = $creationDate.Year.ToString()

       $MM = $creationDate.Month.ToString(); if ($MM.Length -eq 1) {$MM="0" + $MM};

       $DD = $creationDate.Day.ToString(); if ($DD.Length -eq 1) {$DD="0" + $DD};

       $hh = $creationDate.Hour.ToString(); if ($hh.Length -eq 1) {$hh="0" + $hh};

       $min = $creationDate.Minute.ToString(); if ($min.Length -eq 1) {$min="0" + $min};

       $ss = $creationDate.Second.ToString(); if ($ss.Length -eq 1) {$ss="0" + $ss};

       $creationDateStr = $YYYY + $MM + $DD + $hh + $min + $ss + '.0Z'

 

       Write-Host "Looking for all enabled users on ["$DC.Name"] whose account have been created before "$creationDate `

                  "("$creationDateStr")"

 

       $searcher.Filter = "(&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(whenCreated<="`

                         + $creationDateStr + "))"

       #Setting the paging option to allow AD to bypass its default limit of 1000 objects returned per search request.

       $searcher.PageSize = 100;

 

       # Issue the LDAP Search request against AD

       $users = $searcher.FindAll()

       if ($users.Count -eq 0)

       {

              Write-Host " No account found on the DC ["$DC.Name"]"

       }

       else

       {

              Write-Host "["$users.Count"] accounts found on the DC ["$DC.Name"]"

       }

             

       foreach ($user in $users)

       {

               # Read the user properties

              [string]$DN = $user.Properties.distinguishedname

              [string]$displayName = $user.Properties.displayname

              [string]$lastLogonInterval = $user.Properties.lastlogon

      

              # Convert the date and time to the local time zone

              $lastLogon = [System.DateTime]::FromFileTime($lastLogonInterval) # Time expressed in local time (and not GMT)

              #Write-Host " The user "$displayName" ("$DN") last logged on to the DC ["$DC.Name"] on "$lastLogon #"("$lastLogonInterval")"

      

               # If the hashtable does not already contain a record for the user add it. The key for the hashtable is the user DN

              if (!($inactiveUserList.Keys -contains $DN))

              {

                     $inactiveUserList[$DN] = $lastLogonInterval #The user logon time stamp is added to the list

              }

              elseif ($lastLogonInterval -gt $inactiveUserList[$DN])

              {

                    #If the lastLogon value read is greater than the one previously recorded for the same user on another DC, then store in`

                    the hashtable the latest value

                     $inactiveUserList[$DN] = $lastLogonInterval #The list is updated with the latest logon time stamp for the user

              }

       }

       Write-Host

}

 

if ($inactiveUserList.Count -gt 0)

{

       Write-Host "Disabling ["$inactiveUserList.Count"] accounts that have not logged on since "$lastLogonTimeStampLimit "GMT `

                  ("$lastLogonIntervalLimit")"

       # For each user account recorded in the hashtable, disable the user account if its lastLogon is less than $lastLogonIntervalLimit `

        (which is current-date - $NbDays)

       foreach ($DN in $inactiveUserList.Keys)

       {

              if ($inactiveUserList[$DN] -lt $lastLogonIntervalLimit)

              {

                     Write-Host "Disabling user: "$DN "[LastLogon:"$inactiveUserList[$DN]"]"

                     $ldapURL = "LDAP://" + $DN

                     $account= [ADSI]$ldapURL

                     $account.psbase.invokeset("AccountDisabled", "True")

                     $account.setinfo()

              }

       }

}