A while back I tasked myself with creating a way to scan Active Directory for any "stale accounts", both user and computer. Our policy dictated that any user who had been disabled for more than 60 days should be removed permanently. So I set off to write a script to do just that.
This would appear to be simple. Do a few LDAP queries and then write the results to a text file or (if you're really confident) delete them automatically. However, if you're familiar with how a user authenticates with a domain, you may see the hurdle you would be required to jump for this to really work.
The script needs to query every domain controller on the network to be 100% accurate. You can try querying the LastLogonTimestamp user attribute, but that's updated on a schedule (every 14 days). The LastLogon user attribute is updated every time the user logs in, but that's kept by each individual controller and is not synchronized.
For example, your corporate office generally authenticates with your primary controller, so you execute the query (looking for LastLogonTimestamp) against that controller. Well your boss has been authenticated with one of your other controllers for the past few months, so his Timestamp shows as being a month or two old.
Oops, you just deleted his/her account.
This script will query every controller and keeps the results in a dictionary object. It then compares the dictionary objects against each other to produce one master list of users coupled with their most recent logon date and time. Finally, the scripts cuts out any user who has logged on in the last 60 days and outputs those remaining "stale" accounts to a text file on the root of C:\.
It queries both machine and user accounts.
Please excuse the formatting and length. The script is as follows:
NOTE: I know some lines are cut off. I'm working on getting something setup for downloading the file or just shortening up some of the code for the site.
Credit for the script that I built this script off of goes to Richard Mueller.
His original scripts and documentation can be found here (http://www.rlmueller.net/Last%20Logon.htm)
'------------------------------ 'Author: Christopher Maddalena w/ Richard Mueller 'Date: October 17, 2007 'Purpose: List stale user and computer account in AD in a text file. 'How: Dictionary objects are created to store the computer and user names for both disabled. ' and current accounts. These objects are segregated in this manner to allow for ' comparison. If a user/computer name exists on both lists then that account is active. ' This is done to remove the chance an active user or computer account are marked as stale ' because they have not been active on one particular domain controller. ' ADO and LDAP are used to query each domain controller sperately for the name and lastLogon ' attribute to ensure accuracy. The lastLogon attribute is compared to the current ' date and time set to the time zone bias pulled from he registry. ' The stale users and computers are written to a log file stored locally on the C:\ drive. '------------------------------ Option Explicit Call Main() WScript.Quit(0) On Error Resume Next Sub Main Dim intHigh, intLow, intSeconds, str64Bit, arrDC() Dim strNumDays, strTodaysDate, strDeleteDate, strRealDeleteDate, strTimeZoneKey, strTimeZone, k, strConfig, strDNSDomain, strQuery, strDN Dim strDTMDate, strDTMDateValue, strDTMAdjusted, strStaleDays, strUser, strBase, strFilter, strAttributes, strComputer Dim objFileSystem, objShell, objLogFile, objComputerList, objUserList, objCurrentUserList, objCurrentComputerList, objRootDSE, objCommand Dim objConnection, objRecordSet, objDC, objDate 'Set objects Set objFileSystem = CreateObject("Scripting.FileSystemObject") Set objShell = CreateObject("WScript.Shell") Set objLogFile = objFileSystem.CreateTextFile("C:\StaleAccounts.txt", True) Set objRootDSE = GetObject("LDAP://RootDSE") 'Set dictionary objects Set objComputerList = CreateObject("Scripting.Dictionary") objComputerList.CompareMode = vbTextCompare Set objUserList = CreateObject("Scripting.Dictionary") objUserList.CompareMode = vbTextCompare Set objCurrentUserList = CreateObject("Scripting.Dictionary") objCurrentUserList.CompareMode = vbTextCompare Set objCurrentComputerList = CreateObject("Scripting.Dictionary") objCurrentComputerList.CompareMode = vbTextCompare 'Set values for current date time and strNumDays = 30 'Number of days stale strStaleDays = strNumDays strStaleDays = 0 - strStaleDays strTodaysDate = CDate(Now()) strDeleteDate = CDate(Now()) - strNumDays strRealDeleteDate = "#" & strDeleteDate & "#" strDTMDateValue = DateAdd("d", strStaleDays, strTodaysDate) objLogFile.WriteLine(strDTMDateValue) 'Obtain local time zone bias strTimeZoneKey = objShell.RegRead("HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\TimeZoneInformation\ActiveTimeBias") If (UCase(TypeName(strTimeZoneKey)) = "LONG") Then strTimeZone = strTimeZoneKey ElseIf (UCase(TypeName(strTimeZoneKey)) + "VARIANT()") Then strTimeZone = 0 For k = 0 To UBound(strTimeZoneKey) strTimeZone = strTimeZone + (strTimeZoneKey(k) * 256^k) Next End If 'Convert datetime value to UTC for query strDTMAdjusted = DateAdd("n", strTimeZone, strDTMDateValue) 'Find number of seconds since 1/1/1601 intSeconds = DateDiff("s", #1/1/1601#, strDTMAdjusted) 'Convert seconds to a string and convert to 100-nanosecond intervals str64Bit = CStr(intSeconds) & "0000000" 'Determine configuration context and DNS domain from RootDSE strConfig = objRootDSE.Get("configurationNamingContext") strDNSDomain = objRootDSE.Get("defaultNamingContext") 'Use ADO to search AD for nTDSDSA to identify domain controllers Set objCommand = CreateObject("ADODB.Command") Set objConnection = CreateObject("ADODB.Connection") objConnection.Provider = "ADsDSOObject" objConnection.Open "Active Directory Provider" objCommand.ActiveConnection = objConnection strBase = " strFilter = "(objectClass=nTDSDSA)" strAttributes = "AdsPath" strQuery = strBase & ";" & strFilter & ";" & strAttributes & ";subtree" objCommand.CommandText = strQuery objCommand.Properties("Page Size") = 100 objCommand.Properties("Asynchronous") = True objCommand.Properties("Timeout") = 60 objCommand.Properties("Cache Results") = False Set objRecordSet = objCommand.Execute 'Enumerate through nTDSDSA and set domain controller AdsPaths into arrays k = 0 Do Until objRecordSet.EOF Set objDC = GetObject(GetObject(objRecordSet.Fields("AdsPath").Value).Parent) ReDim Preserve arrDC(k) arrDC(k) = objDC.DNSHostName k = k + 1 objRecordSet.MoveNext Loop objRecordSet.Close 'Retrieve lastLogon attribute for each USER on each controller For k = 0 To UBound(arrDC) strBase = " strFilter = "(&(ObjectCategory=Person)(objectClass=User))" strAttributes = "name,lastLogon" strQuery = strBase & ";" & strFilter & ";" & strAttributes _ & ";subtree" objCommand.CommandText = strQuery On Error Resume Next Set objRecordSet = objCommand.Execute If (Err.number <> 0) Then On Error GoTo 0 objLogFile.WriteLine("Domain Controller not available: " & arrDC(k)) Else On Error GoTo 0 Do Until objRecordSet.EOF strDN = objRecordSet.Fields("name").Value On Error Resume Next Set objDate = objRecordSet.Fields("lastLogon").Value If (Err.number <> 0) Then On Error GoTo 0 strDTMDate = #1/1/1601# Else On Error GoTo 0 intHigh = objDate.HighPart intLow = objDate.LowPart If (intLow < inthigh =" intHigh" inthigh =" 0)" intlow =" 0)" strdtmdate =" #1/1/1601#" strdtmdate =" #1/1/1601#"> strDTMDate Then If (objUserList.Exists(strDN) = True) Then If (strDTMDate > objUserList(strDN)) Then objUserList.Item(strDN) = strDTMDate End If Else objUserList.Add strDN, strDTMDate End If Else If (objCurrentUserList.Exists(strDN) = True) Then If (strDTMDate > objCurrentUserList(strDN)) Then objCurrentUserList.Item(strDN) = strDTMDate End If Else objCurrentUserList.Add strDN, strDTMDate End If End If If objUserList.Exists(strDN) AND objCurrentUserList.Exists(strDN) Then objUserList.Remove(strDN) End If objRecordSet.MoveNext Loop objRecordSet.Close End If Next 'Output users to log file objLogFile.WriteLine "" objLogFile.WriteLine("USERS + LAST LOGIN DATE AND TIME: ") objLogFile.WriteLine "" For Each strUser In objUserList.Keys objLogFile.WriteLine strUser & ";" & objUserList.Item(strUser) Next 'Retrieve lastLogon attribute for each COMPUTER on each controller For k = 0 To UBound(arrDC) strBase = " strFilter = "(&(objectClass=Computer)(lastLogon<=" & str64Bit & "))" strAttributes = "name,lastLogon" strQuery = strBase & ";" & strFilter & ";" & strAttributes _ & ";subtree" objCommand.CommandText = strQuery On Error Resume Next Set objRecordSet = objCommand.Execute If (Err.number <> 0) Then On Error GoTo 0 objLogFile.WriteLine("Domain Controller not available: " & arrDC(k)) Else On Error GoTo 0 Do Until objRecordSet.EOF strDN = objRecordSet.Fields("name").Value On Error Resume Next Set objDate = objRecordSet.Fields("lastLogon").Value If (Err.number <> 0) Then On Error GoTo 0 strDTMDate = #1/1/1601# Else On Error GoTo 0 intHigh = objDate.HighPart intLow = objDate.LowPart If (intLow < inthigh =" intHigh" inthigh =" 0)" intlow =" 0)" strdtmdate =" #1/1/1601#" strdtmdate =" #1/1/1601#"> strDTMDate Then If (objComputerList.Exists(strDN) = True) Then If (strDTMDate > objComputerList(strDN)) Then objComputerList.Item(strDN) = strDTMDate End If Else objComputerList.Add strDN, strDTMDate End If Else If objComputerList.Exists(strDN) AND objCurrentComputerList.Exists(strDN) Then objComputerList.Remove(strDN) End If End If objRecordSet.MoveNext Loop objRecordSet.Close End If Next objLogFile.WriteLine("") objLogFile.WriteLine("COMPUTERS + DATE OF LAST USER LOG IN: ") objLogFile.WriteLine "" For Each strComputer In objComputerList.Keys objLogFile.WriteLine strComputer & ";" & objComputerList.Item(strComputer) Next 'Clean up objConnection.Close Set objRootDSE = Nothing Set objConnection = Nothing Set objCommand = Nothing Set objRecordset = Nothing Set objDC = Nothing Set objDate = Nothing Set objUserList = Nothing Set objComputerList = Nothing Set objShell = Nothing MsgBox("Query is complete.") End Sub |
2 comments:
Thanks, it is nice script to Clean-up Active Directory but i have tried this active directory management tool to clean inactive users and computer accounts that have not logged in last x number of days. It helps to generate report which are based on inactive users, logged on, not logged on and get real last log on/ log off report.
Post a Comment