Read nested Active Directory groups in PowerShell

A few pitfalls exist if you try to read the members of nested Active Directory groups with the PowerShell cmdlet Get-ADGroupMember and the -recursive parameter.

You don't have to work with Active Directory groups for very long before you can see how they become complicated, sprawling messes, especially once you start managing nested groups. When we say "nested group" we are referring to the Active Directory groups in your organization that have groups embedded within them.

It doesn't have to stop there, you know. You can have groups within groups within groups and so on. Because of this, using simple one-liners on your groups to find their true membership details may not be enough. We have to dig deeper to find out who actually belongs in which group.

Setting up Active Directory ^

I'm sure if you're tuning in to read about Active Directory concepts, you already have some experience with Active Directory and have the tools installed on your machine. In case you don't, you can follow this handy guide for the full instructions. But at a high level, you'll need the following:

  • Remote Server Administration Tools—specifically, you'll need the Active Directory Domain Services (AD DS) and AD Lightweight Directory Services (LDS) tools installed.
  • The Active Directory module, which you can import with a simple Import-Module ActiveDirectory.

To simplify our demo, I'm also going to set up three Active Directory groups. Our first group will be called Top and will be the parent of all the other groups. Our next group will be called Middle, and our last group will be called Bottom. To make our group memberships look as nested as possible, all the members of Bottom will be in Middle, and all the members of Middle will be in Top. I'll also sprinkle a few other members that aren't nested into each group.

Top group properties

Top group properties

Middle group properties

Middle group properties

Bottom group properties

Bottom group properties

Get members of a group ^

We can look at members of a group by using the Get-ADGroupMember cmdlet. This cmdlet is useful for a couple of reasons. If we wanted to query each group individually, we could simply perform the following query and retrieve all the users in a single group. In my case, I'll query my Top group to see what all we get back:

As we can see, PowerShell responds back with all of the members of our Top group, which includes our Top User and the Middle group.

Get members of child groups ^

We can also get a recursive result using the Get-ADGroupMember cmdlet. By simply adding the -recursive flag on the cmdlet, PowerShell will enumerate each group and return a full result with all of their members. From here you could do any of your regular filtering and formatting to return precisely what you want.

Recursive result

Recursive result

There are only a few pitfalls to using the -recursive flag, but depending on your script, they may be pretty detrimental. One secret I haven't let on yet is that our user Second User is actually disabled. What if we were supposed to be scripting against only enabled users? Disabled users? Also, what if we were supposed to preserve the group names in our script? I'll show you how to do each of these below.

Returning only enabled or disabled users

What if, instead of all users available, we needed only the disabled or enabled users? We can filter our commands a few ways to do this. The simplest and easiest, however, is to use the pipe to create a list of the appropriate users. For finding the enabled users, we can modify our command as follows:

The command above would return all of our users, with the exception of our user02 disabled user. Similarly, for disabled users, you can simply switch the filter for the Enabled property to $false:

Returning all group names

It's useful to have the parent group name of the members we are querying. If we want to keep these names returned with our object, we have to do a bit of custom scripting to accomplish this. In the script below, we'll not only grab the enabled users, but then we'll perform some additional filtering to return information about the user on a single line.

From this result, you could filter further to see all the groups in common. This is especially useful during account cleanups and audits.

Summary ^

As you can see, the Get-ADGroupMember cmdlet is a highly useful one. We covered how to query an individual group's members and how to get the nested members of all groups. The fun doesn't end here though, since you could easily take the output of these commands and throw them into your next one. You could modify these users, put them in a CSV, or even create your own custom object and allow manipulating them later. The choice is yours with PowerShell!

Join the 4sysops PowerShell group!

