Working with the lastLogon attribute in PowerShell

I have been spending a lot of time working on Powershell scripts. Specifically, I have been re-writing many VB scripts and processes into more efficient powershell versions. In my last post I mentioned that I planed to share how I made a task that took 8 hours to execute run in only 28 minutes. There are two parts to this efficiency — Start-Job and Group-Object. In a future post I’ll write more about the Start-Job functionality, but the greatest benefit was from Group-Object — and thats what I’ll focus on in this post.

The task I re-wrote looked at about 50,000 user accounts and collected many details about the accounts for auditing purposes. Due to some constraints, I needed to use the lastLogon attribute instead of lastLogonTimeStamp. You can read more about the constraints here: http://www.rlmueller.net/Last%20Logon.htm, but this line pretty much sums it up: Because the lastLogon attribute is not replicated in Active Directory, a different value can be stored in the copy of Active Directory on each Domain Controller. In VB script, I was using an LDAP bind to each domain controller for each user account and then evaluated the lastLogon attribute, which was very inefficient.

Here is the powershell version of this code, which is much more efficient and flexible (as you can get the last login time from each/all domain controllers very easy).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$lastLogons = @()
$domainControllers | %{ # I used an LDAP query for computers with primaryGroupID=516 to get a list of domain controllers distinguished names
    $objDC = New-Object DirectoryServices.DirectoryEntry "LDAP://$_"
    $dcName = $objDC.dnsHostName.ToString()
    $de = New-Object System.DirectoryServices.DirectoryEntry ("LDAP://$dcName")
    $Rech = New-Object System.DirectoryServices.DirectorySearcher($de)
    $Rech.filter = "(&(objectCategory=User)(!objectClass=Computer)(lastLogon>=1))"
    $Rech.SearchScope = "subtree"
    $Rech.sizelimit = "90000"
    $Rech.pagesize = "90000"
    $Rech.PropertiesToLoad.Add("distinguishedName");
    $Rech.PropertiesToLoad.Add("lastLogon");
    $liste = $Rech.FindAll()
    $lastLogons += ($liste | select @{n='DN';e={$_.properties.distinguishedname}},@{n='LastLogon';e={$_.properties.lastlogon}}, @{n='DC';e={$dcName}})
}
 $groupedLastLogin = $lastLogons | Group-Object -Property DN -AsHashTable -AsString
 
# To get an individuals last login information, just select it from the hash table
$trueLastLogin = $groupedLastLogin.Item($objUser.distinguishedName.ToString()) | sort LastLogon -Descending | select -First 1
$lastDC = $trueLastLogin.DC
$lastLogonTrueDate = [datetime]::FromFileTimeUTC($trueLastLogin.lastLogon)

After creating this code, I attempted to select user information from the $lastLogons variable. This was very slow — even slower than the previous VB script that did excessive LDAP binds. The efficiency in this script was gained by grouping the objects into a hash table ($groupedLastLogin) by distinguished name, and then accessing the specific key value as needed.

I know several of my posts have strayed from the normal VMware/vSphere/PowerCLI topics recently, but I have some plans to get back on track early next year. Please stay tuned!

2 comments

  1. Hecktor says:

    Thank you for posting this, however i get this error when i try to run it

    You cannot call a method on a null-valued expression.

    At C:\Scripts\old diff.ps1:4 char:42
    + $dcName = $objDC.dnsHostName.ToString <<<< ()
    + CategoryInfo : InvalidOperation: (ToString:String) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

    0
    1
    Exception calling "FindAll" with "0" argument(s): "Unknown error (0x80005000)"

    At C:\Scripts\old diff.ps1:13 char:27
    + $liste = $Rech.FindAll <<<< ()
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException

    You cannot call a method on a null-valued expression.

    At C:\Scripts\old diff.ps1:19 char:76
    + $trueLastLogin = $groupedLastLogin.Item($objUser.distinguishedName.ToString <<<< ())
    | sort LastLogon -Descending | select -First 1
    + CategoryInfo : InvalidOperation: (ToString:String) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

  2. Sorry for the delay in responding, I needed this code today and just happened across your comment.

    In my scenario, the $domainControllers variable contained an array of domain controller distinguishedNames. For example, $domainControllers = @(“CN=dc1,OU=Domain Controllers,DC=domain,DC=local”,”CN=dc2,OU=Domain Controllers,DC=domain,DC=local”) The script then completes an LDAP bind to each domain controller to get the DNS Host Name. With the error you are getting, it appears that the $objDC.dnsHostName attribute does not contain string text. Since this is automatically populated by all Windows servers, I’m assuming that the previous LDAP bind failed, possibly due to no domain controllers being stored in the $domainControllers variable. You can confirm this by adding $_ ; to the beginning of line 3 ( DollarSign UnderScore semicolon ). This should write the first domain controller DN to the screen before attempting the LDAP bind. You’ll want to make sure the value echoed to the screen is a valid DN of a domain controller.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

Notify me of followup comments via e-mail. You can also subscribe without commenting.