Solved

Enumerate AD Group levels (no users)

Posted on 2011-02-16
23
651 Views
Last Modified: 2012-05-11
My goal is to figure out the group nested levels in my environment; I don't care about users for this exercise. Here's what I'm looking for:

GroupA
     GroupAa
          GroupAaa
     GroupAb
GroupB
     GroupBa
     GroupBb
GroupC
     GroupCa
     GroupCb
     GroupCc
This could be in an output table format such as:
Level 0     Level1        Level2   ....
GroupA    GroupAa    GroupAaa ....
                  GroupAb
GroupB    GroupBa
                  GroupBb
GroupC   GroupCa
                 GroupCb
                 GroupCc
I'm aware of the Quest Cmlet "Get-QADGroupMember" with the -Indirect parameter, but this doesnt provide the hierarchy that I'm looking for. I've found many techniques to find out users in nested groups, but not anything that can help me accomplish what I'm trying to do. I'm including some code that I found on EE....look at example C...that's the closest code that I've come across but it uses users instead of groups...I'm hoping someone can help me rewrite it to get what I'm looking for. Thanks.

#Ensure Activeroles snap-in is loaded
add-pssnapin quest.activeroles.admanagement -ea SilentlyContinue

#Parent group for all commands below
$rootgroup = "Group1"

#Example A
Write-Host "Return all user accounts in a group, regardless of level of nesting:"
$users = get-qadgroupmember $rootgroup -indirect -type User
$users | Format-Table Name -auto 
Write-Host

#Example B
Write-Host "Return all group accounts in a group, regardless of level of nesting:"
$groups1 = get-qadgroupmember $rootgroup -indirect -type Group
$groups1 | Format-Table Name -auto
Write-Host

#Example C
Write-Host "Return all user accounts in a group (as above), and specify which nested groups they're a member of:"
$groups2 = get-qadgroupmember $rootgroup -indirect -type group
$users2 = get-qadgroupmember $rootgroup -indirect -type user

foreach ($user in $users2) {
    $parentgroups = @()
    $memberships = $user.MemberOf
    foreach ($membership in $memberships) {
        foreach ($othergroup in $groups2) {
            If ($membership -eq $othergroup.DN) {
                $parentgroups += $othergroup
            }
        }
    }
$user | Add-Member -membertype "Noteproperty" -Name "ParentGroups" -Value $parentgroups
}
$users2 | Sort-Object ParentGroups | Format-Table Name, ParentGroups -auto

