Use PowerShell to Find and Remove Inactive Active Directory Users

Summary: Guest blogger, Ken McFerron, discusses how to use Windows PowerShell to find and to disable or remove inactive Active Directory users.

Microsoft Scripting Guy, Ed Wilson, is here. One of the highlights of our trip to Canada, was—well, there were lots of highlights—but one of the highlights was coming through Pittsburgh and having dinner with Ken and his wife. When the Scripting Wife and I first met Ken in person (at the Windows PowerShell deep dive in Vegas), we were impressed with Ken’s knowledge and enthusiasm (although the Scripting Wife already knew Ken from the PowerScripting Podcast chat room this was the first time I had met him). We later had a chance to see him at Atlanta TechStravaganza 2011. He is the founder of the Pittsburgh PowerShell Users group (I am speaking in person at their first meeting on December 13, 2011), and he is extremely passionate about Windows PowerShell. Here is what Ken has to say about himself.

My name is Ken McFerron. I currently work as a senior system administrator, and I focus on Active Directory. I have been in the IT field since 1999, and I started using VBScript and Batch scripting shortly after. I have always enjoyed trying to automate as much as I can with my scripts. I was introduced to Windows PowerShell around 2008, and I have been trying to learn as much as I can about it since then. I use Windows PowerShell on a daily basis now, and I dread going back to troubleshoot or update old VBScript scripts—these usually end up getting converted to Windows PowerShell. I have been working on getting a Windows PowerShell users group started in the Pittsburgh area. On December 13, we will be having our first meeting. I cannot wait to get the group started and start sharing and learning more about Windows PowerShell with others in the area.

One big problem for companies that do not utilize an identity management system (such as Forefront Identiy Manager 2010) is stale user accounts. I have seen companies that have thousands of accounts for users who have not logged into the domain in years, or at all. With Windows PowerShell and the Microsoft Active Directory (AD) module, the task of identifying and deleting these accounts is an easy one.

First we need to determine what we need to look for. Beginning with Active Directory in Windows Server 2003, there is an attribute called LastLogonTimeStamp, which is replicated between domain controllers every 9 to 14 days. The AD module also displays this attribute in an easy-to-read format called LastLogonDate. There are some instances when this attribute is not updated, so I also like to look at PasswordLastSet.

So the first step is to query AD to find all the enabled accounts that have the attributes LastLogonTimeStamp and PasswordLastSet that are over 90 days old. Any users that have not logged on will not have a value for LastLogonDate. One way to do this is to use the Get-ADUser cmdlet, and then pipe the results to Where-Object to do the filtering as follows:

If we run Measure-Command again, we can see that the time has really decreased.

Now that we have a list of all the user accounts, we need to determine what to do with them. I like to disable the accounts first before I delete them. If you find that one of these accounts is needed, it is much easier to enable the account than to restore it. Some administrators like to move all of these user accounts to a separate OU, and disable all the accounts for X number of days before they delete them. This will work most of the time. But I do not like doing it because you can run into some issues. For example, you could run into people who have the same name. You cannot have identical distinguished names in AD, so if you try to move one, you will get and error message like this:

So I like to leave the accounts in place and update an attribute with the date that they were disabled. To keep it simple, I will use the Description attribute. When we determine how long to keep these accounts disabled, we can read this attribute and then delete any accounts that have been disabled for X number of days. To update the description attribute we would use the Set-ADUser cmdlet as follows:

Now that we have all the accounts disabled, we need to delete them. We can use the Remove-ADObject cmdlet to delete the account, and then use Get-ADUser to read the Description attribute. To compare the date that the account was disabled to the current date, we can use Where-Object, as shown here:

Be very careful with this. The command that I have provided will prompt for every user before deleting the accounts. To get a list, you can use WhatIf,or if you do not want to get prompted, you can use Confirm:$False, as shown here:

In summary, we opened this post with a couple one liners that can disable accounts for users who have not logged on or changed their passwords in the last 90 days. We just created a couple of additional one liners to delete disabled accounts after 14 days. Now we can put everything together into a single script. I added a bit of code to handle common error conditions and to log accounts that are deleted and disabled, but the essential script is the four one liners that we examined earlier. Here is the complete script:

#import the ActiveDirectory Module

Import-Module ActiveDirectory

#Create a variable for the date stamp in the log file

$LogDate = get-date -f yyyyMMddhhmm

#Sets the OU to do the base search for all user accounts, change for your env.

$SearchBase = “OU=User_Accounts,DC=DEVLAB,DC=LOCAL”

#Create an empty array for the log file

$LogArray = @()

#Sets the number of days to delete user accounts based on value in description field

$Disabledage = (get-date).adddays(-14)

#Sets the number of days to disable user accounts based on lastlogontimestamp and pwdlastset.

