Posts RSS Comments RSS 253 Posts and 411 Comments till now

Working with LDAP Stats Control in Powershell

What: The stats control is a LDAP control that you can pass that will tell the server to return its internal stats on a query.

Why: The stats control is a great way to see what the Domain Controller does with your filter. Like what indexes it hits, how many entries it had to visit, how much time the DC spent, and entries visited. It is very useful in creating the most efficient LDAP Query possible.

How: I Used System.DirectoryServices.Protocols.DirectoryControl to pass the LDAP control to the Server and I used System.DirectoryServices.Protocols.BERConverter along with the protocol spec here: LDAP_SERVER_GET_STATS_OID: 1.2.840.113556.1.4.970 to decode the Byte Array that was returned.

Here is what is Returned:
For 2000
threadCount: Number of threads that were processing LDAP requests on the DC at the time the search operation was performed.
coreTime: The time in milliseconds which core logic in the DC spent processing the request.
callTime: The overall time in milliseconds that the DC spent processing the request.
searchSubOperations: The number of individual operations which the DC performed in processing the request.

For 2003/2008
threadCount: Number of threads that were processing LDAP requests on the DC at the time the search operation was performed.
callTime: The overall time in milliseconds that the DC spent processing the request
entriesReturned: The number of objects returned in the search result.
entriesVisited: The number of objects that the DC considered for inclusion in the search result.
filter: String which represents the optimized form of the search filter used by the DC to perform a search. This very well may be different than the filter that was passed.
index: String which indicates which database indexes were used by the DC to perform the search.

For 2008 Only
pagesReferenced: The number of database pages referenced by the DC in processing the search.
pagesRead: The number of database pages read from disk.
pagesPreread: The number of database pages preread from disk by the DC in processing the search.
pagesDirtied: The number of clean database pages modified by the DC in processing the search.
pagesRedirtied: The number of previously modified database pages that were modified by the DC in processing the search.
logRecordCount: The number of database log records generated by the DC in processing the search.
logRecordBytes: The size in bytes of database log records generated by the DC in processing the search.

Note:
– Must have SE_DEBUG_PRIVILEGE
– I did NOT implement SO_EXTENDED_FMT flag.
– I did NOT test 2000.
– The functions that decodes Byte Array actually return objects, but for this test I just outputed the test to mimic ADFind.exe
– Special thanks to Robin Caron, joe Richards, and Dmitri Gavrilov for help with the decoding.
– Here is GREAT Doc on the Controls (and everything else AD) [MS-ADTS]: Active Directory Technical Specification

Code:

Param(
        $filter = "(objectclass=*)",
        $base,
        $Server,
        [int]$pageSize = 1000,
        [string[]]$props = @("1.1"),
        [switch]$StatsOnly,
        [switch]$Verbose
    )
function CreateStatsObject2008{
    Param($StatsArray)
    $DecodedArray = [System.DirectoryServices.Protocols.BerConverter]::Decode("{iiiiiiiiiaiaiiiiiiiiiiiiii}",$StatsArray) # Win2008
    $myStatsObject = New-Object System.Object
    $myStatsObject | Add-Member -Name "ThreadCount"     -Value $DecodedArray[1]  -MemberType "NoteProperty"
    $myStatsObject | Add-Member -Name "CallTime"        -Value $DecodedArray[3]  -MemberType "NoteProperty"
    $myStatsObject | Add-Member -Name "EntriesReturned" -Value $DecodedArray[5]  -MemberType "NoteProperty"
    $myStatsObject | Add-Member -Name "EntriesVisited"  -Value $DecodedArray[7]  -MemberType "NoteProperty"
    $myStatsObject | Add-Member -Name "Filter"          -Value $DecodedArray[9]  -MemberType "NoteProperty"
    $myStatsObject | Add-Member -Name "Index"           -Value $DecodedArray[11] -MemberType "NoteProperty"
    $myStatsObject | Add-Member -Name "PagesReferenced" -Value $DecodedArray[13] -MemberType "NoteProperty"
    $myStatsObject | Add-Member -Name "PagesRead"       -Value $DecodedArray[15] -MemberType "NoteProperty"
    $myStatsObject | Add-Member -Name "PagesPreread"    -Value $DecodedArray[17] -MemberType "NoteProperty"
    $myStatsObject | Add-Member -Name "PagesDirtied"    -Value $DecodedArray[19] -MemberType "NoteProperty"
    $myStatsObject | Add-Member -Name "PagesRedirtied"  -Value $DecodedArray[21] -MemberType "NoteProperty"
    $myStatsObject | Add-Member -Name "LogRecordCount"  -Value $DecodedArray[23] -MemberType "NoteProperty"
    $myStatsObject | Add-Member -Name "LogRecordBytes"  -Value $DecodedArray[25] -MemberType "NoteProperty"
    $myStatsObject
}
function CreateStatsObject2003{
    Param($StatsArray)
    $DecodedArray = [System.DirectoryServices.Protocols.BerConverter]::Decode("{iiiiiiiiiaia}",$StatsArray) # Win2003
    $myStatsObject = New-Object System.Object
    $myStatsObject | Add-Member -Name "ThreadCount"     -Value $DecodedArray[1]  -MemberType "NoteProperty"
    $myStatsObject | Add-Member -Name "CallTime"        -Value $DecodedArray[3]  -MemberType "NoteProperty"
    $myStatsObject | Add-Member -Name "EntriesReturned" -Value $DecodedArray[5]  -MemberType "NoteProperty"
    $myStatsObject | Add-Member -Name "EntriesVisited"  -Value $DecodedArray[7]  -MemberType "NoteProperty"
    $myStatsObject | Add-Member -Name "Filter"          -Value $DecodedArray[9]  -MemberType "NoteProperty"
    $myStatsObject | Add-Member -Name "Index"           -Value $DecodedArray[11] -MemberType "NoteProperty"
    $myStatsObject
}
function CreateStatsObject2000{
    Param($StatsArray)
    $DecodedArray = [System.DirectoryServices.Protocols.BerConverter]::Decode("{iiiiiiii}",$StatsArray) # Win2000
    $myStatsObject = New-Object System.Object
    $myStatsObject | Add-Member -Name "ThreadCount"          -Value $DecodedArray[1]  -MemberType "NoteProperty"
    $myStatsObject | Add-Member -Name "CoreTime"             -Value $DecodedArray[3]  -MemberType "NoteProperty"
    $myStatsObject | Add-Member -Name "CallTime"             -Value $DecodedArray[5]  -MemberType "NoteProperty"
    $myStatsObject | Add-Member -Name "searchSubOperations"  -Value $DecodedArray[7]  -MemberType "NoteProperty"
    $myStatsObject
}

