Hi PowerShell folks,
today I’m going to show some perfomance tricks querying Active Dirctory using LDAPFilter and to handle them especially in bigger environments. >1k entries but up to much more. With this examples it is no problem querying a huge amount up to 100k entries or more.
Get-ADGroupMember of many groups
Imagine you want to get maybe the EmailAdress of all users who are member of one of some defined groups.
In this case you first need to get all groups and then get the members of the groups. The typically code for this situation is the following, as I’ve seen it in many scripts from a lot of different people:
Bad example
# Defining User Groups from somewhere in this case as an array of strings
$UserGroups = @("UserGroup1","UserGroup2","UserGroup3","UserGroup4")
# creating array to collect all groups
$AllADGroups = @()
# iterating through all usergorups
foreach($UserGroup in $UserGroups){
# and put it into array
$AllADGroups += Get-ADGroup $UserGroup
}
# creating array for all users
$AllUsers = @()
# now iterating through all groups
foreach($ADGroup in $ADGroups){
# get AD GroupMember of current group and put them into array
# at this point everyone realizes, that the Get-ADGroupMember CMDlet doesn't return a proper
# ADPrincipal object, so the most are doing the following to get the EmailAddress property
$AllUsers += $ADGroup | Get-ADGroupMember | Get-ADUser -Property EmailAddress
}
This is a really bad example with the following issues in it.
- The code above is sending many queries to the DomainController, at least two for every group and one for every user.
This doesn’t scale if you’re querying more groups or users. Especially if you need to get them recurse.
In this case you’re script is producing a lot of network traffic with large overhead for all the packets and waits for every answer before it can continue. You should go out for a walk or go drink some coffee with you’re collegues at work while your slow script is running. - This script uses arrays and the += operator to fill them. This produces a lot of memory consumtion because an array is of a fixed size n, so PowerShell just creates a new one with the size n+1 every time the += operand has been called. Imagine this with over 200 groups and 10K UserObjects. In this case it is better to use an ArrayList. *PowerShell 7.5 has fixed this
Good example using LDAPFilter
# Defining User Groups from somewhere in this case as an array of strings
$UserGroups = @("UserGroup1","UserGroup2","UserGroup3","UserGroup4")
# creating filter for groups
$filter = "`"Name -eq {0}`"" -f ($UserGroups -join " -or Name -eq ")
# creating array to collect all groups
$AllADGroups = Get-ADGroup -Filter $filter
# creating ldapFilter for memberOf one of the groups
# the basic ldapfilter will be an or claused memberOf query
# this will result in an ldapfilter like: "(|(memberOf=CN=UserGroup1...=COM)(memberOf=CN=UserGroup2...=COM)(memberOf=CN=UserGroup3...=COM))(memberOf=CN=UserGroup4...=COM)"
$ldapFilter = "(|(memberOf={0}))" -f ($AllADGroups.DistinguishedName -join ')(memberOf=')
# if you need to get all member recrusive, create the following ldapFilter
# but be carefull if you have too many groups you might get a timout
$ldapFilterNested = "(|(memberOf:1.2.840.113556.1.4.1941:={0}))" -f ($AllADGroups.DistinguishedName -join ')(memberOf:1.2.840.113556.1.4.1941:=')
# Get member of all groups containing all properties you need
$AllUsers = Get-ADUser -LDAPFilter $ldapFilter -Properties EmailAddress
# Get nested member of all groups containing all properties you need
$AllUsersNested = Get-ADUser -LDAPFilter $ldapFilterNested -Properties EmailAddress
In the example above you only query the DomainController two times. This is way much faster then sending one query for each group and each user.
If you’re in a multi domain environment, you should query the Global Catalog
Testing ComputerObjects from a different source against ActiveDirectory
In this example I’m refering to a post on reddit, where an admin tries to test all computers from inventory against the ActiveDirectory and get those which aren’t.
Therefore he imported 15k ComputerNames into an array and queried them in a foreach loop for every entry in this list.
In this case the author of the post produces 15k AD queries and puts those inside of a try catch block to collect those which aren’t member of the ActiveDirectoy.
So I’ve done this in an environment with a lot more than 15k entries with the following piece of code:
# Split-Array function from ChatGPT
function Split-Array {
param (
[Parameter(Mandatory = $true)]
[Array]$InputArray,
[int]$NumberOfChunks,
[int]$ChunkSize
)
if (-not $NumberOfChunks -and -not $ChunkSize) {
throw "You must specify either -NumberOfChunks or -ChunkSize."
}
if ($NumberOfChunks -and $ChunkSize) {
throw "Please specify only one: -NumberOfChunks or -ChunkSize, not both."
}
$result = @()
if ($NumberOfChunks) {
$chunkSize = [Math]::Ceiling($InputArray.Count / $NumberOfChunks)
}
else {
$chunkSize = $ChunkSize
}
for ($i = 0; $i -lt $InputArray.Count; $i += $chunkSize) {
$result += ,($InputArray[$i..([Math]::Min($i + $chunkSize - 1, $InputArray.Count - 1))])
}
return $result
}
# Importing Inventory data from csv in this case
$InventoryComputer = Import-Csv -Path C:\Path\ToInventory.csv -Encoding utf8 -Delimiter ";"
# In my environment I needed limit the requests to prevent timeouts from the domaincontroller, so I've splitted it to arrays with a chunkSize of 10k or something like this
$InventoryComputerParts = Split-Array -InputArray $InventoryComputer -ChunkSize 10000
# Create an ArrayList
$ADComputer = [System.Collections.ArrayList]@()
foreach ($Part in $InventoryComputerParts) {
# Building an LdapFilter while joining the array of ComputerName into a LdapFilter, using the filter as glue :)
$LdapFilter = "(|(name={0}))" -f ($Part.ComputerName -join ')(name=')
# Query the ADController
$ResultSet = Get-ADComputer -LdapFilter $LdapFilter
# check if there is a result
if ($ResultSet) {
# Adding the result, forced to be an array, to the ArrayList with AddRange while suppressing the output by typecasting it to void. (/¯◡ ‿ ◡)/¯ ~ ┻━┻
# Typecasting the $ResultSet into an array is nessessary to avoid errors if $ResultSet is one single object, as returned different by the Get-ADComputer cmdlet
[void]$ADComputer.AddRange([array]$ResultSet)
}
}
# Creating hashtables for fast queries :)
$ADComputerHashtable = $ADComputer | Group-Object -Property Name -AsHashTable
$InventoryComputerHashtable = $InventoryComputer | Group-Object -Property ComputerName -AsHashTable
# Identify inventory computers missing from Active Directory
$InventoryComputer.Where({ !$ADComputerHashtable[$_.ComputerName] })
# Identify Active Directory computers missing from inventory
$ADComputer.Where({ !$InventoryComputer[$_.ComputerName] })
Try to understand how ldapfilter work and think about it before querying big data from you Domain Controllers. It saves you a lot of time, especially while developing or troubleshooting. Since PowerShell 7.5 implemented a faster way of handling the += operator of arrays you don’t need ArrayLists anymore, but I would recommend everyone to use them for bigger data operations. It gives you a better understanding of the materia and you know what you’re code is doing.