Hey Ed, I am new to Windows Power Shell and this script does almost exactly of what I was looking for, however, I need to change the code sothat the script will check the user accounts in multiple OUs and move the accounts that have not been logged in to in X days to a disabled users accounts OU (that already exists), and then check that OU foraccounts that have been disabled for more than X days and delete them. Do you have any suggestions?

Thanks, nice article about find and remove inactive AD users and I found good information fromhttp://www.lepide.com/active-directory-cleaner/ which helps to manage inactive account(users and computers) and disable or delete the accounts that are not logged on for more than a specific period
to improve the security of the company.

This is a great script. I'm using it in my environment. The only problem I tend to run into is that it also pulls recently created account, accounts that have not been logged into yet (accounts created within 7 days). How would I exclude ad accounts that have been created within the past 7 days? I've looked at putting a filter in for the account creation date. but Haven't been able to get it to work. I'm fairly new to powershell so all the help you could give me would be greatly appreciated.

You can have a look at Lepide active directory cleaner(http://www.lepide.com/active-directory-cleaner/) that is having equipped with several prominent features and helps to easily locate user
accounts that are obsolete or not in use for a long time by defining accurate inactivity period. Further, you can take appropriate action to remove, disable or move them to another OU, depends upon your requirement.

As I mentioned in my post I do not like moving disabled users to a Single OU. While this may work for most small companies you could run into problems with users with the same name. I posted a screenshot of a sample error message you would get using the move-adobject cmdlet with duplicate names. Thanks for posting a link to your script. I always like looking at other scripts to get ideas.

Thanks for your reply. I used the description attribute for simplicity in the blog post. I agree there could be some useful information in there. The best way would be to extend the schema with a custom attribute or use a attribute that is not in use. I liked to use the exchange extension attributes before but now with the Odata restrictions on Exchange I stay away from using those.

You bring up a good point about the date format. I am not use to having people in other countries reading anything I post and did not think about the format. I like your solution better and will keep that in mind for any future date formats I use.

Thanks again for you response to this post and all the other ones you respond to. The information you provide has been very helpful in learning more about powershell.

You might also consider moving accounts when your script disables them to a specific OU using the Move-ADObject cmdlet. This way the helpdesk, security or some other team that reviews stale accounts can look in one spot. You're also welcome to graft on some bits from a script I wrote a while back that gathers up the disabled/deleted accounts in arrays, then builds them into an HTML email to be sent to the relevant parties. I'm trying to make a summary email a staple of any automation scripts I write…

this is an interesting, reasonable and careful approach to solve the problem of inactive users!

I like the idea to use the description attribute and store the disabling date in it even if this attribute might contain some useful information that have to be restored in case of renabling the account later on.

( An alternative approach might be: Read the Information attribute and prefix it with the 10 characters of the short DateTime string — it could be removed easily later on, if the account should be enabled again.

But even this doen't work if the resulting length would be longer as the max. length of the attribute )

And one other little thing to mention is that the test with $regexp to verify that a date is comtained in the description field doesn't work in germany!

We use netwrix inactive users tracker for this. It’s a freeware tool that automatically detects and reports on all user accounts that have been inactive for a specified number of days. They also offer it as part of their identity management suite.

But between disabling and later on deleting, it will be nice to remove all groups from the user before the deletion so if the data owners are reviewing group memberships than the disabled users are no longer and we have not orphans members. What will be the code to strip out all access groups (except Domain Users) ?

Hey Ken. Thanks for the article but I am having awful trouble getting any of it to work. I tried running the first code example and it just returns with nothing. No list is actually generated and the command completes immediately with no error. I then tried the second code example and get the exact same thing. No error and no list is generated. Hell, I apparently can't even use the code example listed under Search-ADAccount for finding users who haven't logged in for 90 days as it returns every single user account in my entire active directory, including mine. It appears I can either automatically disable every account in the domain (including mine) or I can get no users at all. Do you know what could be happening? I am using Server 2008r2.

The set-aduser -description command does not allow piped imput generating the following error

Set-ADUser : The input object cannot be bound to any parameters for the command

either because the command does not take pipeline input or the input and its p

roperties do not match any of the parameters that take pipeline input.

Your script accounts for this by useing a fore loop to set the description, but if you could remove the part of the command that does not work, that would be nice, or at least note that it does not work.

Great script! I’d like to add a column to the log file that includes the lastLogonTimestamp but the output is coming out blank (see below). Any ideas why? The value definitely exists in AD for these disabled users. I have a hunch that it has something to do with the actual value (e.g. – 130100816639918715) vs. the displayed value (e.g. – 4/10/2013 11:34:23 AM Eastern Standard Time). The syntax is categorized as “Large Integer/Interval.”