Finding nested groups in large Active Directory groups can be a challenging task. Active Directory includes the cmdlet Get-ADGroupMember for finding group members, but it cannot be used to query groups with over 5000 members. The cmdlet also suffers from performance bottlenecks. I'd like to share with you a tool I built that solves both those problems.

The problem with nested groups ^

Active Directory supports the concept of "nesting" groups inside one another. For example, consider two groups: GroupA and GroupB. GroupB can be a member of GroupA. If I assign GroupA write permissions to Folder1, then the members of GroupB also have write access to Folder1. Nesting groups inside each other can be a powerful way to assign access dynamically. There is no limit to the amount of nesting in Active Directory. For example, this scenario is valid (but not recommended):

  • GroupE is a member of GroupD
  • GroupD is a member of GroupC
  • GroupC is a member of GroupB
  • GroupB is a member of GroupA

That scenario can be challenging to unwind and report on. A circular reference can occur if an administrator is not careful. This occurs when groups are nested inside each other, creating an endless loop.

  • GroupC is a member of GroupB
  • GroupB is a member of GroupA
  • GroupA is a member of GroupC

Get-ADGroupMember basics ^

Microsoft created the Get-ADGroupMember cmdlet to return lists of group members. For everyday tasks, it works as advertised. If I want to see the members of a group called ParentGroup05, the syntax is straightforward.

PS C:\> Get-ADGroupMember ParentGroup05 | select Name, objectclass

The results show that there are three group members and a nested group named NestedGroup05.

Get ADGroupMember syntax

Get ADGroupMember syntax

The cmdlet also supports recursive lookups, which return users from all nested groups. Here, we can see that it shows six users: three in the parent group and three in the nested group.

PS C:\> Get-ADGroupMember ParentGroup05 -recursive | select Name, objectclass
Get ADGroupMember recursive lookup

Get ADGroupMember recursive lookup

But if you look closely, we have a potential problem. The cmdlet so far has shown the number of users OR a list of users and groups. If we perform a recursive search, it returns the data in one format (a flat list of users without group membership info). If we perform a non-recursive search, it returns the data in another format (a list of parent group users and a list of nested groups).

  • What if I need to know how many users are in each nested group?
  • What if I need to know which members belong to which group?

Get-ADGroup ^

In the examples above, there were criteria that could be useful: the presence of nested groups, the number of users in a nested group, and the concept of recursion or finding groups within groups. Get-ADGroupMember has trouble returning this information. However, Get-ADGroup can return the information we're looking for.

You may wonder how it is possible that Get-ADGroup can return group member information. Remember that when looking at group information in Active Directory Users and Computers, you can see MemberOf information. This means that there is a relationship between the group and the group members that Active Directory tracks. If we look at the same information with PowerShell, we can see that Get-ADGroup knows about the group member information.

Viewing Get ADGroup properties

Viewing Get ADGroup properties

Why is this important? There are two important aspects here that make this significant. We said earlier that Get-ADGroupMember isn't returning the data we want, so we need to look elsewhere. Get-ADGroup offers an alternate way to get the data we are looking for, and we can use PowerShell to access the data. If it's available via PowerShell, then we should be able to grab the data and format it as we wish.

Get-NestedGroup ^

Get-ADGroup can access the required information, but to do so requires using LDAP filters. If you have any experience using LDAP filters, then you know that their syntax can be challenging for users to understand. We can access most of the information required using one line of code:

Get-ADGroup -LDAPFilter "(&(objectCategory=group)(memberof=$($ADGrp.DistinguishedName)))"

This line instructs Active Directory to return only the groups that are members of $ADGrp. I saved the output to a variable and then the rest of the information was available by grabbing various properties. From there, I built a function that allows me to pass in a group (or multiple groups) to query and then format the results as I wish.

Here's what the output of my function Get-NestedGroup looks like when I query a group called ParentGroup05. The group has one nested group inside it, which has three members.

Get-NestedGroup ParentGroup05
Get NestedGroup output results

Get NestedGroup output results

I designed the tool to look for nested group members two levels deep. That means that if we search for nested groups in ParentGroup01, then it will return any nested groups and then check those nested groups for nested groups.

Get-NestedGroup ParentGroup01 | Format-Table
Get NestedGroup Returning two levels of nested groups

Get NestedGroup Returning two levels of nested groups

The group ParentGroup01 was searched and returned two groups: NestedGroup01 and NestedGroup02.