#If running script in a powershell console window, pause when finished
If (!($host.name -match "ISE")) {Write-Host "Press and key to continue...";$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown");Write-Host ""}

Open in new window

0
Comment
Question by:bndit
  • 12
  • 11
23 Comments
 
LVL 70

Expert Comment

by:Chris Dent
ID: 34909736

You'll need to write your own recursion.

I can't test this, I'm afraid you might have to help me with that part until tomorrow. But...
Function Get-GroupMember {
  Param(
    [String]$Identity,
    [Int32]$i = 0
  )

  "$(' ' * $i * 2)$($_)"

  $i++
  Get-QADGroupMember $DN -Type Group | ForEach-Object {
    Get-GroupMember $_.SamAccountName $i
  }
}

Get-GroupMember "Group1"

Open in new window

It should, if it works as I intend, show you something of a tree-view for each group in the hierarchy. It won't show the headers, adding those is even harder :)

Chris
0
 
LVL 2

Author Comment

by:bndit
ID: 34909869
It's failing on the -Identity parameter...any ideas?
error.jpg
0
 
LVL 70

Expert Comment

by:Chris Dent
ID: 34909927
Sorry, changed stuff half-way through.
Function Get-GroupMember {
  Param(
    [String]$Identity,
    [Int32]$i = 0
  )

  "$(' ' * $i * 2)$($_)"

  $i++
  Get-QADGroupMember $Identity -Type Group | ForEach-Object {
    Get-GroupMember $_.SamAccountName $i
  }
}

Get-GroupMember "Group1"

Open in new window

Chris
0
 
LVL 2

Author Comment

by:bndit
ID: 34909983
Thanks Chris...the script works now, but goes into endless loops. I'm not so much interested in the "tree" view of the groups as much as I'm interested in the "table" output...thanks for trying though.
0
 
LVL 70

Expert Comment

by:Chris Dent
ID: 34910060

Hmm cyclic membership? I didn't check for that because it's quite rare. However, it can only loop if it feeds the original group name back in further down the hierarchy. Do the group names repeat?

The table view is hard because of this:

<Group1>
      | -- <Group 2>
      |           | -- <Group 4>
      |
      | -- <Group 3>
                  | -- <Group 5>
                  | -- <Group 6>

How are you expecting to that appear in a table given that each column in the table may have more than one entry for each group?

That is, it shows you the groups within the group, but does not show you hierarchy really.

Chris
0
 
LVL 70

Expert Comment

by:Chris Dent
ID: 34914189
Okay, round two, easier now I'm at work :)

This one is a little more complex.

$Identity: When you call Get-GroupMember it defines the starting point, within the function it represents the current group.
$Depth: A counter, used to track depth
$GroupHierarchy: Created outside of the function, used to store data across all calls to the function
$LoopPrevention: A hash table, it stores group names and is here to prevent infinite loops

I'm sure the output will need work, at the moment I just want to see the recursive function work properly.
Function Get-GroupMember {
  Param(
    [Parameter(Mandatory = $True)]
    [String]$Identity,
    [Int32]$Depth = 1,
    [Parameter(Mandatory = $True)]
    [Object]$GroupHierarchy,
    [HashTable]$LoopPrevention = @{}
  )

  If (!$LoopPrevention.Contains($Identity)) {
    $LoopPrevention.Add($Identity, "")
  
    If (!($GroupHierarchy.PsObject.Properties | Where-Object { $_.Name -eq "Level$Depth" })) {
      $GroupHierarchy | Add-Member NoteProperty "Level$Depth" @($Identity)
    } Else {
      $GroupHierarchy."Level$Depth" += $Identity
    }
    $Depth++

    Get-QADGroupMember $Identity -Type Group | ForEach-Object {
      Get-GroupMember $_.SamAccountName $Depth $GroupHierarchy $LoopPrevention
    }
  }
}

# This just be created outside of the recursive function, but we could make a second function as a wrapper to hide this
$GroupHierarchy = New-Object Object
# Call the recursive function
Get-GroupMember "Some Base Group" -GroupHierarchy $GroupHierarchy
# Return the table
$GroupHierarchy

Open in new window

Chris
0
 
LVL 2

Author Comment

by:bndit
ID: 34919455
Chris,

I tested the script...and it works. Here's what I had in my test lab:

<All-DLs>
      | -- <DL-Alpha>
      |           | -- <Group 0>
      |           |            | -- <Group A>
      |           |            |            | -- <Group B>
      |           |            |                         | -- <Group C>
      |           | -- <Group 1>    
      |           | -- <Group 2>    
      |           | -- <Group 3>    
      |           | -- <Group 4>    
      |                                      
      | -- <DL-Bravo>                
                  | -- <Group 5>      
                  |            | -- <Group D>     |
                  |            |            | -- <Group E>
                  |            |            |            | -- <Group F>
                  | -- <Group 6>
                  | -- <Group 7>
                  | -- <Group 8>
                  | -- <Group 9>

Anyway, I did test nesting a group from a higher level a second time in of the lower levels and the output basically omits that group. I guess in my production environment if I do have that type of cyclical nesting situation going on I won't know..but that's fine. Also, you're right the output conveys the information, but it's not something that I can use.... .CSV would be ideal. Lastly, I think that now that the script is working I could turn it into an actual search instead of running it on a single group...like this:

1. Get all the groups in my domain and store them into an array.
2. Pass that array to this function and let the function iterate thru each group...

If I have approx. 350-400..do you think I'll have issues running the script the way it is or should it be able to handle that number of groups? Also, do you see a benefit in first determining top-level groups (groups that dont have a value for the 'MemberOf' property)...store those groups in an array and feed that array to the function? That was my logic yesterday but didn't get far.
output.png
0
 
LVL 70

Expert Comment

by:Chris Dent
ID: 34919568

> output basically omits that group

We could, potentially, have it output the group name again? Or it could add a "Status" member flagging that the chain contains a loop? Is either useful?

I think there's value in finding all top level groups, it feels better to limit the set that it must run for, this is a pretty heavy operation at the end of the day.

It would go something like this:
Get-QADGroup -LdapFilter "(!(memberOf=*))" | ForEach-Object {
  $GroupHierarchy = New-Object Object
  Get-GroupMember $_.SamAccountName -GroupHierarchy $GroupHierarchy
  $GroupHierarchy
}

Open in new window

So far so good, our last challenge is that every hierarchy we return must have the same number of members (Level*) if we're to call Export-Csv. Still, lets see how that plays for you first?

Chris
0
 
LVL 2

Author Comment

by:bndit
ID: 34919742
Ok, that last bit of code allows me to control the search...so I can deal with it. As for the flagging, I think it'd be VERY useful in my case to flag that there's a chain in the loop..and point out which group(s) it is. Not sure if I followed you on the returned hierarchy having the same number of members...I'm guessing that's gonna be a problem since some groups will have different nested levels.
0
 
LVL 70

Expert Comment

by:Chris Dent
ID: 34920275
Right, this has the same caveat as last night, I'm at home with no testing environment so fingers crossed and all that :)

Anyway, in theory it includes everything we've discussed. The hairy bit at the end sorts out members so we can pipe off to Export-Csv.
Function Get-GroupMember {
  Param(
    [Parameter(Mandatory = $True)]
    [String]$Identity,
    [Int32]$Depth = 1,
    [Parameter(Mandatory = $True)]
    [Object]$GroupHierarchy,
    [HashTable]$LoopPrevention = @{}
  )

  If (!$LoopPrevention.Contains($Identity)) {
    $LoopPrevention.Add($Identity, "")
  
    If (!($GroupHierarchy.PsObject.Properties | Where-Object { $_.Name -eq "Level$Depth" })) {
      $GroupHierarchy | Add-Member NoteProperty "Level$Depth" @($Identity)
    } Else {
      $GroupHierarchy."Level$Depth" += $Identity
    }
    $Depth++

    Get-QADGroupMember $Identity -Type Group | ForEach-Object {
      Get-GroupMember $_.SamAccountName $Depth $GroupHierarchy $LoopPrevention
    }
  } Else {
    # Track loops and how deep in the chain they occurred (we don't have the parent group at this stage)

    $GroupHierarchy.LoopPrevention += "$Identity repeated at level $Depth"

  }
}

$Results = Get-QADGroup -LdapFilter "(!(memberOf=*))" | ForEach-Object {
  $GroupHierarchy = New-Object Object
  $GroupHierarchy | Add-Member NoteProperty LoopPrevention @()
  Get-GroupMember $_.SamAccountName -GroupHierarchy $GroupHierarchy
  $GroupHierarchy
}

# Give all objects in the result set the name members

$Properties = @{}
$Results | ForEach-Object {
  $_.PsObject.Properties | ForEach-Object { 
    If (!$Properties.Contains($_.Name)) { $Properties.Add($_.Name, "") } 
  }
}
$PropertyList = $Properties.Keys | Sort-Object

$Results = $Results | ForEach-Object {
  ForEach ($Property in $PropertyList) {
    If (!($_ | Get-Member $Property)) {
      $_ | Add-Member NoteProperty $Property ""
    }
  }
  $_
}

# Finally, write it all to a CSV

$Results | Export-Csv "Out.csv" -NoTypeInformation

Open in new window

Chris
0
 
LVL 2

Author Comment

by:bndit
ID: 34920886
Output was this:

"LoopPrevention","Level1","Level2","Level3","Level4","Level5","Level6"
"System.Object[]","System.Object[]","System.Object[]","","","",""
"System.Object[]","System.Object[]","System.Object[]","","","",""
"System.Object[]","System.Object[]","System.Object[]","","","",""
"System.Object[]","System.Object[]","","","","",""
"System.Object[]","System.Object[]","","","","",""
"System.Object[]","System.Object[]","","","","",""
"System.Object[]","System.Object[]","","","","",""
"System.Object[]","System.Object[]","","","","",""
"System.Object[]","System.Object[]","","","","",""
0
How to run any project with ease

Manage projects of all sizes how you want. Great for personal to-do lists, project milestones, team priorities and launch plans.
- Combine task lists, docs, spreadsheets, and chat in one
- View and edit from mobile/offline
- Cut down on emails

 
LVL 70

Expert Comment

by:Chris Dent
ID: 34921766
Almost there, I'll patch that up in the morning. Don't let me forget :)

