Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations wOOdy-Soft on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

Login Stamp

Status
Not open for further replies.

Bhavin78

IS-IT--Management
Oct 26, 2004
320
I have total 7 domain controllers in one for each site.
I want to run a query and find out account that did not log in since last 180 days. I run query from AD.

The result I get is from all 7 domain controller or just from the domain controller I ran.

I want to run it at one place and get correct result.
 
if you are searching lastlogontimestamp, then by the timeframe you are looking for, those values should be equal on all DCs as it tracks the last time a user logged onto the domain and is replicated among DCs (note that by default this value is not stored in the GC)



heres a script i wrote to ID stale objects, im copying it in here with no warranty implied :)

ive used it quite a few times...just type in the DN of the domain, the type of object (defaults to user if i remember right), and how many days is considered stale, in your case, 180 :)

i copied with word wrap on...


'=================================================================================
'=================================================================================
' -ID-STALE-OBJECTS.vbs-
' -Author: Brandon Wilson-
' -Version 5-
'The purpose of this script is to identify stale account within a given DN and its
'subtrees. You specify the DN, and you specify the number of days that consitute a
'stale account.
'=================================================================================
'=================================================================================

option explicit
on error resume next
dim ADconn, ADcmd, ADRecSet, ADusr, ADusrprop 'for the connection to AD, the command to perform, the record set results, the user object DN, and any specific properties
dim cFSO, cTxtFile, oTxtFile, cDir 'for creating the file system object, creating a text file, and writing to the text file
dim q1, q2, q3 'questions to be asked/input to collect
dim cTime, oTime, dtComp1, dtComp2 'current time, object last logon time, compare the dates
dim z, y, x 'counter variables
dim getADusr
const ForWriting = 8
const ADS_SCOPE_SUBTREE = 2


'gathering intel

q1 = inputbox("What is the Distinguished Name of the container holding the objects you wish to find? This may be a domain, OU, container, etc.", "LDAP Distinguished Name Entry Page", "OU=<ou>,OU=<ou>,DC=<domain>,DC=<extension>")
if q1 = "" then
wscript.echo "You must enter a Distinguished Name"
end if
q2 = inputbox("What type of object are you searching for? Enter as: user, computer, group, etc.", "Object Type Selection", "user")
if q2 = "" then
wscript.echo "You must enter an object type to query"
end if
q3 = inputbox("How many days is considered to be a stale object?", "Stale Object Time Specification", "60")

'creating the text files

