2008-07-23

Active Directory Clean-up Script

NOTE: This script has been updated! Please see the new post for the new script and a download link.

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:

Jhon Drake said...
This comment has been removed by the author.
james marsh said...

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.