Computers have accounts in Active Directory and log on just as user accounts do. The “user name” of a computer is its name with a dollar sign appended, e.g: MYPC1$. The password is set by the machine when it is joined to the domain and changed every 30 days by the machine. Just as with user accounts, computer accounts gets left in the domain when the machine is decomissioned or reinstalled under another name etc. This leaves our directory service full of outdated user and computer object. (Outdated in this context means “has not logged on to the domain for X days). This in turn makes it hard to know how many computers are actually active. So how to battle this problem?
On Windows 2000 every DC maintend an attribute on each computer object called lastLogon. It stored the timestamp of the last logon for a computer on that DC. The “on that DC” part is important, because the lastLogon attribute was not replicated beyond the local DC. So to figure out when computer CORP-PC1 last logged on, you would have to query the lastLogon attribute on all the DCs in the domain and find the most recent one. This could be done, but was tedious and time consuming. Enter Windows Server 2003.
In Windows Server 2003 a new attribute was introduced; lastLogonTimestamp. Just like lastLogon, lastLogonTimestamp stored the timestamp of the last logon to the domain for a computer account, but lastLogonTimestamp is a replicated attribute, which means that now every DC knows the most recent logon time for a computer. So to figure out when CORP-PC1 last logged on to any DC in the domain we can query any DC for the lastLogonTimestamp attribute of CORP-PC1. Very nice.
I often find myself in a situation where I need to “clean house” in customer’s directories so I created a script that uses lastLogonTimestamp to find all computers that have not logged on to the domain for X days. X varies greatly from customer to customer, but a good start would be 90 days. The sciprt is called FindOutdatedComputers.vbs and in this post I would like to share it with you.
FindOutdateComptuers.vbs has 3 user changeable variables: intTimeLimit, strDomain and strDC. These are defined at the very beginning of the code and should be the only variables you change.
intTimeLimit: the number of days since a computer logged on to the domain
strDomain: LDAP domain name; e.g. dc=mydomain,dc=com
strDC: FQDN of DC in domain; e.g. dc1.mydomain.com
The script must be run with cscript.exe and takes one command line argument; a 0 (zero) or a 1 (one). 0 mens just echo out the machines that have not logged on since the defined limit and 1 means echo out, but also move the comptuters to the Outdated Computer Objects OU (which the script creates). It will never delete any information from your AD, only move computer accounts to the Outdated Comptuer Objects OU. If you omit this argument, the default action is just to display the results and not move anything.
For FindOutdatedComptuers.vbs to work your domain functional level must be at least Windows Server 2003. The script checks for this and warns you if you are below the needed level.
Here is the code:
' FindOutdatedComputers.vbs
' by Morgan Simonsen
' http://morgansimonsen.wordpress.com
'
' This script will search an Active Directory domain for computer accounts that have
' not logged on the domain in the specified time limit (default 60 days).
'
' 600 000 000 100-nanosecond intervals in 1 minute
' 1440 minutes in 24-hours
' 30 * 1440 * 600 000 000 = time in 100-nanosecond intervals since 1.1.1601
'
' For Windows 2000, Windows XP and Windows Server 2003, the default computer account password change is 30 days,
' on Windows NT-based computers, the machine account password automatically changes every seven days
'
' http://support.microsoft.com/kb/q154501/
' http://support.microsoft.com/default.aspx?scid=kb;en-us;q175468
' http://www.microsoft.com/technet/scriptcenter/topics/win2003/lastlogon.mspx
'
' USAGE:
' cscript.exe FindOutdatedComputerObjects_vXXX.vbs <0|1 only list or list and move objects, default is list only>
'
' Changelog:
'
' Version 1.7 (20120228)
' - Added detection of script host (WSCRIPT.EXE/CSCRIPT.EXE)
'
' Version 1.6 (20120213)
' - Added back ability to specify domain and DC.
' Autodetection worked well for forests with only one domain.
'
' Version 1.5 (20111215)
' - Removed version from script name, this is now stored in the strScriptVersion attribute.
' - Added automatic discovery of DC
' - Added detection for FSMO roles
'
' Version 1.4 (20111201)
' - Added automatic discovery of domain
'===========================
' User changeable variables
'===========================
'Should computers be listed only or listed and moved?
strMove = 0 '1 or 0; 0 lists outdated computers only, 1 lists and moves (Default value, can be overridden with command line parameters)
'Time limit in number of days
intTimeLimit = 180
strDomain = "dc=mydomain,dc=com"
strDC = "dc1.mydomain.com"
'====================================
' Make no changes beyond this point!
'====================================
strScriptVersion = "1.7"
strScriptDate = "2012-02-28"
' See if we are running with WSCRIPT.EXE or CSCRIPT.EXE
DetectScriptEngine()
Set objRootDSE = GetObject("LDAP://RootDSE")
'strDomain = objRootDSE.Get("defaultNamingContext")
'strDC = objRootDSE.Get("dnsHostName")
strOutdatedObjectsOURDN = "OU=Outdated computer objects"
strSearchFilter = "(objectClass=computer)"
strAttributes = "name,distinguishedName,operatingSystem,dNSHostName" 'Comma separated
strLevel = "subtree"
strReboot = 0
Dim count
Set objArgs = WScript.arguments
If objArgs.Count = 0 Then
WScript.Echo "No arguments submitted, using default values"
Else
strMove = objArgs.item(0)
End If
Set objWSHShell = WScript.CreateObject("WScript.Shell")
' Echo script info
WScript.Echo "FindOutdatedComputers.vbs"
WScript.Echo "by Morgan Simonsen (www.simonsen.bz)"
WScript.Echo "Script version : " & strScriptVersion
WScript.Echo "Script date : " & strScriptDate
WScript.Echo
If CheckDomainLevel(strDomain) = False Then
WScript.Echo "Your domain is not in Windows 2003 Mode. This mode is required to check last logon time."
WScript.Echo "Attribute: lastLogonTimestamp"
WScript.Quit
End If
Dim strFSMOSchemaMaster
Dim strFSMOInfrastructureMaster
Dim strFSMOPDCEmulator
Dim strFSMORIDMaster
Dim strFSMODomainNamingMaster
Call ConfigureOU()
Call FindFSMOOwners()
'WScript.Echo "time is:" & Now - #1/1/1601#
Set objADODBConnection = CreateObject("ADODB.Connection")
objADODBConnection.Provider = "ADsDSOObject"
objADODBConnection.Open
Set objADODBCommand = CreateObject("ADODB.Command")
Set objADODBCommand.ActiveConnection = objADODBConnection
objADODBCommand.Properties("Page Size") = 500
'objADODBCommand.CommandText = "<LDAP://" & strDC & "/" & strDomain & ">;" & strSearchFilter & ";" & strAttributes & ";" & strLevel
objADODBCommand.CommandText = "<LDAP://" & strDomain & ">;" & strSearchFilter & ";" & strAttributes & ";" & strLevel
Set objRecordSet = objADODBCommand.Execute
count = 0
While Not objRecordset.EOF
'Wscript.Echo objRecordset.Fields("name")
Call GetLastLogonTime(objRecordset.Fields("distinguishedName"))
objRecordset.MoveNext
Wend
WScript.Echo "Total number of computers in domain: " & objRecordset.RecordCount
WScript.Echo "Number of computers that have not logged on in " & intTimeLimit & " days: " & count
objADODBConnection.Close
Function GetLastLogonTime(strComputerDN)
On Error Resume Next
set objComputer = GetObject("LDAP://" & strDC & "/" & strComputerDN)
'WScript.Echo "Computer: " & objComputer.cn
set objLogon = objComputer.Get("lastLogonTimestamp")
intLogonTime = objLogon.HighPart * (2^32) + objLogon.LowPart
intLogonTime = intLogonTime / (60 * 10000000)
intLogonTime = intLogonTime / 1440
'WScript.Echo "Approx last logon timestamp: " & intLogonTime + #1/1/1601#
If intLogonTime + #1/1/1601# < Now - intTimeLimit Then
Wscript.Echo "Computer: " & objRecordset.Fields("name")
WScript.Echo " Has not logged on in " & intTimeLimit & " days"
WScript.Echo " Approx last logon timestamp: " & intLogonTime + #1/1/1601#
If strMove = 1 Then
WScript.Echo " Moving computer object..."
Call MoveComputer(objComputer.distinguishedName,objComputer.Name)
End If
If strReboot = 1 Then
WScript.Echo " Attempting reboot..."
Call Restart(objComputer.dNSHostName)
End If
count = count + 1
End If
Set objComputer = Nothing
End Function
Function MoveComputer(ComputerDN,ComputerName)
On Error Resume Next
Err.Clear
Set objNewOU = GetObject("LDAP://" & strOutdatedObjectsOURDN & "," & strDomain)
Set objMoveComputer = objNewOU.MoveHere ("LDAP://" & ComputerDN, ComputerName)
WScript.Echo "Computer object moved (" & Err.Number & ")"
End Function
Function ConfigureOU()
On Error Resume Next
Err.Clear
Set objNewOU = GetObject("LDAP://" & strOutdatedObjectsOURDN & "," & strDomain)
If Err.Number <> 0 Then
WScript.Echo "OU for outdated computer objects does not exist; creating it..."
Set objDomain = GetObject("LDAP://" & strDomain)
Set objNewOU = objDomain.Create("organizationalUnit", strOutdatedObjectsOURDN)
objNewOU.SetInfo
Else
WScript.Echo "Outdated computer objects OU exists; continuing..."
End If
End Function
Function CheckDomainLevel(domain)
Set objDomain = GetObject("LDAP://" & domain)
objDomain.GetInfo
If objDomain.Get("msDS-Behavior-Version") >= 2 AND objDomain.Get("nTMixedDomain") = 0 Then
CheckDomainLevel = True
Else
CheckDomainLevel = False
End If
End Function
Function Restart(comp)
'On Error Resume Next
Err.Clear
Set objPing = GetObject("winmgmts:{impersonationLevel=impersonate}").ExecQuery("select * from Win32_PingStatus where address = '" & comp & "'")
For Each objStatus in objPing
If objStatus.StatusCode = 0 Then
'Host was reachable
' Connect to computer
Set OpSysSet = GetObject("winmgmts:{(Shutdown)}//" & comp & "/root/cimv2").ExecQuery("select * from Win32_OperatingSystem where Primary=true")
' Actual shutdown
for each OpSys in OpSysSet
OpSys.Reboot()
next
Set OpSysSet = nothing
If Err <> 0 Then
WScript.Echo " Reboot failed." & Err.Number & " " & Err.Description
Else
WScript.Echo " Reboot initiated..."
End If
Else
WScript.Echo " Host unreachable."
'Host was unreachable
End If
Next
' WScript.Quit
End Function
Function findFSMOOwners()
Set objSchema = GetObject ("LDAP://" & objRootDSE.Get("schemaNamingContext"))
strSchemaMaster = objSchema.Get("fSMORoleOwner")
Set objNtds = GetObject("LDAP://" & strSchemaMaster)
Set objComputer = GetObject(objNtds.Parent)
strFSMOSchemaMaster = objComputer.Name
Set objPartitions = GetObject("LDAP://CN=Partitions," & objRootDSE.Get("configurationNamingContext"))
strDomainNamingMaster = objPartitions.Get("fSMORoleOwner")
Set objNtds = GetObject("LDAP://" & strDomainNamingMaster)
Set objComputer = GetObject(objNtds.Parent)
strFSMODomainNamingMaster = objComputer.Name
Set objDomain = GetObject ("LDAP://" & objRootDSE.Get("defaultNamingContext"))
strPdcEmulator = objDomain.Get("fSMORoleOwner")
Set objNtds = GetObject("LDAP://" & strPdcEmulator)
Set objComputer = GetObject(objNtds.Parent)
strFSMOPDCEmulator = objComputer.Name
Set objRidManager = GetObject("LDAP://CN=RID Manager$,CN=System," & objRootDSE.Get("defaultNamingContext"))
strRidMaster = objRidManager.Get("fSMORoleOwner")
Set objNtds = GetObject("LDAP://" & strRidMaster)
Set objComputer = GetObject(objNtds.Parent)
strFSMORIDMaster = objComputer.Name
Set objInfrastructure = GetObject("LDAP://CN=Infrastructure," & objRootDSE.Get("defaultNamingContext"))
strInfrastructureMaster = objInfrastructure.Get("fSMORoleOwner")
Set objNtds = GetObject("LDAP://" & strInfrastructureMaster)
Set objComputer = GetObject(objNtds.Parent)
strFSMOInfrastructureMaster = objComputer.Name
End Function
Function DetectScriptEngine()
ScriptHost = WScript.FullName
ScriptHost = Right(ScriptHost, Len(ScriptHost) - InStrRev(ScriptHost, "\"))
If (UCase(ScriptHost) = "WSCRIPT.EXE") Then
WScript.Echo ("This script does not work with WScript." & vbNewLine & "Please run it using CSCRIPT.EXE")
WScript.Quit
End If
End Function