if($Verbose){$VerbosePreference = "Continue"}

Write-Verbose " – Loading System.DirectoryServices.Protocols"
[VOID][System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.Protocols")
   
[int]$pageCount = 0
[int]$objcount = 0

if(!$Server)
{
    $rootDSE = [ADSI]"LDAP://rootDSE"
    $Server = $rootDSE.dnsHostName
    if(!$base){$base = $rootDSE.defaultNamingContext}
    switch ($rootDSE.domainControllerFunctionality)
    {
        0 {$expression = ‘CreateStatsObject2000 $stats’}
        2 {$expression = ‘CreateStatsObject2003 $stats’}
        3 {$expression = ‘CreateStatsObject2008 $stats’}
    }
}

Write-Verbose " – Using Server:  [$Server]"
Write-Verbose " – Using Base:    [$base]"
Write-Verbose " – Using Filter:  [$filter]"
Write-Verbose " – Page Size:     [$PageSize]"
Write-Verbose " – Returning:     [$props]"
Write-Verbose " – CSV:           [$csv]"
Write-Verbose " – NoHeaders:     [$noHeader]"
Write-Verbose " – Count:         [$Count]"
Write-Verbose " – StatsOnly:     [$StatsOnly]"
Write-Verbose " – Expression:    [$expression]"

Write-Verbose " – Creating LDAP connection Object"
$connection = New-Object System.DirectoryServices.Protocols.LdapConnection($Server)  
$Subtree = [System.DirectoryServices.Protocols.SearchScope]"Subtree"

Write-Verbose " + Creating SearchRequest Object"
$SearchRequest = New-Object System.DirectoryServices.Protocols.SearchRequest($base,$filter,$Subtree,$props)

Write-Verbose "   – Creating System.DirectoryServices.Protocols.PageResultRequestControl Object"
$PagedRequest  = New-Object System.DirectoryServices.Protocols.PageResultRequestControl($pageSize)

Write-Verbose "   – Creating System.DirectoryServices.Protocols.SearchOptionsControl Object"
$SearchOptions = New-Object System.DirectoryServices.Protocols.SearchOptionsControl([System.DirectoryServices.Protocols.SearchOption]::DomainScope)

Write-Verbose "   – Creating System.DirectoryServices.Protocols.DirectoryControl Control for OID: [1.2.840.113556.1.4.970]"
$oid = "1.2.840.113556.1.4.970"
$StatsControl = New-Object System.DirectoryServices.Protocols.DirectoryControl($oid,$null,$false,$true)

Write-Verbose "   – Adding Controls"
[void]$SearchRequest.Controls.add($pagedRequest)
[void]$SearchRequest.Controls.Add($searchOptions)
[void]$SearchRequest.Controls.Add($StatsControl)

$start = Get-Date
while ($True)
{
    # Increment the pageCount by 1
    $pageCount++

    # Cast the directory response into a SearchResponse object
    Write-Verbose " – Cast the directory response into a SearchResponse object"
    $searchResponse = $connection.SendRequest($searchRequest)

    # Display the retrieved page number and the number of directory entries in the retrieved page
    Write-Verbose (" – Page:{0} Contains {1} response entries" -f $pageCount,$searchResponse.entries.count)

    Write-Verbose " – Returning Stats for Page:$PageCount"
    $stats = $searchResponse.Controls[0].GetValue()
    $ResultStats = invoke-Expression $expression
    if($pageCount -eq 1)
    {
        $StatsFilter = $ResultStats.Filter
        $StatsIndex = $ResultStats.Index
        Write-Verbose "   + Setting Filter to [$StatsFilter]"
        Write-Verbose "   + Setting Index  to [$StatsIndex]"
    }
   
    # If Cookie Length is 0, there are no more pages to request"
    if ($searchResponse.Controls[1].Cookie.Length -eq 0)
    {
        if($count){$objcount}
        "`nStatistics"
        "================================="
        "Elapsed Time: {0} (ms)" -f ((Get-Date).Subtract($start).TotalMilliseconds)
        "Returned {0} entries of {1} visited – ({2})`n" -f $ResultStats.EntriesReturned,$ResultStats.EntriesVisited,($ResultStats.EntriesReturned/$ResultStats.EntriesVisited).ToString(‘p’)
        "Used Filter:"
        "- {0}`n" -f $StatsFilter
        "Used Indices:"
        "- {0}`n" -f $StatsIndex
        break
    }

    # Set the cookie of the pageRequest equal to the cookie of the pageResponse to request the next
    # page of data in the send request and cast the directory control into a PageResultResponseControl object
    Write-Verbose " – Setting Cookie on SearchResponse to the PageReQuest"
    $pagedRequest.Cookie = $searchResponse.Controls[1].Cookie
}

Comments are closed.