Find Inactive User Accounts In Your Domain

Find Inactive User Accounts In Your Domain

Active Directory is a directory service that maintains information about users, computers and related objects. Here is how you can find inactive user accounts.

It is a database of relational information that needs maintenance over time to be useful and relevant. A directory will have accounts no longer used. Finding those accounts in Active Directory is not as easy as it sounds at first glance. Let’s walk through finding inactive user accounts and automating removing them.

Define Search Criteria

Let’s define what is needed for this process. Our goal is to locate employee accounts that have not logged into the network for an extended period. I’ve always used 90 or 120 days as a good measure to query inactivity by, but that number can be as low 14 days. You need to decide what is right for your organization. 90 days gives a big enough cushion to allow for people out of the office for vacation, family emergencies, or extended medical leaves. Once the accounts reach 90 days of inactivity, we want to disable those accounts and move them to a separate OU.

Defining what we are looking for first allows us to build a simple ruleset to follow. Our ruleset looks like this:

  • Find and disable active accounts that have no logon activity for 90 days.
  • Move each disabled account into the Disabled-Users OU
  • Mark each disabled user with a comment that an automated process disabled it.

Find Account Inactivity Properties

Each user account has several attributes containing login information. We want to find attributes that show last login time. If we can find those attributes, we can use them to query for accounts not logged in since a certain date. We can use PowerShell to display all the ruleset and choose an attribute to work with.

Get-ADUser is the most used cmdlet for showing user information. You could use Get-ADObject and Search-ADAccount, but Get-ADUser is the best cmdlet for our task. To show all the user properties, we need to add -properties * to the cmdlet syntax. If we leave that syntax out, we will only see the default properties, which is only 10 properties.

Get-ADUser Michael_Kanakos -Properties *

We return a long list of fields from our query (over 150!). We can search for attributes contain particular words by using wildcards. This helps us locate attributes that may be useful for our task. We want to find logon information, so let’s search for attributes containing the word Logon.

Once we tell PowerShell to get all properties, it would be helpful if we could limit the list of properties to only show properties that the match word Logon. Select-Object provides the ability to do a wild-card match and limit our results. The | character (called “the pipe”) takes the results on the left and passes them to Select-Object. Select-Object then performs the wild-card matching and then limits the results based on our wild-card criteria.

Get-ADUser username -Properties * | Select-Object *logon*

BadLogonCount          : 0
lastLogon              : 132181280348543735
LastLogonDate          : 11/11/2019 9:08:45 PM
lastLogonTimestamp     : 132179981259860013
logonCount             : 328
LogonWorkstations      :
MNSLogonAccount        : False
SmartcardLogonRequired : False

The results will vary a little based on what the domain functional level of the Active Directory is. My search returns eight attributes. Three look promising: LastLogon, LastLogonDate and LastLogonTimeStamp. Some of the values may look a little weird if you are unfamiliar with how Active Directory stores date/time information in certain attributes. Some date info is traditional date-time info, and some are saved as “ticks.”

PowerShell and .NET Framework date-time values represent dates as the number of ticks since 12:00 AM January 1, 0001. Ticks are equal to one ten-millionth of a second, which means there are 10,000 ticks per millisecond. There are math cmdlets that can convert ticks into a standard date format.

PowerShell shows the actual tick that represents the date/time. If you are comfortable converting ticks to date format, then you can look at those fields in Active Directory User and Computers or Active Directory Administrative Center. In both apps, the ticks are represented as a time-date format.



Logon Attributes Explained

Our search found multiple properties that show date/time logon information. Two properties have the same date and time and one does not. What’s going on here?

The variance in date-time info is by design and is in place to protect DC’s from getting crushed with replication traffic trying to keep all the logon info in sync. The LastLogon property updates every time you authenticate, but the data is stored on the DC you authenticated against and not replicated to the other DC’s. The LastLogonTimeStamp and LastLogonDate properties are being replicated to all DC’s but the replication only happens infrequently.

If we look at the dates, we can see how the delayed replication could impact our query. Notice that LastLogonTimeStamp is actually two days behind in this example. Every time a user interactively logs in, touches a network file share, or performs other activities that require the network to authenticate the account, it stores logon info in Active Directory. If the dc’s replicated that data EVERY TIME someone touched something on the network, the DC’s could be overwhelmed in a large environment. The result is that some logon information is accurate but not replicated, and some logon information replicates, but only occasionally.

