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 did you think of this article?




Trackbacks
  • No trackbacks exist for this post.
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
  • 3/23/2010 9:57 PM Chris wrote:
    Any reason it's only doing 1000 users?
    Reply to this
  • 3/24/2010 10:04 AM Tim Medin wrote:
    An Active Directory search, by default, returns only 1000 items. To around the issue you have to use the PageSize property.

    $searcher.PageSize = 1000

    "The way to get around that issue is to assign a value to the PageSize property. When you do that, your search script will return (in this case) the first 1,000 items, pause for a split second, then return the next 1,000. This process will continue until all the items meeting the search criteria have been returned." from http://www.microsoft.com/technet/scriptcenter/topics/winpsh/searchad.mspx
    Reply to this
  • 3/28/2010 3:03 PM Chris wrote:
    Thanks for the pagesize fix.

    I'm trying to add another property, modificationdate, but aren't having much luck. I tried replacing one of your properties as a test as I thought it would be simple, but nope :(

    Can you point me in the right direction please?
    Reply to this
  • 3/28/2010 10:37 PM Tim Medin wrote:
    The property you need is called whenChanged.
    Just add this line:
    $searcher.PropertiesToLoad.Add("whenChanged") | out-null

    Reply to this
  • 2/9/2012 2:35 AM Marc wrote:
    Hi Tim,

    Very good script. Took indeed just a couple of seconds to execute. But now I'm wondering how I can export this to a csv file so I can import it in Excel?

    Kind regards,
    Marc
    Reply to this
  • 2/10/2012 8:27 AM Tim Medin wrote:
    That is the easiest part. You simple pipe the command into Export-CSV and you have the file.

    myscriptname.ps1 | export-csv myfile.csv

    Reply to this
Leave a comment

Submitted comments are subject to moderation before being displayed.

 Enter the above security code (required)

 Name (required)

 Email (will not be published) (required)

Your comment is 0 characters limited to 3000 characters.