Find Disabled, Inactive Active Directory Users Accounts with PowerShell Revisited

In an earlier article, I discussed how to use the Microsoft Active Directory module to discover disabled, expired and inactive user accounts. This requires a newer domain controller and a client with RSAT installed. But perhaps some of you can’t meet those requirements, or you need to develop a PowerShell solution that doesn’t require RSAT. There is another way to find disabled, expired, and inactive accounts, but it takes a bit more PowerShell scripting on your part. If you’re up to it, let’s dig in.

There is an entire library of .NET classes that are specifically made for working with Active Directory. You don’t need to be concerned about what version of Windows or Active Directory is running on your domain controllers when you’re working with these classes. As long as you can connect to the domain controllers over the traditional LDAP port of 389, I don’t think you will have any problems. We will be using an instance of System.DirectoryServices.DirectorySearcher for the task at hand. The solution that is demonstrated in this article works best if you’re running your PowerShell session with credentials that can search Active Directory.
First, I will create an instance of this object.

$search = New-Object System.DirectoryServices.DirectorySearcher

This is now just another PowerShell object.

Creating an instance of a PowerShell object. (Image Credit: Jeff Hicks)
Creating an instance of a PowerShell object. (Image Credit: Jeff Hicks)

By default, the searcher will search your entire domain.
Searching the domain with Windows PowerShell. (Image Credit: Jeff Hicks)
Searching the domain with Windows PowerShell. (Image Credit: Jeff Hicks)

For my purposes, I want to narrow the search.

$search.SearchRoot = [ADSI]"LDAP://CHI-DC04/OU=Employees,DC=Globomantics,DC=Local"

The SearchRoot property is expecting a System.DirectoryServices.DirectoryEntry object, and the [ADSI] type accelerator is a shortcut for creating that type of object. The LDAP moniker is case sensitive. My path includes the name of my Windows Server 2012 domain controller, CHI-DC04. You shouldn’t have to specify a domain controller, but I found I got better results if I did specify the domain controller in my own environment.
The next and most important property to define is the filter.

$filter = "(&(objectCategory=person)(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=2))"
$search.Filter = $filter

You may be wondering how I came up with that filter. I will admit that creating a LDAP filter can be complicated, but I cheat a little when I can. Open Active Directory Users and Computers, and go to the Saved Queries folder.

Creating a new query. (Image Credit: Jeff Hicks)
Creating a new query. (Image Credit: Jeff Hicks)

Right-click to create a new query. Use the Define Query option to build your query. You should eventually get a Query string.
Defining the query string. (Image Credit: Jeff Hicks)
Defining the query string. (Image Credit: Jeff Hicks)

Now you can copy and paste the query string into PowerShell. You may see a message that the string can’t be displayed because it is calculated at runtime. When that happens, you’ll need to construct the query on your own. But I have some examples on how to do so later in the article.

To invoke the search, you have two methods: FindOne() and FindAll(). I recommend testing with the former. This will search Active Directory and return the first matching object.
Using the FindOne() method in Windows PowerShell. (Image Credit: Jeff Hicks)
Using the FindOne() method in Windows PowerShell. (Image Credit: Jeff Hicks)

My query works. Let’s see what I got.
Results after using the FindOne() method. (Image Credit: Jeff Hicks)
Results after using the FindOne() method. (Image Credit: Jeff Hicks)

The resulting object is a property collection. It is not that difficult to read, but it isn’t very PowerShell friendly. Here is some code to go through the properties and turn everything into a custom object.

$r = $search.findone().Properties.GetEnumerator() | foreach -begin {$hash=@{}} -process {
$hash.add($_.key,$($_.Value))
} -end {[pscustomobject]$Hash}
$r | Select Name,Title,Department,DistinguishedName,WhenChanged,LastLogonTimeStamp

I am only interested in a few properties.

Turning information into custom objects for better readability and use within PowerShell. (Image Credit: Jeff Hicks)
Turning information into custom objects for better readability and use within PowerShell. (Image Credit: Jeff Hicks)

Although the LastLogonStamp property is pretty meaningless, that value is automatically formatted for you when you use the Microsoft cmdlets. Here, you have to handle it yourself. Or allow me to help.

Function Convert-LastLogonTimeStamp {
Param([int64]$LastOn=0)
[datetime]$utc="1/1/1601"
if ($LastOn -eq 0) {
    $utc
} else {
    [datetime]$utc="1/1/1601"
    $i=$LastOn/864000000000
    [datetime]$utcdate = $utc.AddDays($i)
    #adjust for time zone
    $offset = Get-WmiObject -class Win32_TimeZone
    $utcdate.AddMinutes($offset.bias)
}
} #end function