Your question was not answered? Ask in the forum!

  1. Jim Lovejoy 2 years ago

    PS C:\WINDOWS\system32> Get-ADGroupMember -Identity top -Recursive | `
    Get-ADUser -Filter {Enabled -eq $true} | ForEach-Object {
    New-Object PSObject -Property @{
    UserName = $_.DisplayName
    AccountName = $_.SamAccountName
    Groups = ($_.memberof | Get-ADGroup | (Select-Object -ExpandProperty Name) -join ","}
    } | Select-Object UserName,AccountName,Groups

    At line:6 char:47
    + ... berof | Get-ADGroup | (Select-Object -ExpandProperty Name) -join ","}
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Expressions are only allowed as the first element of a pipeline.
    At line:6 char:93
    + ... berof | Get-ADGroup | (Select-Object -ExpandProperty Name) -join ","}
    + ~
    Missing closing ')' in expression.
    + CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : ExpressionsMustBeFirstInPipeline

    • Author

      You are getting this error because there are multiple groups returned from the "Get-ADGroup" cmdlet on line 6.  You can fix this by placing a "ForEach-Object" after the pipe, so line 6 will end up looking like this:

       Groups = ($_.memberof | Get-ADGroup | ForEach-Object (Select-Object -ExpandProperty Name) -join ",")

      Note: I also made the final character a close parenthesis ")" rather than a bracket "}".  I think this might have been an error in uploading the article to the site, sorry about that!


  2. Alex Ø. T. Hansen 2 years ago

    The -recursive method doesn't always work.
    I have created a function that does basically the same.

    • Author

      Very nice!  Would you mind sharing your function, or is it published on GitHub or other version control system?

      In my experience, the -recursive method hasn't failed.  Could you describe an instance where it had?  I'd like to be able to account for failures, but I wasn't aware that this was an iffy parameter.



  3. Guilherme Rusth 2 years ago

    for my script i just need to know the name of the nested group members and if they are enable or disable can you help me ?


  4. Tim 1 year ago

    Hi Bryce,

    could you expand your article for the case of "copying all groups with their members(groups and user) to another organisation unit"

    So the result is a clone of a organisation unit with all its groups, subgroups and users.

    Best regards,


  5. Simon 2 months ago

    I get the following error after adding the ForEach-Object -

    ForEach-Object : Cannot bind parameter 'RemainingScripts'. Cannot convert the "-join" value of type "System.String" to 
    type "System.Management.Automation.ScriptBlock".
    At line:6 char:47
    + ... t-ADGroup | ForEach-Object (Select-Object -ExpandProperty Name) -join ...
    +                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : InvalidArgument: (:) [ForEach-Object], ParameterBindingException
        + FullyQualifiedErrorId : CannotConvertArgumentNoMessage,Microsoft.PowerShell.Commands.ForEachObjectCommand


  6. ezdoesIT 4 weeks ago

    First of all, thank you to the author for providing this code, there are other posts out their, but I like how clean this code is. It is definitely more abstract, but it is easy to turn it into a function given its succinct structure.

    I could not get the author's code to work as shown below:

    I removed the opening parentheses after "...Get-ADGroup |...", below is that working code for me:

    Also, I didn't want to filter out enabled or disabled users, because we are auditing our environment, so I grabbed all users and their "Enabled" status. I like to stay true to AD attribute names, so in the hashtable or New-Object, I updated it to reflect attribute names found in AD. Lastly, I sorted all accounts based on "sAMAccountNames", since our SIEM is used to validate what we find in AD, see below for my code:

    I converted the double-quotes for the comma used as the delimiter/separator for groups to single-quotes. Double-quotes are only needed when you need to interpolate a variable and don't want the literal string of a variable vs. its value.

    So, for me, this still does not fully meet my requirements, because I pull groups from more than one domain, so I am currently building that out as well; although, it greatly helped me jump start my function! If you are in the same scenario as me, you will see red error messages, when PowerShell cannot gain access or query those groups in the list from another domain.

    Hopefully, this helps someone else!


  7. ezDoesIT 4 weeks ago

    I made a mistake still in my last post, I failed to pass the "-Properties memberOf" parameter for the "Get-ADuser CMDLET; otherwise, the loop will not have any data populated for "memberOf" user property and it will not build the "Groups" list for each user, see updated code below:

    I hope that clears things up!


Leave a reply

Your email address will not be published. Required fields are marked *


© 4sysops 2006 - 2020


Please ask IT administration questions in the forums. Any other messages are welcome.


Log in with your credentials


Forgot your details?

Create Account