set cFSO = createobject("scripting.filesystemobject")
set cDir = cFSO.createfolder("c:\ID-Stale-Objects")
set cDir = nothing
set cTxtFile = cFSO.createtextfile("c:\ID-Stale-Objects\Stale" & q2 & "Objects.txt")
set cTxtFile = nothing
set cTxtFile = cFSO.createtextfile("c:\ID-Stale-Objects\" & q2 & "Objects-NeverUsed.txt")
set cTxtFile = nothing

'binding to AD using LDAP to the DN specified

set ADconn = createobject("ADODB.connection")
set ADcmd = createobject("ADODB.command")
ADconn.open("PROVIDER=ADsDSOObject")
set ADcmd.activeconnection = ADconn
ADcmd.commandtext = "Select distinguishedName, Name, sAMAccountName, lastLogon, lastLogonTimestamp from 'LDAP://" & q1 & "'" & " where objectCategory=" & "'" & q2 & "'"
ADcmd.properties("Searchscope") = ADS_SCOPE_SUBTREE
ADcmd.properties("Page Size") = 50000
z = 0
y = 0
cTime = Now
set oTxtFile = cFSo_Opentextfile("c:\ID-Stale-Objects\Stale" & q2 & "Objects.txt", ForWriting)
oTxtFile.writeline("Querying for stale " & q2 & " objects in the Distinguished Name " & q1 & "." & " Date: " & cTime)
oTxtFile.writeline("Querying Distinguished Name: " & q1)
oTxtFile.writeline("Search scope: Tree and all subtrees")
oTxtFile.writeline("Object search type: " & q2)
oTxtFile.writeline("Days back to query: " & q3)
oTxtFile.writeline("Beginning process at: " & Now)
oTxtFile.writeline("--------------------------------------------------------------")
oTxtFile.writeline("--------------------------------------------------------------")
oTxtFile.writeline("--------------------------------------------------------------")
oTxtFile.writeline("--------------------------------------------------------------")
set oTxtFile = nothing
set oTxtFile = cFSo_Opentextfile("c:\ID-Stale-Objects\" & q2 & "Objects-NeverUsed.txt", ForWriting)
oTxtFile.writeline("Querying for Unused " & q2 & " objects in the Distinguished Name " & q1 & "." & " Date: " & cTime)
oTxtFile.writeline("Querying Distinguished Name: " & q1)
oTxtFile.writeline("Search scope: Tree and all subtrees")
oTxtFile.writeline("Object search type: " & q2)
oTxtFile.writeline("Days back to query: " & q3)
oTxtFile.writeline("Beginning process at: " & Now)
oTxtFile.writeline("--------------------------------------------------------------")
oTxtFile.writeline("--------------------------------------------------------------")
oTxtFile.writeline("--------------------------------------------------------------")
oTxtFile.writeline("--------------------------------------------------------------")
set oTxtFile = nothing

'moving through retrieved records and performing the comparison of last logon time stamp against the current time

set ADRecSet = ADcmd.execute
ADRecSet.movefirst
Do Until ADRecSet.EOF
ADusr = ADRecSet.fields("distinguishedName").value
set getADusr = getobject("LDAP://" & ADusr)
set ADusrprop = getADusr.get("lastLogonTimestamp")
oTime = ADusrprop.highpart * (2^32) + ADusrprop.lowpart
oTime = oTime / (60 * 10000000)
oTime = oTime / 1440
dtComp1 = oTime + #1/1/1601#
dtComp2 = cTime - q3
if dtComp1 = "1/1/1601" then
z = z + 1
set oTxtFile = cFSo_Opentextfile("c:\ID-Stale-Objects\" & q2 & "Objects-NeverUsed.txt", ForWriting)
oTxtFile.writeline("-----------------" & q2 & z & " ------------------")
oTxtFile.writeline("Object Friendly Name: " & ADRecSet.fields("Name").value)
oTxtFile.writeline("Object Logon Name/ID (sAMAccountName): " & ADRecSet.fields("sAMAccountName").value)
oTxtFile.writeline("Object Distinguished Name: " & ADRecSet.fields("distinguishedName").value)
oTxtFile.writeline("Last Logon Timestamp: " & dtComp1)
oTxtFile.writeline("Current Date/Time: " & cTime)
oTxtFile.writeline("--------------------------------------------------------------")
set oTxtFile = nothing
elseif dtComp1 = "1/3/1601" then
z = z + 1
set oTxtFile = cFSo_Opentextfile("c:\ID-Stale-Objects\" & q2 & "Objects-NeverUsed.txt", ForWriting)
oTxtFile.writeline("-----------------" & q2 & z & " ------------------")
oTxtFile.writeline("Object Friendly Name: " & ADRecSet.fields("Name").value)
oTxtFile.writeline("Object Logon Name/ID (sAMAccountName): " & ADRecSet.fields("sAMAccountName").value)
oTxtFile.writeline("Object Distinguished Name: " & ADRecSet.fields("distinguishedName").value)
oTxtFile.writeline("Last Logon Timestamp: " & dtComp1)
oTxtFile.writeline("Current Date/Time: " & cTime)
oTxtFile.writeline("--------------------------------------------------------------")
set oTxtFile = nothing
elseif dtComp1 = "1/2/1601" then
z = z + 1
set oTxtFile = cFSo_Opentextfile("c:\ID-Stale-Objects\" & q2 & "Objects-NeverUsed.txt", ForWriting)
oTxtFile.writeline("-----------------" & q2 & z & " ------------------")
oTxtFile.writeline("Object Friendly Name: " & ADRecSet.fields("Name").value)
oTxtFile.writeline("Object Logon Name/ID (sAMAccountName): " & ADRecSet.fields("sAMAccountName").value)
oTxtFile.writeline("Object Distinguished Name: " & ADRecSet.fields("distinguishedName").value)
oTxtFile.writeline("Last Logon Timestamp: " & dtComp1)
oTxtFile.writeline("Current Date/Time: " & cTime)
oTxtFile.writeline("--------------------------------------------------------------")
set oTxtFile = nothing
elseif dtComp1 = "1/4/1601" then
z = z + 1
set oTxtFile = cFSo_Opentextfile("c:\ID-Stale-Objects\" & q2 & "Objects-NeverUsed.txt", ForWriting)
oTxtFile.writeline("-----------------" & q2 & z & " ------------------")
oTxtFile.writeline("Object Friendly Name: " & ADRecSet.fields("Name").value)
oTxtFile.writeline("Object Logon Name/ID (sAMAccountName): " & ADRecSet.fields("sAMAccountName").value)
oTxtFile.writeline("Object Distinguished Name: " & ADRecSet.fields("distinguishedName").value)
oTxtFile.writeline("Last Logon Timestamp: " & dtComp1)
oTxtFile.writeline("Current Date/Time: " & cTime)
oTxtFile.writeline("--------------------------------------------------------------")
set oTxtFile = nothing
elseif dtComp1 = "1/5/1601" then
z = z + 1
set oTxtFile = cFSo_Opentextfile("c:\ID-Stale-Objects\" & q2 & "Objects-NeverUsed.txt", ForWriting)
oTxtFile.writeline("-----------------" & q2 & z & " ------------------")
oTxtFile.writeline("Object Friendly Name: " & ADRecSet.fields("Name").value)
oTxtFile.writeline("Object Logon Name/ID (sAMAccountName): " & ADRecSet.fields("sAMAccountName").value)
oTxtFile.writeline("Object Distinguished Name: " & ADRecSet.fields("distinguishedName").value)
oTxtFile.writeline("Last Logon Timestamp: " & dtComp1)
oTxtFile.writeline("Current Date/Time: " & cTime)
oTxtFile.writeline("--------------------------------------------------------------")
set oTxtFile = nothing
elseif dtComp1 < dtComp2 then
y = y + 1
set oTxtFile = cFSo_Opentextfile("c:\ID-Stale-Objects\Stale" & q2 & "Objects.txt", ForWriting)
oTxtFile.writeline("-----------------" & " Stale User " & y & " ------------------")
oTxtFile.writeline("Object Friendly Name: " & ADRecSet.fields("Name").value)
oTxtFile.writeline("Object Logon Name/ID (sAMAccountName): " & ADRecSet.fields("sAMAccountName").value)
oTxtFile.writeline("Object Distinguished Name: " & ADRecSet.fields("distinguishedName").value)
oTxtFile.writeline("Last Logon Timestamp: " & dtComp1)
oTxtFile.writeline("Current Date/Time: " & cTime)
oTxtFile.writeline("--------------------------------------------------------------")
set oTxtFile = nothing
end if
set ADusr = nothing
set ADusrprop = nothing
ADRecSet.movenext
loop

'closing our resources

set oTxtFile = cFSo_Opentextfile("c:\ID-Stale-Objects\Stale" & q2 & "Objects.txt", ForWriting)
oTxtFile.writeline("--------------------------------------------------------------")
oTxtFile.writeline("--------------------------------------------------------------")
oTxtFile.writeline("--------------------------------------------------------------")
oTxtFile.writeline("--------------------------------------------------------------")
oTxtFile.writeline("Queried for stale " & q2 & " objects in the Distinguished Name " & q1 & "." & " Date: " & cTime)
oTxtFile.writeline("Queried Distinguished Name: " & q1)
oTxtFile.writeline("Search scope: Tree and all subtrees")
oTxtFile.writeline("Object search type: " & q2)
oTxtFile.writeline("Days back to query: " & q3)
oTxtFile.writeline("JOB COMPLETE at: " & Now)
oTxtFile.writeline("--------------------------------------------------------------")
oTxtFile.writeline("--------------------------------------------------------------")
set oTxtFile = nothing
oTxtFile.writeline("--------------------------------------------------------------")
oTxtFile.writeline("--------------------------------------------------------------")
oTxtFile.writeline("--------------------------------------------------------------")
oTxtFile.writeline("--------------------------------------------------------------")
set oTxtFile = cFSo_Opentextfile("c:\ID-Stale-Objects\" & q2 & "Objects-NeverUsed.txt", ForWriting)
oTxtFile.writeline("Querying for Unused " & q2 & " objects in the Distinguished Name " & q1 & "." & " Date: " & cTime)
oTxtFile.writeline("Querying Distinguished Name: " & q1)
oTxtFile.writeline("Search scope: Tree and all subtrees")
oTxtFile.writeline("Object search type: " & q2)
oTxtFile.writeline("Days back to query: " & q3)
oTxtFile.writeline("JOB COMPLETE at: " & Now)
oTxtFile.writeline("--------------------------------------------------------------")
oTxtFile.writeline("--------------------------------------------------------------")
set oTxtFile = nothing

set ADconn = nothing
set ADcmd = nothing
set ADRecSet = nothing

wscript.echo "The query is now complete. Please see ID-STALE-OBJECTS directory on the root of the C: drive"
wscript.quit

-Brandon Wilson
MCSE00/03, MCSA:Messaging00, MCSA03, A+
Sr. Infrastructure Management Analyst
Distributed Systems Engineering
ACS, Inc.
 
Ok. There are 2 fields in AD that deal with the logon attribute. The lastLogon attribute is stored per DC and is NOT replicated. It is the most accurate and up-to-date, but its not replicated EVER. The lastLogonTimestamp IS replicated, but it only replicates like once per 7 or 14 days (cannot remember the interval).

So the best way to find the most accurate information is to run the query on all DC's for lastLogon attribute and use the latest. Here is a script that I use that works well. I used a bunch of code that I found rlmueller.net and made some additions of my own. This script will find all DC's, then adjust for the time offset, and then get the most recent lastLogon attribute for every user. It takes a while to run, but it is as accurate as you can get in AD.

Code:
' LastLogon.vbs
' VBScript program to determine when each user in the domain last logged
' on.
'
' ----------------------------------------------------------------------
' Copyright (c) 2002 Richard L. Mueller
' Hilltop Lab web site - [URL unfurl="true"]http://www.rlmueller.net[/URL]
' Version 1.0 - December 7, 2002
' Version 1.1 - January 17, 2003 - Account for null value for lastLogon.
' Version 1.2 - January 23, 2003 - Account for DC not available.
' Version 1.3 - February 3, 2003 - Retrieve users but not contacts.
' Version 1.4 - February 19, 2003 - Standardize Hungarian notation.
' Version 1.5 - March 11, 2003 - Remove SearchScope property.
' Version 1.6 - May 9, 2003 - Account for error in IADsLargeInteger
'                             property methods HighPart and LowPart.
' Version 1.7 - January 25, 2004 - Modify error trapping.
'
' Because the lastLogon attribute is not replicated, every Domain
' Controller in the domain must be queried to find the latest lastLogon
' date for each user. The lastest date found is kept in a dictionary
' object. The program first uses ADO to search the domain for all Domain
' Controllers. The AdsPath of each Domain Controller is saved in an
' array. Then, for each Domain Controller, ADO is used to search the
' copy of Active Directory on that Domain Controller for all user
' objects and return the lastLogon attribute. The lastLogon attribute is
' a 64-bit number representing the number of 100 nanosecond intervals
' since 12:00 am January 1, 1601. This value is converted to a date. The
' last logon date is in UTC (Coordinated Univeral Time). It must be
' adjusted by the Time Zone bias in the machine registry to convert to
' local time.
'
' You have a royalty-free right to use, modify, reproduce, and
' distribute this script file in any way you find useful, provided that
' you agree that the copyright owner above has no warranty, obligations,
' or liability for such use.

'Many edits made to original script by djtech2k sportsfan@teamarsenal.net

Option Explicit

Dim objRootDSE, strConfig, objConnection, objCommand, strQuery
Dim objRecordSet, objDC
Dim strDNSDomain, objShell, lngBiasKey, lngBias, k, arrstrDCs()
Dim strDN, dtmDate, objDate, lngDate, objList, strUser
Dim strBase, strFilter, strAttributes, lngHigh, lngLow
Dim strSAM, strText

' Use a dictionary object to track latest lastLogon for each user.
Set objList = CreateObject("Scripting.Dictionary")
objList.CompareMode = vbTextCompare

' Obtain local Time Zone bias from machine registry.
Set objShell = CreateObject("Wscript.Shell")
lngBiasKey = objShell.RegRead("HKLM\System\CurrentControlSet\Control\" _
  & "TimeZoneInformation\ActiveTimeBias")
If UCase(TypeName(lngBiasKey)) = "LONG" Then
  lngBias = lngBiasKey
ElseIf UCase(TypeName(lngBiasKey)) = "VARIANT()" Then
  lngBias = 0
  For k = 0 To UBound(lngBiasKey)
    lngBias = lngBias + (lngBiasKey(k) * 256^k)
  Next
End If

' Determine configuration context and DNS domain from RootDSE object.
Set objRootDSE = GetObject("LDAP://RootDSE")
strConfig = objRootDSE.Get("configurationNamingContext")
strDNSDomain = objRootDSE.Get("defaultNamingContext")

' Use ADO to search Active Directory for ObjectClass nTDSDSA.
' This will identify all Domain Controllers.
Set objCommand = CreateObject("ADODB.Command")
Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
objCommand.ActiveConnection = objConnection

strBase = "<LDAP://" & strConfig & ">"
strFilter = "(objectClass=nTDSDSA)"
strAttributes = "AdsPath"
strQuery = strBase & ";" & strFilter & ";" & strAttributes & ";subtree"

objCommand.CommandText = strQuery
objCommand.Properties("Page Size") = 100
objCommand.Properties("Timeout") = 360
objCommand.Properties("Cache Results") = False

Set objRecordSet = objCommand.Execute

' Enumerate parent objects of class nTDSDSA. Save Domain Controller
' AdsPaths in dynamic array arrstrDCs.
k = 0
Do Until objRecordSet.EOF
  Set objDC = _
    GetObject(GetObject(objRecordSet.Fields("AdsPath")).Parent)
  ReDim Preserve arrstrDCs(k)
  arrstrDCs(k) = objDC.DNSHostName
  k = k + 1
  objRecordSet.MoveNext
Loop

' Retrieve lastLogon attribute for each user on each Domain Controller.
For k = 0 To Ubound(arrstrDCs)
  strBase = "<LDAP://" & arrstrDCs(k) & "/" & strDNSDomain & ">"
  strFilter = "(&(objectCategory=person)(objectClass=user))"
  strAttributes = "distinguishedName,sAMAccountName,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
    Wscript.Echo "Domain Controller not available: " & arrstrDCs(k)
  Else
    On Error GoTo 0
    Do Until objRecordSet.EOF
      strDN = objRecordSet.Fields("distinguishedName")
	  strSAM = objRecordSet.Fields("sAMAccountName")
      lngDate = objRecordSet.Fields("lastLogon")
      On Error Resume Next
      Set objDate = lngDate
      If Err.Number <> 0 Then
        On Error GoTo 0
        dtmDate = #1/1/1601#
      Else
        On Error GoTo 0
        lngHigh = objDate.HighPart
        lngLow = objDate.LowPart
        If lngLow < 0 Then
          lngHigh = lngHigh + 1
        End If
        If (lngHigh = 0) And (lngLow = 0 ) Then
          dtmDate = #1/1/1601#
        Else
          dtmDate = #1/1/1601# + (((lngHigh * (2 ^ 32)) _
            + lngLow)/600000000 - lngBias)/1440
        End If
      End If
	  strText = strDN & " ; " & strSAM
	    If objList.Exists(strText) Then
        If dtmDate > objList(strText) Then
          objList(strText) = dtmDate
        End If
      Else
        objList.Add strText, dtmDate
      End If
	    objRecordSet.MoveNext
    Loop
  End If
Next

' Output latest lastLogon date for each user.
For Each strUser In objList
  Wscript.Echo strUser & " ; " & objList(strUser)
Next

' Clean up.
objConnection.Close
Set objRootDSE = Nothing
Set objConnection = Nothing
Set objCommand = Nothing
Set objRecordSet = Nothing
Set objDC = Nothing
Set objDate = Nothing
Set objList = Nothing
Set objShell = Nothing
 
what I was planning was to run query from AD and delete those users from AD MMC but now it's getting complicated.
Let see if this is fixed in future version of AD
 
Ok, well the code I posted will report those users back to you. If you want that same code to delete users, then its an easy addition to the code. If you want to do it from ADUC, you can query for the attribute, but again it will only grab that value from the DC you are hitting. If done from ADUC, it will not show you the latest value considering all DC's in the mix.

As for your comment, nothing is broken. It is working how it was designed. We have provided you what you asked for so I do not know what is more complicated now.
 
since you are looking at 180 days since the last logon, querying lastlogontimestamp should be all you need to do

if you were looking at logons within the last 2 weeks, then lastlogon would be appropriate to use

-Brandon Wilson
MCSE00/03, MCSA:Messaging00, MCSA03, A+
Sr. Infrastructure Management Analyst
Distributed Systems Engineering
ACS, Inc.
 
I found many different answer.

ADgod says
lastlogontimestamp is replicated on all dc but is not stored in GC in my case all DC's are GC

djtech2k (MIS) says
There are 2 fields in AD that deal with the logon attribute. The lastLogon attribute is stored per DC and is NOT replicated. It is the most accurate and up-to-date, but its not replicated EVER. The lastLogonTimestamp IS replicated, but it only replicates like once per 7 or 14 days (cannot remember the interval).

if what djtech2k says is right than I can run query for lastlogontimstamp less than 180 days from any one of the DC and I should get accurate result and I would be able to delete them from AD MMC. But, according to ADGod I cannot do this as my all DC's are GC.

Any further help please.
 
you misunderstood me


if you are querying for "stale" objects over 2 weeks old, you need only query the lastLogonTimeStamp, if that timeframe is less than 2 weeks, the appropriate attribute would be lastLogon

so just use the script I provided and youll be good to go

lastLogonTimeStamp is all you need to query with looking for 180 days since logon

-Brandon Wilson
MCSE00/03, MCSA:Messaging00, MCSA03, A+
Sr. Infrastructure Management Analyst
Distributed Systems Engineering
ACS, Inc.
 
Well what about people that have not logged in in 180 days less 2 weeks? What I mean is since lastlogontimestamp does not get updated until every 2 weeks, then what happens if a person's 180 falls 1.5 weeks since the lastlogontimestamp was replicated?
 
that is a possibility

-Brandon Wilson
MCSE00/03, MCSA:Messaging00, MCSA03, A+
Sr. Infrastructure Management Analyst
Distributed Systems Engineering
ACS, Inc.
 
Status
Not open for further replies.

Part and Inventory Search

Sponsor

Back
Top