Armed with this, I can revise my previous command.

$r | Select Name,Title,Department,DistinguishedName,WhenChanged,@{Name="LastLogon";Expression={Convert-LastLogonTimeStamp $_.lastLogonTimeStamp}}

A variation of the previous PowerShell command. (Image Credit: Jeff Hicks)
A variation of the previous PowerShell command. (Image Credit: Jeff Hicks)

Excellent. Now that I have something that works for one, and I can expand it for all.
Using the FindAll() method in Windows PowerShell. (Image Credit: Jeff Hicks)
Using the FindAll() method in Windows PowerShell. (Image Credit: Jeff Hicks)

It looks like I have 47 disabled accounts.

$all | Select Path | Out-GridView

Using Out-GridView to see the results of my disabled accounts. (Image Credit: Jeff Hicks)
Using Out-GridView to see the results of my disabled accounts. (Image Credit: Jeff Hicks)

Now I need to process each user and turn the properties in an object that I can use.

$disabled = Foreach ($user in $all) {
$user.Properties.GetEnumerator() |
foreach -begin {$hash=@{}} -process {
$hash.add($_.key,$($_.Value))
} -end {[pscustomobject]$Hash}
}

Processing each user into an object that can be used. (Image Credit: Jeff Hicks)
Processing each user into an object that can be used. (Image Credit: Jeff Hicks) 

One thing to note is that the search query will return whatever properties are defined so when you look at a set of results, not every property will be set for every user.

$disabled | Select Name,Title,Department,DistinguishedName,WhenChanged,@{Name="LastLogon";Expression={Convert-LastLogonTimeStamp $_.lastLogonTimeStamp}}

Disabled or inactive users. (Image Credit: Jeff Hicks)
Disabled or inactive users. (Image Credit: Jeff Hicks)

But you can still do some interesting things with results.
Filtering results by department group. (Image Credit: Jeff Hicks)
Filtering results by department group. (Image Credit: Jeff Hicks) 

Or format as a nice table.

$disabled | sort Department | Format-Table -GroupBy Department -Property Name,
Title,@{Name="LastLogon";Expression={Convert-LastLogonTimeStamp $_.lastLogonTimeStamp}},
Distinguishedname

Formatting the results into a table. (Image Credit: Jeff Hicks)
Formatting the results into a table. (Image Credit: Jeff Hicks) 

Before we look at other filters, let me point out that you can also create the searcher object with a type accelerator. All you need to do is define a filter.

[adsisearcher]$Searcher = $filter
$searcher.SearchRoot = [ADSI]"LDAP://CHI-DC04/OU=Employees,DC=Globomantics,DC=Local"

This object is the same as what I created earlier. The only difference is that I didn’t need to use New-Object.

Next, let’s find expired accounts. This is one of those queries that is calculated at run time. The AccountExpires property uses the same tick concept as LastLogon so to create a filter I need to calculate a tick value.

$today = Get-Date
[datetime]$utc = "1/1/1601"
$ticks = ($today - $utc).ticks
$searcher.filter = "(&(objectCategory=person)(objectClass=user)(!accountexpires=0)(accountexpires<=$ticks))"

Finding expired user accounts with PowerShell. (Image Credit: Jeff Hicks)
Finding expired user accounts with PowerShell. (Image Credit: Jeff Hicks)

The filter should find all user accounts that have expired. But it should exclude accounts that will expire in the future and non-expiring accounts.
I can use the same concept to find user accounts that have not logged on say in the last 120 days. I am using 120 days as an indicator of an inactive account.

$days = 120
$cutoff = (Get-Date).AddDays(-120)
$ticks = ($cutoff - $utc).ticks
$searcher.filter = "(&(objectCategory=person)(objectClass=user)(lastlogontimestamp<=$ticks))"
$all = $searcher.FindAll()

I can work with results just as I did before.

$inactive = Foreach ($user in $all) {
$user.Properties.GetEnumerator() |
foreach -begin {$hash=@{}} -process {
$hash.add($_.key,$($_.Value))
} -end {[pscustomobject]$Hash}
}
$inactive | Select Name,Title,Department,DistinguishedName,WhenChanged,
@{Name="LastLogon";Expression={Convert-LastLogonTimeStamp $_.lastLogonTimeStamp}} |
Out-Gridview -title "Last Logon"

112814 1609 FindDisable17
The most difficult part of using the directory searcher is developing an LDAP filter. Using the query builder in Active Directory Users and Computers can help. Once you have mastered creating LDAP filters, you can also use them with the Microsoft Active Directory cmdlets. Over all I think you’ll find the cmdlets easier to use. But if you have some PowerShell chops, you can certainly create your own Active Directory tools.