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:
get-aduser -SearchBase "OU=User_Accounts,DC=DEVLAB,DC=LOCAL" -filter * -Properties lastlogondate, passwordlastset | Where-Object {($_.lastlogondate -le $90days -or $_.lastlogondate -notlike "*")-AND ($_.passwordlastset -le $90days) -AND ($_.Enabled -eq $True)} | Select-Object name, lastlogondate, passwordlastset
Doing it this way will work, but it is not the most efficient. By running Measure-Command on my virtual machine, you can see how long this took to complete for about 10,000 users.
A better way to filter the users would be to remove the pipe to Where-Object, and use the following filter:
$90Days = (get-date).adddays(-90)
Get-ADUser -SearchBase "OU=User_Accounts,DC=DEVLAB,DC=LOCAL" -filter {(lastlogondate -notlike "*" -OR lastlogondate -le $90days) -AND (passwordlastset -le $90days) -AND (enabled -eq $True)} -Properties lastlogondate, passwordlastset | Select-Object name, lastlogondate, passwordlastset
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:
Get-ADUser -SearchBase "OU=User_Accounts,DC=DEVLAB,DC=LOCAL" -filter {lastlogondate -le $90days -AND passwordlastset -le $90days} -Properties lastlogondate, passwordlastset | set-aduser -Description (get-date).toshortdatestring())
This will update the description, but not disable the account. So we need to disable the account as well. We can use the PassThru switch to update the description and disable each account.
Get-ADUser -SearchBase "OU=User_Accounts,DC=DEVLAB,DC=LOCAL" -filter {lastlogondate -le $90days -AND passwordlastset -le $90days} -Properties lastlogondate, passwordlastset | set-aduser -Description ((get-date).toshortdatestring()) –passthru | Disable-ADAccount
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:
$14days = (get-date).adddays(-14)
Get-Aduser -SearchBase "OU=User_Accounts,DC=DEVLAB,DC=LOCAL" -Filter {enabled -eq $False} -properties description | Where { (get-date $_.Description) -le $14Days} | remove-adobject
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:
Get-Aduser -SearchBase "OU=User_Accounts,DC=DEVLAB,DC=LOCAL" -Filter {enabled -eq $False} -properties description | Where { (get-date $_.Description) -le $14Days} | remove-adobject –whatif
Get-Aduser -SearchBase "OU=User_Accounts,DC=DEVLAB,DC=LOCAL" -Filter {enabled -eq $False} -properties description | Where { (get-date $_.Description) -le $14Days} | remove-adobject –confirm:$False
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.
$PasswordAge = (Get-Date).adddays(-90)
#RegEx pattern to verify date format in user description field.
$RegEx = '^(0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])[- /.](20)\d\d$'
#Use ForEach to loop through all users with description date older than date set. Deletes the accounts and adds to log array.
ForEach ($DeletedUser in (Get-Aduser -searchbase $SearchBase -Filter {enabled -eq $False} -properties description ) ){
#Verifies description field is in the correct date format by matching the regular expression from above to prevent errors with other disbaled users.
If ($DeletedUser.Description -match $Regex){
#Compares date in the description field to the DisabledAge set.
If((get-date $DeletedUser.Description) -le $Disabledage){
#Deletes the user object. This will prompt for each user. To suppress the prompt add "-confirm:$False". To log only add "-whatif".
Remove-ADObject $DeletedUser
#Create new object for logging
$obj = New-Object PSObject
$obj | Add-Member -MemberType NoteProperty -Name "Name" -Value $DeletedUser.name
$obj | Add-Member -MemberType NoteProperty -Name "samAccountName" -Value $DeletedUser.samaccountname
$obj | Add-Member -MemberType NoteProperty -Name "DistinguishedName" -Value $DeletedUser.DistinguishedName
$obj | Add-Member -MemberType NoteProperty -Name "Status" -Value 'Deleted'
#Adds object to the log array
$LogArray += $obj
}
}
}
#Use ForEach to loop through all users with pwdlastset and lastlogontimestamp greater than date set. Also added users with no lastlogon date set. Disables the accounts and adds to log array.
ForEach ($DisabledUser in (Get-ADUser -searchbase $SearchBase -filter {((lastlogondate -notlike "*") -OR (lastlogondate -le $Passwordage)) -AND (passwordlastset -le $Passwordage) -AND (enabled -eq $True)} )) {
#Sets the user objects description attribute to a date stamp. Example "11/13/2011"
set-aduser $DisabledUser -Description ((get-date).toshortdatestring())
#Disabled user object. To log only add "-whatif"
Disable-ADAccount $DisabledUser
#Create new object for logging
$obj = New-Object PSObject
$obj | Add-Member -MemberType NoteProperty -Name "Name" -Value $DisabledUser.name
$obj | Add-Member -MemberType NoteProperty -Name "samAccountName" -Value $DisabledUser.samaccountname
$obj | Add-Member -MemberType NoteProperty -Name "DistinguishedName" -Value $DisabledUser.DistinguishedName
$obj | Add-Member -MemberType NoteProperty -Name "Status" -Value 'Disabled'
#Adds object to the log array
$LogArray += $obj
}
#Exports log array to CSV file in the temp directory with a date and time stamp in the file name.
$logArray | Export-Csv "C:\Temp\User_Report_$logDate.csv" -NoTypeInformation
Guest Blogger Week will continue tomorrow when Josh Gavant will talk about using SharePoint Web Services with Windows PowerShell to query for search results.
Thank you, Ken, and see you in a couple of weeks for the Pittsburgh PowerShell Users Group meeting.
I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
Ed Wilson, Microsoft Scripting Guy