PowerShell script to disable inactive accounts in Active Directory

PowerShell script to disable inactive accounts in Active Directory

  • Comments 11
  • Likes

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
  • PingBack from http://justanothersysadmin.wordpress.com/2008/03/24/find-disabled-and-inactive-user-and-computer-accounts-using-powershell-part1/

  • 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")

  • If LastLogon (LastLogonTimeStamp) not set, script not analyze this user.

  • An example of how to use this might help. I can't even get the script started from the PS command line!

  • 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.

  • Not sure the signifance, but I noticed that I was unable to execute $account.setinfo() as that method did not exist, after $account.psbase |gm ran, I found that CommitChanges() seemed to be a likely replacement. So in my case, running Windows 7, Powershell v2 and a 2008 domain, $account.psbase.commitchanges() is what locked the disabled property down for me. Additionally this example uses user objects, and in my case i needed to disable computer objects, maybe that's the difference, but either way I felt I should drop this comment, since I based my code off the above script.

    Thanks!

  • 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

  • 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.

  • 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

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

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

  • 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).

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment