PowerShell Problem Solver: Enumerating Members of Large Active Directory Groups

In a recent PowerShell Problem Solver article I demonstrated how you can use PowerShell to enumerate members of an Active Directory group. I believe that for most of you the code samples in that article will suffice. But as I pointed out at the end of the article, there is a potential issue with very large groups.

A large group will be anything with more than 5000 members. It is still possible to enumerate these groups, but it will take a few extra steps. We will still be using the Active Directory cmdlets, but we will need to process the group members individually, especially if you want to recurse through any nested groups. I’m going to demonstrate with a group called Test Group 1 that I know has more than 5000 members, including a few nested groups.

Get-adgroup 'Test Group 1' –properties Member | select –expandproperty Member | measure-object
Enumerating members of large Active Directory groups in Windows PowerShell. (Image Credits: Jeff Hicks)
Enumerating members of large Active Directory groups in Windows PowerShell. (Image Credits: Jeff Hicks)

The problem is that I can’t use Get-ADGroupmember.

Error with the Get-ADGroupmember cmdlet in Windows PowerShell. (Image Credit: Jeff Hicks)
Error with the Get-ADGroupmember cmdlet in Windows PowerShell. (Image Credit: Jeff Hicks)

So I’ll have to take matters into my own hands with a recursive function.

Function Get-MyLargeGroup {
[cmdletbinding()]
Param(
[Parameter(Position=0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
[ValidateNotNullorEmpty()]
[string]$Name)
Begin {
    Write-Verbose "Starting $($MyInvocation.MyCommand)"
} #begin
Process {
Write-Verbose "Retrieving members from $Name"
$mygroup = Get-ADGroup -Identity $Name -Properties Members
foreach ($member in $mygroup.members) {
  $object = $member | Get-ADObject -Properties samaccountname
  if ($object.ObjectClass -eq 'Group') {
    Write-Verbose "Found nested group $($object.distinguishedname)"
    #recursively run this command for the nested group
    & $MyInvocation.MyCommand -name $object.Name
  }
  else {
   Select-Object -InputObject $object -property ObjectClass,Name,SamAccountname,DistinguishedName
  }
} #foreach
} #process
End {
    Write-Verbose "Ending $($MyInvocation.MyCommand)"
} #end
} #end function

The function takes the name of a group and retrieves the group along with its Members property. This will be a collection of distinguishednames for ever member. It might be a user, a computer or another group. I can use Get-ADObject to retrieve the member from Active Directory.

$object = $member | Get-ADObject -Properties samaccountname

All members of a group will have a samaccountname property, so I’ll grab that as well. From here I can check the object class to determine if it is a user, computer or another group. If it is a group then I can recursively call the function and enumerate the nested group. Otherwise, I’ll select some properties from the user or computer object.

  if ($object.ObjectClass -eq 'Group') {
    Write-Verbose "Found nested group $($object.distinguishedname)"
    #recursively run this command for the nested group
    & $MyInvocation.MyCommand -name $object.Name
  }
  else {
   Select-Object -InputObject $object -property ObjectClass,Name,SamAccountname,DistinguishedName
  }

I called the function Get-MyLargeGroup, but you can name it anything you want. To use, simply specify the name of a group.

Using the Get-MyLargeGroup function in Windows PowerShell. (Image Credit: Jeff Hicks)
Using the Get-MyLargeGroup function in Windows PowerShell. (Image Credit: Jeff Hicks)

I have saved the results to a variable $r2. From the verbose output, you can see that it found nested groups and enumerated those as well. How many members did I find?

The function found 5021 members. (Image Credit: Jeff Hicks)
The function found 5021 members. (Image Credit: Jeff Hicks)

However, there is a potential wrinkle. It is possible that a user account might be a member or more than one group inside this group.

For example, a user could be a direct member of Test Group 1 and a member of the nested group, Project Black. Let’s filter those duplicates out. I know for a fact there are duplicates in this group.

​>$r3 = $r2 | Select-Object * -Unique
New result number with duplicates filtered out. (Image Credit: Jeff Hicks)
New result number with duplicates filtered out. (Image Credit: Jeff Hicks)

Not a lot, but a few. Here are the final results.

$r3 | Out-Gridview -Title "Test Group 1 Members"
Final results of our test group. (Image Credits: Jeff Hicks)
Final results of our test group. (Image Credits: Jeff Hicks)

The intent from my previous post was to export results to a CSV file, which I can still accomplish:

$r3 | export-csv c:\work\Test-Group-1.csv -NoTypeInformation

I can use this function actually for any size group and come up with a one line command to export group members to a CSV file.

Get-MyLargeGroup -Name "Chicago Engineering" | Select * -unique | sort name | export-csv c:\work\engineering.csv -NoTypeInformation

There won’t be any Groups in the output because they were enumerated. But there could be computer accounts and perhaps you only really care about user accounts. Here’s an example of what I’m talking about.
120314 1555 PowerShellP8
To filter them out I could simply pipe to Where object.

Filtering using the where object in Windows PowerShell. (Image Credit: Jeff Hicks)
Filtering using the where object in Windows PowerShell. (Image Credit: Jeff Hicks)

Or for the sake of simplicity, I could modify my function. Here’s a variation that limits the output to user accounts. And because you know you are querying for user accounts, you can specify additional properties that only apply to users.

Function Get-MyLargeGroup2 {
[cmdletbinding()]
Param(
[Parameter(Position=0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
[ValidateNotNullorEmpty()]
[string]$Name,
[string[]]$Properties = @("SamAccountname","Title","Description","Department")
)
Begin {
    Write-Verbose "Starting $($MyInvocation.MyCommand)"
    #add default properties
    $Properties += "ObjectClass"
    $Properties += "Name"
    $Properties += "DistinguishedName"
    Write-verbose "retrieving these user properties"
    Write-Verbose ( $Properties -join "," | Out-String)
} #begin
Process {
Write-Verbose "Retrieving members from $Name"
$mygroup = Get-ADGroup -Identity $Name -Properties Members
foreach ($member in $mygroup.members) {
  $object = $member | Get-ADObject -Properties $properties
  if ($object.ObjectClass -eq 'Group') {
    Write-Verbose "Found nested group $($object.distinguishedname)"
    #recursively run this command for the nested group
    & $MyInvocation.MyCommand -name $object.Name
  }
  elseif ($object.objectclass -eq 'User') {
    Select-Object -InputObject $object -property $Properties
  }
} #foreach
} #process
End {
    Write-Verbose "Ending $($MyInvocation.MyCommand)"
} #end
} #end function

This version includes a parameter to specify additional user properties. In the Begin scriptblock, I add some standard properties.

Now, the only output from the function is if the object is a user.

elseif ($object.objectclass -eq 'User') {
    Select-Object -InputObject $object -property $Properties
  }

Again, I can use this for any size group.

Get-mylargegroup2 "Project Black" | out-gridview –title "Project Black"

120314 1555 PowerShellP10
Now you should have several ways to enumerate members of an Active Directory group. No matter how you enumerate the group, exporting the results to a CSV or XML file is all the same, That’s the great thing about PowerShell: learn the basics once and apply it everywhere.