Chris
0
 
LVL 2

Author Comment

by:bndit
ID: 34921812
Thanks Chris.
0
 
LVL 70

Accepted Solution

by:
Chris Dent earned 500 total points
ID: 34924542
Little changes this time. I've had it write fields like this:

Level1   Level2
Group   SubGroup1
            SubGroup2

Lets see how that works out :)
Function Get-GroupMember {
  Param(
    [Parameter(Mandatory = $True)]
    [String]$Identity,
    [Int32]$Depth = 1,
    [Parameter(Mandatory = $True)]
    [Object]$GroupHierarchy,
    [HashTable]$LoopPrevention = @{}
  )

  If (!$LoopPrevention.Contains($Identity)) {
    $LoopPrevention.Add($Identity, "")
  
    If (!($GroupHierarchy.PsObject.Properties | Where-Object { $_.Name -eq "Level$Depth" })) {
      $GroupHierarchy | Add-Member NoteProperty "Level$Depth" $Identity
    } Else {
      $GroupHierarchy."Level$Depth" = $GroupHierarchy."Level$Depth" + "`n$Identity"
    }
    $Depth++

    Get-QADGroupMember $Identity -Type Group | ForEach-Object {
      Get-GroupMember $_.SamAccountName $Depth $GroupHierarchy $LoopPrevention
    }
  } Else {
    # Track loops and how deep in the chain they occurred (we don't have the parent group at this stage)

    $GroupHierarchy.LoopPrevention = $GroupHierarchy.LoopPrevention + "`n$Identity repeated at level $Depth"

  }
}

