I wrote a function a while back that is used to query a local group on a remote or local system (or systems) and based on the –Depth parameter, will perform a recursive query for all members of that group to include local and domain groups and users. I felt that it was something worth sharing out just in case someone has a need for it. It also isn’t the same script that I wrote back in January here: https://learn-powershell.net/2013/01/22/find-and-report-members-of-a-local-group/

To avoid an issue with circular groups (probably not the technical name for it), I use a hash table to manage the local/domain groups to ensure that they are not queried again later on. By circular groups, I am talking about groups that are members of a parent group that may have the parent group listed as a member later on down the group membership. A more understandable example is here:

Because Administrators exist in Group3 and Administrators has Group3 as a member, this madness will never stop or will stop when it hits some limit on depth. Regardless, this would be a very annoying thing to have happen.

[cmdletbinding()] Param ( [parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)] [Alias('CN','__Server','Computer','IPAddress')] [string[]]$Computername = $env:COMPUTERNAME, [parameter()] [string]$Group = "Administrators", [parameter()] [int]$Depth = ([int]::MaxValue) )

Nothing really new here. I am setting up my parameters for Computername, a Group name (which defaults to’Administrators and the Depth limit. You will notice that I am using [int]::MaxValue which is 2147483647 which is my ”unlimited” recursion. If someone has a nested group that far down, then my hats off to you!

#region Extra Configurations Write-Verbose ("Depth: {0}" -f $Depth) #endregion Extra Configurations #region Helper Functions Function Get-NetBIOSDomain { Try { $Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() $Root = $Domain.GetDirectoryEntry() $Base = ($Root.distinguishedName) # Use the NameTranslate object. $Script:Translate = New-Object -comObject "NameTranslate" $Script:objNT = $Translate.GetType() # Initialize NameTranslate by locating the Global Catalog. $objNT.InvokeMember("Init", "InvokeMethod", $Null, $Translate, (3, $Null)) # Retrieve NetBIOS name of the current domain. $objNT.InvokeMember("Set", "InvokeMethod", $Null, $Translate, (1, "$Base")) $objNT.InvokeMember("Get", "InvokeMethod", $Null, $Translate, 3) } Catch {Write-Warning ("{0}" -f $_.Exception.Message)} }

This piece of code starts out with some verbose output (if using –Verbose) that will show the parameters being used as well as what the current depth that will be allowed for the nested groups.

The Get-NetBIOSDomain function is used only to help get the distinguishedName of a domain group in the Get-DomainGroupMember function (shown later in this article).

Function Get-LocalGroupMember { [cmdletbinding()] Param ( [parameter()] [System.DirectoryServices.DirectoryEntry]$LocalGroup ) $Counter++ # Invoke the Members method and convert to an array of member objects. $Members= @($LocalGroup.psbase.Invoke("Members")) ForEach ($Member In $Members) { Try { $Name = $Member.GetType().InvokeMember("Name", 'GetProperty', $Null, $Member, $Null) $Path = $Member.GetType().InvokeMember("ADsPath", 'GetProperty', $Null, $Member, $Null) # Check if this member is a group. $isGroup = ($Member.GetType().InvokeMember("Class", 'GetProperty', $Null, $Member, $Null) -eq "group") If (($Path -like "*/$Computer/*")) { $Type = 'Local' } Else {$Type = 'Domain'} New-Object PSObject -Property @{ Computername = $Computer Name = $Name Type = $Type ParentGroup = $LocalGroup.Name[0] isGroup = $isGroup Depth = $Counter } If ($isGroup) { Write-Verbose ("{0} is a group" -f $Name) # Check if this group is local or domain. Write-Verbose ("Checking if Counter: {0} is less than Depth: {1}" -f $Counter, $Depth) If ($Counter -lt $Depth) { If ($Type -eq 'Local') { If ($Groups[$Name] -notcontains 'Local') { Write-Verbose ("{0}: Getting local group members" -f $Name) $Groups[$Name] += ,'Local' # Enumerate members of local group. Get-LocalGroupMember $Member } } Else { If ($Groups[$Name] -notcontains 'Domain') { Write-Verbose ("{0}: Getting domain group members" -f $Name) $Groups[$Name] += ,'Domain' # Enumerate members of domain group. Get-DomainGroupMember $Member $Name $True } } } } } Catch { Write-Warning ("{0}" -f $_.Exception.Message) } } }

Get-LocalGroupMember is my first helper function when it comes to getting members of a group. In this case, I am gathering the members of a local group. A number of possibilities exist here such as whether the member is actually a User, or if it is another group that is either local on the system or a domain group. Based on the group type, it will either call itself again for the local group or call the Get-DomainGroupMember function which will be explained next. You can tell from my $Groups variable that it is the hash table used to make sure that the group hasn’t already queried as well as the counter which helps to determine the depth level of the current group and its members.

Function Get-DomainGroupMember { [cmdletbinding()] Param ( [parameter()] $DomainGroup, [parameter()] [string]$NTName, [parameter()] [string]$blnNT ) $Counter++ If ($blnNT -eq $True) { # Convert NetBIOS domain name of group to Distinguished Name. $objNT.InvokeMember("Set", "InvokeMethod", $Null, $Translate, (3, ("{0}{1}" -f $NetBIOSDomain.Trim(),$NTName))) $DN = $objNT.InvokeMember("Get", "InvokeMethod", $Null, $Translate, 1) $ADGroup = [ADSI]"LDAP://$DN" } Else { $DN = $DomainGroup.distinguishedName $ADGroup = $DomainGroup } ForEach ($MemberDN In $ADGroup.Member) { $MemberGroup = [ADSI]("LDAP://{0}" -f ($MemberDN -replace '/','\/')) New-Object PSObject -Property @{ Computername = $Computer Name = $MemberGroup.name[0] Type = 'Domain' ParentGroup = $NTName isGroup = ($MemberGroup.Class -eq "group") Depth = $Counter } # Check if this member is a group. If ($MemberGroup.Class -eq "group") { Write-Verbose ("{0} is a group" -f $MemberGroup.name[0]) Write-Verbose ("Checking if Counter: {0} is less than Depth: {1}" -f $Counter, $Depth) If ($Counter -lt $Depth) { If ($Groups[$MemberGroup.name[0]] -notcontains 'Domain') { Write-Verbose ("{0}: Getting domain group members" -f $MemberGroup.name[0]) $Groups[$MemberGroup.name[0]] += ,'Domain' # Enumerate members of domain group. Get-DomainGroupMember $MemberGroup $MemberGroup.Name[0] $False } } } } }

The Get-DomainGroupMember is my second helper function used to get group members. As the name implies, this will gather the group memberships that have been queried. the NetBIOSDomain name is also used here to find out the actual distinguishedName of the group so I can be used with the [ADSI] accelerator to make the query for group members. As with my Get-LocalGroupMember function, this makes use of the same hash table and counter to handle circular groups and recursion depth.

Process { #region Get Local Group Members ForEach ($Computer in $Computername) { $Script:Groups = @{} $Script:Counter=0 # Bind to the group object with the WinNT provider. $ADSIGroup = [ADSI]"WinNT://$Computer/$Group,group" Write-Verbose ("Checking {0} membership for {1}" -f $Group,$Computer) $Groups[$Group] += ,'Local' Get-LocalGroupMember -LocalGroup $ADSIGroup } #endregion Get Local Group Members }

Here is where everything happens for the queries. Everything that exists in the Process block is here for a reason so I am not needlessly making the same variable creation for each computer passed through the pipeline. Each computer will have a fresh counter and new hash table to handle recursion depth and circular groups, respectfully. Because the first group queried is the one that we defined in the Group parameter, it is already known to be a local group and Get-LocalGroupMember is called first.

I also have some runspace stuff in the code as well, but being how I have already talked about this in other articles, I figured it wasn’t worth mentioning again. If you really want to see the code behind the runspaces, check out those articles here.

Ok, enough talk about the code! It is time to see Get-LocalGroupMembership in action.

Get-LocalGroupMembership | Format-Table –AutoSize

You will notice that the groups are only nested 2 levels deep. Also important to notice is that I have some circular groups here with Sysops Admins under Enterprise Admins. Sysops Admins which is also listed under Administrators in which Sysops Admins has… Enterprise Admins listed as a member and thus the circle of member begins! Or at least it would have begun had the code not caught it.

I do not have other remote systems to use –Computername but trust me in that it does work against remote systems.

Get-LocalGroupMembership -Depth 1 | Format-Table –AutoSize

This is an example of setting the Depth parameter to only go 1 level deep.

That is all for this function. Feel free to download it from the Script Repository (link below) and let me know what you think!

Download

Technet Script Repository