2008-09-08

New & Improved Active Directory Clean-up Script

Introducing the new and improved AD clean-up script!

This new iteration is has a new function added to it called NSLookup(). This function does just what you'd imagine it does; runs nslookup against a computer name.

Before a machine account is added to the stale account text file the NSLookup() function performs the command with that machine's name. If a name is not returned in the results then the machine is added to the stale accounts.

Another new feature of the script creates a separate text file for machine names that do return a name in the results. The text file is called Problem Accounts.txt. The reason for this is any account that is not reporting login timestamps to the domain controller(s), but is recognized by nslookup could be experiencing problems and warrant further investigation.

The new script runs just as fast as the old version. By using nslookup instead of pinging the machine name the script does not have to wait for a response or timeout. I do recommend running this script with cscript now because of this. If you simply run it as a .vbs file you'll watch a ton of command windows open and close very rapidly. It's rather amusing, but it can be irritating.

Try opening it like that with Wireshark running and you're likely to cause someone to have a seizure.

I'm now going to begin working on version 3. Originally, I did not want to automate this process completely, but now that it is practically bulletproof I am going to automatically disable and move the stale accounts. So look forward to that.

As before, 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)

Download Link







'------------------------------
'Author: Christopher Maddalena w/ Richard Mueller
'Date: October 17, 2007; Revised: September 8, 2008
'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.
' Run this script using cscript: cscript ADCleanUpV2.vbs > output.txt
'------------------------------

Option Explicit
Call Main()
WScript.Quit(0)

On Error Resume Next

Sub Main
Dim intHigh, intLow, intSeconds, str64Bit, strOutput, arrDC()
Dim strNumDays, strTodaysDate, strDeleteDate, strRealDeleteDate, strTimeZoneKey, strTimeZone, k, strConfig, strDNSDomain, strQuery, strDN
Dim strDTMDate, strDTMDateValue, strDTMAdjusted, strStaleDays, strUser, strBase, strFilter, strAttributes, strComputer

'Declare and set objects
Dim objFileSystem : Set objFileSystem = CreateObject("Scripting.FileSystemObject")
Dim objShell : Set objShell = CreateObject("WScript.Shell")
Dim objLogFile : Set objLogFile = objFileSystem.CreateTextFile("C:\Stale Accounts.txt", True)
Dim objLogFile2 : Set objLogFile2 = objFileSystem.CreateTextFile("C:\Problem Accounts.txt", True)
Dim objRootDSE : Set objRootDSE = GetObject("LDAP://RootDSE")

'Declare and set dictionary objects
Dim objComputerList : Set objComputerList = CreateObject("Scripting.Dictionary")
objComputerList.CompareMode = vbTextCompare
Dim objUserLIst : Set objUserList = CreateObject("Scripting.Dictionary")
objUserList.CompareMode = vbTextCompare
Dim objCurrentUserList : Set objCurrentUserList = CreateObject("Scripting.Dictionary")
objCurrentUserList.CompareMode = vbTextCompare
Dim objCurrentComputerList : 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
Dim objCommand : Set objCommand = CreateObject("ADODB.Command")
Dim objConnection : 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

Dim objRecordSet : Set objRecordSet = objCommand.Execute

'Enumerate through nTDSDSA and set domain controller AdsPaths into arrays
k = 0
Do Until objRecordSet.EOF
Dim objDC : 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
Dim objDate : 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 ""

objLogFile2.WriteLine "Possible Problematic Machine Accounts:"
objLogFile2.WriteLine "These machines resolve to a name, but do not appear to be communicating with the domain."

'Close text file
objLogFile.Close
objLogFile2.Close

For Each strComputer In objComputerList.Keys
NSLookup(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

Function NSLookup(strComputer)
Dim strLine, strBool

Dim objFileSystem : Set objFileSystem = CreateObject("Scripting.FileSystemObject")
Dim objLogFile : Set objLogFile = objFileSystem.OpenTextFile("C:\Stale Accounts.txt", 8, True)
Dim objLogFile2 : Set objLogFile2 = objFileSystem.OpenTextFile("C:\Problem Accounts.txt", 8, True)
Dim objShell : Set objShell = CreateObject("WScript.Shell")

Dim objWshScriptExec : Set objWshScriptExec = objShell.Exec("nslookup " & strComputer)

Dim objStdOut : Set objStdOut = objWshScriptExec.StdOut

strBool = "0"

Do Until objStdOut.AtEndOfStream
strLine = objStdOut.ReadLine
If left(strLine,4) = "Name" Then
objLogFile2.WriteLine strComputer
objLogFile2.WriteLine ""
strBool = "1"
End If
Loop
If strBool = "0" Then
objLogFile.WriteLine strComputer
End if

objLogFile.Close
objLogFile2.Close
End Function


2 comments:

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

Nice article, thanks for sharing the helpful information about active directory cleanup. I found this Lepide active directory cleaner tool ( https://blog.netwrix.com/2018/02/15/the-ten-best-free-active-directory-management-tools/ ) which provides the facilitate to cleanup inactive or stale active directory accounts and get the real last log details of accounts. This tool helps to find out inactive accounts from active directory environment and generates the report which are based on inactive user/ computer accounts, never logged on users and real last logon details of accounts.