The tool then checked those two groups and found one more nested group named LargeGroup3000. Each group that is found also displays the number of group members and some basic group information.

The Get-NestedGroup tool can be used with a Server parameter. This gives the ability to query results against a specific DC (or a dc in a different domain). If no server is input, the tool finds a DC in the same site that the query is run from. This guarantees that searches remain fast.

The tool can query multiple parent groups at a time like this:

Get-NestedGroup Parentgroup01, Parentgroup02

I prefer the Format-Table output for more complex lookups, but the default view can be useful.

One thing I haven't mentioned yet is how fast this tool is! It returns results in about 15 milliseconds. If a similar recursive group lookup is done with Get-ADGroupMember, the results take anywhere from 5 to 20 seconds to complete.

Get NestedGroup Querying two parent groups

Get NestedGroup Querying two parent groups

I built this tool to help me get around the limits of Get-ADGroupMember when working with large groups. I needed a solution to pull out the nested groups from parent groups and give me the relevant information about those nested groups. Keep an eye out for my sister tool for grabbing the nested user information from parent groups.

The latest version of this tool is always available in my PowerShell GitHub Repo.

Subscribe to 4sysops newsletter!

function Get-NestedGroup {

    <#
    .SYNOPSIS
        Gets a list of nested groups inside an Active Directory group

    .DESCRIPTION
        Gets a list of nested groups inside an Active Directory group using LDAPFilter. Checks for
        two levels of nested groups from the parent group.

    .PARAMETER Group
        The name of an Active Directory group

    .PARAMETER Server
        The name of Domain controller to use for query. Valid entries are a server name or servername:3268 for a
        Global Catalog query.

    .EXAMPLE
        PS C:\> get-nestedgroup "Server Admins"

        ParentGroup            : Server Admins
        NestedGroup            : NYC Server Admins
        NestedGroupMemberCount : 8
        ObjectClass            : group
        ObjectPath             : contoso.com/Groups/NYC Server Admins
        DistinguishedName      : CN=NYC Server Admins,OU=Groups,DC=contoso,DC=com

        Returns the nested groups that are inside the group named "Server Admins".

        NOTE: NestedGroupMemberCount is the number of objects (aka members) inside the nested group.
        In this example, "NYC Server Admins" contains 8 objects. This number IS NOT the number of nested groups
        inside NYC Server Admins.

    .EXAMPLE

        PS C:\> $selectprops = "ParentGroup","NestedGroup","NestedGroupMemberCount"
        PS C:\> Get-NestedGroup "Exchange Recipient Administrators" | Select-Object $selectprops | format-table

        ParentGroup                       NestedGroup                          NestedGroupMemberCount
        -----------                       -----------                          ----------------------
        Exchange Recipient Administrators Exchange Organization Administrators                      5
        Exchange Recipient Administrators Global Service Desk                                     117
        Exchange Recipient Administrators Mail Admins                                               1

        Returns the nested groups in a table format. Uses a variable to specify the parameters for Select-Object

    .EXAMPLE
        PS C:\> Get-NestGroup $NYCGrps | Format-Table

        There are no nested groups inside NYC-Desktops
        There are no nested groups inside NYC-Servers
        There are no nested groups inside NYC-Laptops
        There are no nested groups inside NYC-Admins
        There are no nested groups inside NYC-HelpDesk

        Checks the six groups saved in the variable $NYCGrps for nested groups. In this example, none of
        six groups have any nested groups.

    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    .NOTES
        AUTHOR:      Mike Kanakos
        VERSION:     1.0.4
        DateCreated: 2020-04-15
        DateUpdated: 2019-07-28
    #>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory = $True)]
        [String[]]$Group,

        [Parameter()]
        [String]$Server = (Get-ADReplicationsite | Get-ADDomainController -SiteName $_.name -Discover -ErrorAction SilentlyContinue).name
    )

    begin { }

    process {
        foreach ($item in $Group) {
            $ADGrp = Get-ADGroup -Identity $item -Server $Server
            $QueryResult = Get-ADGroup -LDAPFilter "(&(objectCategory=group)(memberof=$($ADGrp.DistinguishedName)))" -Properties canonicalname -Server $Server
            if ( $null -ne $QueryResult) {
                foreach ($grp in $QueryResult) {
                    $GrpLookup = Get-ADGroup -Identity "$($Grp.DistinguishedName)" -Properties Members, CanonicalName -Server $Server

                    $NestedGroupInfo = [PSCustomObject]@{
                        'ParentGroup'            = $item
                        'NestedGroup'            = $Grp.Name
                        'NestedGroupMemberCount' = $GrpLookup.Members.count
                        'ObjectClass'            = $Grp.ObjectClass
                        'ObjectPath'             = $GrpLookup.CanonicalName
                        'DistinguishedName'      = $GrpLookup.DistinguishedName
                    } #end PSCustomObject

                    $NestedGroupInfo
                } #end of foreach inside if statement
            }
            else {
                Write-Information "There are no nested groups inside $item" -InformationAction Continue
            } #end if/else

            # checking for groups of nested groups
            foreach ($NestedGrp in $QueryResult) {
                $NestedADGrp = Get-ADGroup -Identity $NestedGrp -Server $Server
                $NestedQueryResult = Get-ADGroup -LDAPFilter "(&(objectCategory=group)(memberof=$($NestedADGrp.DistinguishedName)))" -Properties canonicalname -Server $Server

                If ($null -ne $NestedQueryResult) {
                    foreach ($SubGrp in $NestedQueryResult) {
                        $SubGrpLookup = Get-ADGroup -Identity "$($SubGrp.DistinguishedName)" -Properties Members, CanonicalName -Server $Server
                    }

                    $SubNestedGroupInfo = [PSCustomObject]@{
                        'ParentGroup'            = $NestedADGrp.Name
                        'NestedGroup'            = $SubGrp.Name
                        'NestedGroupMemberCount' = $SubGrpLookup.Members.count
                        'ObjectClass'            = $SubGrp.ObjectClass
                        'ObjectPath'             = $SubGrpLookup.CanonicalName
                        'DistinguishedName'      = $SubGrpLookup.DistinguishedName
                    } #end PSCustomObject

                    $SubNestedGroupInfo
                }
            }
        } #end parent foreach
    } #end process block

    end {}
}#end function
+3
2 Comments
  1. PshMike 1 year ago

    I believe there may be a slight error in your code. I entered an issue in GitHub for you to look at if you like.

    0

    • Author

      Hi Mike, 

      I saw the github issue you opened. Thank you for reaching out. I am going to paste my response I left on github here as well. 
       

      so let's go through the question together.... for those unfamilar with the issue Mike raised. you can look here:
      https://github.com/compwiz32/PowerShell/issues/7
       

      I believe lines 115-124 should be inside of the foreach loop bound by lines 111-113

       

       If ($null -ne $NestedQueryResult) {
                          foreach ($SubGrp in $NestedQueryResult) {
                              $SubGrpLookup = Get-ADGroup -Identity "$($SubGrp.DistinguishedName)" -Properties Members, CanonicalName -Server $Server
                          }
      
                          $SubNestedGroupInfo = [PSCustomObject]@{
                              'ParentGroup'            = $NestedADGrp.Name
                              'NestedGroup'            = $SubGrp.Name
                              'NestedGroupMemberCount' = $SubGrpLookup.Members.count
                              'ObjectClass'            = $SubGrp.ObjectClass
                              'ObjectPath'             = $SubGrpLookup.CanonicalName
                              'DistinguishedName'      = $SubGrpLookup.DistinguishedName
                          } #end PSCustomObject
      
                          $SubNestedGroupInfo
                      }
      

      I am sorry the line numbers dont line up, but i copied lines 110-125 here....

      this IF loop is only used if there is a nested group found in the earlier lookup...

      • it says IF NOT NULL then continue...
      • then it says foreach group in $nestedqueryresult > do the $subgrouplookup
      • then we create an pscustomobject
      • then we return the object
        ... and then it goes back through the foreach if there are more nestedgroups in $nestedqueryresult

      if I move object inside the foreach, then the next time the loop runs, it will overwrite the first result (if there are more than one).

      PowerShell keeps track of the pscustomobject for me. When I come through the second time, it adds to the existing results on output. It took me a long time to understand that logic, but that's Powershell stepping in and doing the work for me ... the next run of the loop ADDS to $SubNestedGroupInfo - it doesn't overwrite it. 

      does that make sense?

      +4

Leave a reply

Please enclose code in pre tags

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

*

© 4sysops 2006 - 2021

CONTACT US

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

Sending

Log in with your credentials

or    

Forgot your details?

Create Account