Finding Old or Unused Accounts with Powershell v2
Here is a version that was 200 times faster in my environment. Depending on the number of domain controllers it could be even faster for you. It does one big query for each domain controller and then compiles the results. The original script took 45 minutes, this version took 13 seconds.
This script returns a list with all users and their last logon date/time. You can then filter by logon's older than a certain date/time, sort, or export it.
$dcs = [System.DirectoryServices.ActiveDirectory.Domain]::getcurrentdomain().DomainControllers | select name
$startdate = get-date('1/1/1601')
$lst = new-Object System.Collections.ArrayList
foreach ($dc in $dcs) {
$root = [ADSI] "LDAP://$($dc.Name):389"
$searcher = New-Object System.DirectoryServices.DirectorySearcher $root
$searcher.filter = "(&(objectCategory=person)(objectClass=user))"
$searcher.PropertiesToLoad.Add("name") | out-null
$searcher.PropertiesToLoad.Add("LastLogon") | out-null
$searcher.PropertiesToLoad.Add("displayName") | out-null
$searcher.PropertiesToLoad.Add("userAccountControl") | out-null
$searcher.PropertiesToLoad.Add("canonicalName") | out-null
$searcher.PropertiesToLoad.Add("title") | out-null
$searcher.PropertiesToLoad.Add("sAMAccountName") | out-null
$searcher.PropertiesToLoad.Add("sn") | out-null
$searcher.PropertiesToLoad.Add("givenName") | out-null
$results = $searcher.FindAll()
foreach ($result in $results)
{
$user = $result.Properties;
$usr = $user | select -property @{name="Name"; expression={$_.name}},
@{name="LastLogon"; expression={$_.lastlogon}},
@{name="DisplayName"; expression={$_.displayname}},
@{name="Disabled"; expression={(($_.useraccountcontrol[0]) -band 2) -eq 2}},
@{name="CanonicalName"; expression={$_.canonicalname}},
@{name="Title"; expression={$_.title}},
@{name="sAMAccountName"; expression={$_.samaccountname}},
@{name="LastName"; expression={$_.sn}},
@{name="FirstName"; expression={$_.givenname}}
$lst.Add($usr) | out-null
}
}
$lst | group name | select-object Name,
@{Expression={ ($_.Group | Measure-Object -property LastLogon -max).Maximum }; Name="LastLogon" },
@{Expression={ ($_.Group | select-object -first 1).DisplayName}; Name="DisplayName" },
@{Expression={ ($_.Group | select-object -first 1).CanonicalName}; Name="CanonicalName" },
@{Expression={ ($_.Group | select-object -first 1).Title}; Name="Title" },
@{Expression={ ($_.Group | select-object -first 1).sAMAccountName}; Name="sAMAccountName" },
@{Expression={ ($_.Group | select-object -first 1).LastName}; Name="LastName" },
@{Expression={ ($_.Group | select-object -first 1).FirstName}; Name="FirstName" },
@{Expression={ ($_.Group | select-object -first 1).Disabled}; Name="Disabled" } |
select-object Name, DisplayName, CanonicalName, Title, sAMAccountName, LastName, FirstName, Disabled,
@{Expression={ $startdate.adddays(($_.LastLogon / (60 * 10000000)) / 1440) }; Name="LastLogon" }





What am I missing?
Unexpected token 'LDAP://$($dc.Name):389' in expression or statement.
At C:\scripts\last-login.ps1:7 char:39
+ $root = [ADSI] LDAP://$($dc.Name):389 <<<<
+ CategoryInfo : ParserError: (LDAP://$($dc.Name):389:String) [], ParseException
+ FullyQualifiedErrorId : UnexpectedToken
Reply to this
There should be double quotes for some reason this got eaten when it was pasted.
$root = [ADSI] "LDAP://$($dc.Name):389"
Reply to this
Thank you! That's a great script.
Reply to this
Thanks!
Reply to this
I am sure this is too simple but why am I getting this error? Exception calling "FindAll" with "0" argument(s): "The server is not operational.
"
Reply to this
I'm guessing the error has to do with this section.
$dcs = [System.DirectoryServices.ActiveDirectory.Domain]::getcurrentdomain).DomainControllers | select name
foreach ($dc in $dcs) {
$root = [ADSI] "LDAP://$($dc.Name):389"
$searcher = New-Object System.DirectoryServices.DirectorySearcher $root
...
My money is that the $dc.Name variable doesn't contain the right data. You can validate that you are getting good data by running this from the cli.
[System.DirectoryServices.ActiveDirectory.Domain]::getcurrentdomain).DomainControllers | select name
And you should get output like this:
Name
----
DC1.domain.local
DC2.domain.local
DC3.domain.local
DC4.domain.local
...
Reply to this
Well it just goes back to the prompt with no results.
Reply to this
it was my issue. Great script thanks
Reply to this
I don't know why that wouldn't populate, but it could be that you don't have rights.
Reply to this
Have you looked into last logon timestamp? It is a replicated value that you can search for. It reduces the need to query multiple domain controllers. The value is replicated once every 14 days so it may be out of date by as much as 2 weeks. I've found that when searching for stale accounts this should still catch 99% of them.
here is a descent write up I found on it.
http://skmullen.wordpress.com/2007/02/07/lastlogontimestamp/
I haven't done much in powershell but I use a VBS script that pulls the last logon timestamp on a pretty regular basis.
Reply to this
I believe this is the same value what I am querying. The reason I interrogate each domain controller is I want a more up to date list. My major concern is flagging accounts that appear to be stale due to the lag but actually aren't. If someone takes vacation for two weeks and the last logon time is lagged by another two weeks it looks like the account hasn't been used in a month.
Reply to this
Tim, nice script :). I am new to PowerShell. How would I filter against accounts that have not been used in the last 60 days?
Also, I noticed one of the accounts reports a LastLogon of 6/19/2009 3:10:56 PM, but I know the person is active and when I look at the account on my local domain controller it clearly says they were logged in today. LastLogon 9/30/2009.
Reply to this
Something like this should work
... | where {$_.LastLogon -lt (Get-Date).AddDays(-60)}
Reply to this