$Results = Get-QADGroup -LdapFilter "(!(memberOf=*))" | ForEach-Object {
  $GroupHierarchy = New-Object Object
  $GroupHierarchy | Add-Member NoteProperty LoopPrevention ""
  Get-GroupMember $_.SamAccountName -GroupHierarchy $GroupHierarchy
  $GroupHierarchy
}

# Give all objects in the result set the name members

$Properties = @{}
$Results | ForEach-Object {
  $_.PsObject.Properties | ForEach-Object { 
    If (!$Properties.Contains($_.Name)) { $Properties.Add($_.Name, "") } 
  }
}
$PropertyList = $Properties.Keys | Sort-Object

$Results = $Results | ForEach-Object {
  ForEach ($Property in $PropertyList) {
    If (!($_ | Get-Member $Property)) {
      $_ | Add-Member NoteProperty $Property ""
    }
  }
  $_
}

# Finally, write it all to a CSV

$Results | Select-Object Level*, LoopPrevention | Export-Csv "Out.csv" -NoTypeInformation

Open in new window

Chris
0
 
LVL 2

Author Comment

by:bndit
ID: 34934461
Chris...something is missing...not sure if it's in the loop that walks through the group or the variable that writes the output..see attachment
output1.png
0
 
LVL 70

Expert Comment

by:Chris Dent
ID: 34934466
Make the row in Excel bigger, it should have added it beneath, but since it writes to CSV it can't auto-size row height or anything like that :)

Chris
0
 
LVL 2

Author Comment

by:bndit
ID: 34934585
Ahhhhh.....Ok, that works. I can see the missing info...So I went ahead and moved the script to production...and I'm only targeting a single OU with 3 DLs with group members (users and groups)...however, I'm only getting 1 group in the output (I expanded the rows). Here's the only change that I made since instead of "sweeping" my entire domain, I can focus direct the search to the specific OU...any ideas?

output2.png
0
 
LVL 70

Expert Comment

by:Chris Dent
ID: 34934600

Hmm the SearchRoot only applies to the base group, so that's good.

Does this bit only return a single group?
Get-QADGroup -SearchRoot 'OU=Test OU,DC=domain,DC=local' -LdapFilter "(!(memberOf=*))"

Open in new window

If it does, you might consider dropping the "memberOf" filter?

Chris
0
 
LVL 2

Author Comment

by:bndit
ID: 34934757
Single group...
0
 
LVL 70

Expert Comment

by:Chris Dent
ID: 34934761
Drop the LDAP filter?

Chris
0
 
LVL 2

Author Comment

by:bndit
ID: 34934782
Seems to work without the ldapfilter...I'll test later tonight. I'll let you know.
0
 
LVL 2

Author Comment

by:bndit
ID: 35024970
Sorry Chris, didn't mean to leave you hanging....got super busy with other fires. Your script helped me a lot...thanks for your help as always!!
0
 
LVL 2

Author Closing Comment

by:bndit
ID: 35024973
Thx
0

Featured Post

What Security Threats Are You Missing?

Enhance your security with threat intelligence from the web. Get trending threat insights on hackers, exploits, and suspicious IP addresses delivered to your inbox with our free Cyber Daily.

Join & Write a Comment

Microsoft Windows Server Update Service (WSUS) is free for everyone, but it lacks of some desirable features like send an e-mail to the administrator with the status of all computers on the WSUS server. This article is based on my PowerShell script …
How to sign a powershell script so you can prevent tampering, and only allow users to run authorised Powershell scripts
Learn the basics of strings in Python: declaration, operations, indices, and slicing. Strings are declared with quotations; for example: s = "string": Strings are immutable.: Strings may be concatenated or multiplied using the addition and multiplic…
The viewer will learn how to count occurrences of each item in an array.

759 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

23 Experts available now in Live!

Get 1:1 Help Now