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()

              }

       }

}

Comments

  • Anonymous
    January 01, 2003
    PingBack from http://justanothersysadmin.wordpress.com/2008/03/24/find-disabled-and-inactive-user-and-computer-accounts-using-powershell-part1/

  • Anonymous
    January 01, 2003
    In my environment to find inactive users and disable them, I use Lepide active directory cleaner(http://www.lepide.com/active-directory-cleaner/ ) that works great for me. It provide the option to find-out and locate user accounts that are obsolete or not in use for a long time and take appropriate action further according to your requirement such as (remove, disable or move them to another
    OU).

  • Anonymous
    January 01, 2003
    I have found that when retrieving the User account properties (e.g. "string]$lastLogonInterval = $user.Properties.lastlogontimestamp") the property value is only correctly retrieved if the property name is all lowercase (e.g. "lastlogontimestamp"). Any other case causes a null value to be returned. If you look at the property names in ADSI Edit, for example, they are all Camel cased (e.g. "lastLogonTimestamp")

  • Anonymous
    January 01, 2003
    The comment has been removed

  • Anonymous
    July 05, 2010
    If LastLogon (LastLogonTimeStamp) not set, script not analyze this user.

  • Anonymous
    October 21, 2010
    The comment has been removed

  • Anonymous
    November 05, 2010
    The term 'which' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the pat h is correct and try again.

  • Anonymous
    July 11, 2011
    Netwrix inactive users tracker monitors all stale  user accounts, and is offered as freeware. If you’re like me and struggle with scripts, this is a very easy solution that I recommend. Download it from netwrix.com

  • Anonymous
    March 06, 2012
    Thaks Eddie-- I just downloaded the NetWrix tool, and it works very ell. Have you evaluated the enterprise version? We're using the freeware version right now, and I'm wondering if you can tell me how the versions differ.

  • Anonymous
    September 12, 2012
    Sorry for the delayed response Jared. Yes, I have evaluated both versions, and there were obviously  a few key  differences that made the enterprise version better than the freeware version. For one, the enterprise version allows automatic disablement or deletion of stale accounts, so there is no need to manually disable them. The enterprise version can also process inactive computer accounts (the freeware version can’t), and it allows for monitoring of inactive accounts in multiple domains/OU’s (the freeware version can only monitor a single domain) .You can see more differences on their site: www.netwrix.com/inactive_users_tracker.html

  • Anonymous
    April 16, 2014
    ASN AD Inactive Account Tracker tool helps you to disable/reset/move inactive users/computers ,

    Find here, http://www.adsysnet.com/downloads.aspx

  • Anonymous
    October 06, 2015
    Eddie, Jared thank you for using Netwrix Inactive Users Tracker tool(https://www.netwrix.com/free_tool_for_tracking_of_inactive_users.html)! Eddie has stated all key differences, I just want to add that full version Netwrix Inactive Users Tracker is also a part of Netwrix Auditor for Active Directory application -https://www.netwrix.com/active_directory_auditing.html.

  • Anonymous
    February 01, 2016
    Cleaning up AD should be automated if there is such possibility. Here's a good example of how it can be done:http://www.adaxes.com/tutorials_AutomatingDailyTasks_AutomaticallyDeprovisionInactiveActiveDirectoryUsers.htm