5/26/2004 -- My business partner, Derek Melber, was recently asked by one of our consulting clients to create a script that would aid them in auditing their domain security. He wanted to create a report that would display users' account status, including lockout, disabled, last time their password was changed, and so forth. He also wanted to display users' last logon date.
All that information is in the domain, but the last logon date in particular can be difficult to figure out in pre-Win2003 domains. That's because, prior to Win2003, the "lastLogon" attribute isn't replicated, meaning only the last DC that a user authenticated to will have an accurate value. So to display an accurate value, you'd have to query every domain controller in the domain—perfect gruntwork for a script, but not the easiest thing to code. I had some free time this past Saturday, though (I have no life), and decided to take a whack at it, using Derek's original script as a starting point.
One thing I wanted to do was be able to check every user in the domain, which is a less-than-straightforward task in AD, where users can be buried under a tree of organizational units (OUs). Fortunately, AD is backward-compatible with Windows NT domains, meaning AD can present its user list as a flat list, ignoring OU membership. Windows NT also provides an easier-to-use LastLogin property for users; AD's native lastLogon property (note the subtle name difference) is a complex integer value that includes both a time and date, and it's a pain to fuss with in VBScript.
On the other hand, NT doesn't make it very easy to enumerate a list of all DCs in the domain. AD doesn't exactly make it easier, but it does have a helpful Domain Controllers OU, which, nine times out of ten, contains every DC in the domain. In fact, pretty much nobody recommends ever moving DCs out of the Domain Controllers OU, so it's a safe bet that all the DCs' computer accounts can be found there.
Given the relative strengths and weaknesses of NT and AD, then, I resolved to use both. The result is the following script, which uses Active Directory Services Interface (ADSI) and VBScript to query an AD domain both from an NT standpoint (using the WinNT: moniker) and from a native AD standpoint (using the LDAP: moniker). Check it out:
'get domain and container Dim sTarget, sOU sTarget = InputBox("Target what domain?", "Domain?", _ "techmentorevent")
sLDAP = InputBox("LDAP version of domain name?", "LDAP Domain?", _ "dc=techmentorevents,dc=pri")
'iterate through all DCs? Dim bCheckAll bCheckAll = MsgBox("Check all DCs for last logon date?", _ 4, "Check all DCs?") If bCheckAll = 6 Then bCheckAll = True Else bCheckAll = False End If
'objects for output file Dim sOutput, oFSO, oTS sOutput = InputBox("Output path and filename?", "Filename?", _ "c:\domainreport.csv") Set oFSO = CreateObject("Scripting.FileSystemObject")
'see if file exists If oFSO.FileExists(sOutput) Then MsgBox("File exists. Please select another.") WScript.Quit End If
'create output file Set oTS = oFSO.CreateTextFile(sOutput) Const cComma = ","
'output header oTS.WriteLine "User" & _ cComma & "Disabled" & cComma & "Locked" & _ cComma & "IdleDays"
'get destination container On Error Resume Next Set oOU = GetObject("WinNT://" & sTarget) If Err <> 0 Then MsgBox "Error: " & vbCrLf & Err.Description WScript.Quit End If On Error Goto 0
'filter for user objects oOU.Filter = Array("User")
'get the Default Domain Controllers container Dim oDCs, oDC Set oDCs = GetObject("LDAP://ou=Domain Controllers," & _ sLDAP)
'go through container objects Dim oObject For Each oObject In oOU
'get attributes Dim bDisabled, bLocked, iIdleDays Dim dLastLogin, dLocalLastLogin
'reset last login date dLastLogin = CDate("1/1/1900")
'is user disabled? If oObject.accountdisabled Then bDisabled = True Else bDisabled = False End If
'is user locked out? If oObject.isaccountlocked Then bLocked = True Else bLocked = False End If
'output basic info oTS.Write oObject.name & cComma & _ bDisabled & cComma & _ bLocked & cComma
'check for last login? If bCheckAll Then
'checking last login on all DCs For Each oDC In oDCs
'get last login from this DC Dim oLocalUser Set oLocalUser = GetObject("WinNT://" & _ Replace(oDC.sAMAccountName,"$","") & "/" & _ oObject.name & ",user")
On Error Resume Next dLocalLastLogin = oLocalUser.LastLogin If Err <> 0 Then dLocalLastLogin = CDate("1/1/1900") End If On Error Goto 0
'is that more recent? If dLocalLastLogin > dLastLogin Then dLastLogin = dLocalLastLogin End If
Next
'output last login oTS.Write DateDiff("d",dLastLogin,Date) & vbcrlf
Else 'not checking last login oTS.Write "n/a" & vbcrlf End If
Next
'close text file and notify oTS.Close MsgBox "Finished. Output is at " & sOutput
When you run this script, you'll have to enter the domain name twice. First, enter a normal NT domain name (the default is TECHMENTOREVENT). Next, enter the LDAP-style domain name. The default is "dc=techmentorevents,dc=pri" for my example domain, techmentorevents.pri. If your domain was braincore.net, you'd enter "dc=braincore,dc=net" for the domain name. You can decide whether or not to check the LastLogin property, because doing so will cause the script to run for a lot longer since it has to query a bunch of computers. The script will, by the way, bomb out if any of your DCs are unreachable. I left it that way on purpose, but you can have it ignore missing DCs by using the following code:
'get last login from this DC Dim oLocalUser On Error Resume Next Set oLocalUser = GetObject("WinNT://" & _ Replace(oDC.sAMAccountName,"$","") & "/" & _ oObject.name & ",user")
The boldfaced line of code tells VBScript to ignore any errors and keep right on processing.
I hope this will be a useful script for you. Those of you lucky enough to work in a native Windows Server 2003 domain don't need this trickery, because Win2003 repliates the lastLogin property to all DCs; just use the Saved Queries feature in 2003's AD Users and Computers console to query for user accounts that haven't logged in during a specified number of days. Note that it's impossible for me to test this script in the myriad environments out there; it's possible your environment may contain something that causes the script to bomb unexpectedly. If it does, please feel free to send me a short e-mail describing the problem and I'll do my best to help you figure it out. This script outputs a file that can easily be imported into Excel so that you can sort by column or do whatever other fancy stuff you like. |