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" }

 del.icio.us  Stumbleupon  Technorati  Digg 

 

What did you think of this article?




Trackbacks
  • No trackbacks exist for this entry.
Comments

  • 8/12/2009 1:12 PM AaronJ wrote:
    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
  • 8/12/2009 2:37 PM Tim Medin wrote:
    There should be double quotes for some reason this got eaten when it was pasted.
    $root = [ADSI] "LDAP://$($dc.Name):389"
    Reply to this
  • 8/12/2009 2:48 PM AaronJ wrote:
    Thank you! That's a great script.
    Reply to this
  • 8/12/2009 3:06 PM Tim Medin wrote:
    Thanks!
    Reply to this
  • 9/22/2009 10:14 AM David wrote:
    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
  • 9/22/2009 10:35 AM Tim Medin wrote:
    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
  • 9/22/2009 12:08 PM David wrote:
    Well it just goes back to the prompt with no results.
    Reply to this
  • 9/22/2009 1:27 PM David wrote:
    it was my issue. Great script thanks
    Reply to this
  • 9/23/2009 6:40 AM Tim Medin wrote:
    I don't know why that wouldn't populate, but it could be that you don't have rights.
    Reply to this
  • 9/29/2009 7:30 AM whoami9801 wrote:
    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
  • 9/29/2009 8:31 AM Tim Medin wrote:

    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
  • 9/30/2009 5:47 PM TommyJ wrote:
    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
  • 10/1/2009 7:04 AM Tim Medin wrote:
    Something like this should work
    ... | where {$_.LastLogon -lt (Get-Date).AddDays(-60)}
    Reply to this
Leave a comment

Submitted comments will be subject to moderation before being displayed.

 Enter the above security code (required)

 Name

 Email (will not be published)

 Website

Your comment is 0 characters limited to 3000 characters.