For our requirements, we don’t need the EXACT logon timestamp. We only need to find accounts that haven’t logged on in a long time (greater than 90 days). Any value can be useful, even if the date is off by a few days. If we use my logon dates as an example, the time queried could be 11/11 or 11/13, depending on the value we use. Fast forward three months and assume I never logged on again. One date would flag as inactive for being over 90 days, and one field would not. But if I set up this process to run monthly, I would catch the account the next time we check. That’s the critical part. If we do this regularly, we can use either field as long as we consistently check.

I used the LastLogonDate property for two reasons. First, it’s already a date value, so I don’t have to deal with converting the value. Second, it’s a replicated value, and that makes my life easier. If I used LastLogon, I would have dates that are not up to date for people who are authenticating against other domain controllers in my network. I would have to query the local DC for each user to get the latest timestamp, and that is a lot of work to do and not very efficient.

To get the info on a single account, the code is simple.

get-aduser Michael_Kanakos -properties LastLogonDate | Select-Object Name, LastLogonDate

Name     LastLogonDate
----     -------------
mkanakos 11/11/2019 9:08:45 PM

To do this for all my users only requires a bit more code. We need to use a filter to query all user accounts.

Get-ADUser -filter * -properties LastLogonDate | Select-Object Name, LastLogonDate

Now let’s find only the accounts with a logon date older than 90 days. We need the current date saved to use as a comparison operator.

$date = (get-date).AddDays(-90)
Get-ADUser -Filter {LastLogonDate -lt $date} -properties LastLogonDate | Select-Object Name, LastLogonDate

This code retrieves all users who haven’t logged in over 90 days. We save the date 90 days ago to a variable. We can create an AD filter to find logons less than that date. Next, we need to add on the requirement to query only active accounts. In this example, I am using splatting to make the code more readable because the syntax is very long. Splatting AD cmdlets can be tricky, so I have also shown the long-form version of the syntax underneath the splatting example.

$date = (get-date).AddDays(-90)

$paramhash = @{
    Filter =        "LastLogonDate -lt $date -and Enabled -eq $true"
    Properties =    'LastLogonDate'

$SelectProps =            'Name','LastLogonDate','Enabled','DistinguishedName'

$InactiveUsers = Get-Aduser @paramhash | Select-Object $SelectProps

$InactiveUsers = Get-ADUser -Filter {LastLogonDate -lt $date -and Enabled -eq $true} -properties LastLogonDate, DistinguishedName | Select-Object Name, LastLogonDate, Enabled, DistinguishedName

This gives us our inactive users who are enabled. We save the results to a variable for re-use. The DistinguishedName property is needed later on. Once we find the users, we can work on our next step: disabling the accounts. We use the Set-ADUser cmdlet to make AD user account changes.

$Today = Get-Date
$DisabledUsers = (
    $InactiveUsers | Foreach-object {
        Set-User $_.DistinguishedName -Enabled $false -Description "Acct disabled on $Today via Inactive Users script"}

We have disabled our user accounts. It’s time to move them to a new OU. For our example, we’ll use the OU named Disabled-Users. The Distinguished Name for this OU is “OU=Disabled-Users,DC=Contoso,DC=Com” We use the Move-ADObject cmdlet to move users to the target OU.

$DisabledUsers | ForEach-Object {
    Move-ADObject $_.DistinguishedName -TargetPath "OU=Disabled-Users,DC=Contoso,DC=Com"}

In our final step, we used the Move-ADObject to move users into a new OU. We now have the core code needed to make a reliable, repeatable automated task. So what’s next? That depends on your individual requirements. This code can and should be set up to run automatically at the same time every month. One easy way to do that is to create a Scheduled Job to run the code on a monthly basis.

There is more we could add to this code to make it better. Code for error checking and additional user information fields would be two useful additions. Most teams running these types of tasks generate a report of what was disabled for someone to review, so the additional fields will var. You could include more fields from Active Directory to make a report more useful. We could take this task one step further by creating another task to delete those disabled users after 6 months.

Solving challenging automation requests is much easier when the tasks are divided into manageable pieces. We started with requirements and built up our syntax in small steps; confirming that each step works before trying to add in another variable. By doing this, we made this task easy to understand and hopefully a great learning experience.

Related Posts

Comments are disabled in preview mode.
Loading animation