param( [Parameter(Mandatory=$true)] [string]$GroupId ) # GroupMemberships.ps1 -GroupId "# Developer ADM" # GroupMemberships.ps1 -GroupId "Domain Admins" # GroupMemberships.ps1 -GroupId "# Developer" # GroupMemberships.ps1 -GroupId "# Interne Automatisering Team-Assistent" # GroupMemberships.ps1 -GroupId "# Interne Automatisering" [string] $date = Get-Date -Format "yyyy-MM-dd HHmm" [string] $OutputPath = ".\$date ($GroupId) group members.csv" [string] $membershipOutputPath = ".\$date ($GroupId) group memberships - parent groups .csv" # Connect to Microsoft Graph if not already connected Write-Host "Connecting to Microsoft Graph..." Connect-MgGraph -Scopes "Group.Read.All", "GroupMember.Read.All" -NoWelcome # If GroupId is actually a group name, resolve it to the actual GroupId if ($GroupId -notmatch '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') { Write-Host "Resolving group name '$GroupId' to GroupId..." try { $group = Get-MgGroup -Filter "displayName eq '$GroupId'" -ErrorAction Stop if ($group) { if ($group.Count -gt 1) { Write-Warning "Multiple groups found with name '$GroupId'. Using the first one." $GroupId = $group[0].Id } else { $GroupId = $group.Id } Write-Host "Resolved to GroupId: $GroupId" } else { Write-Error "Group with name '$GroupId' not found." exit 1 } } catch { Write-Error "Error resolving group name: $($_.Exception.Message)" exit 1 } } # Function to get groups that this group is a member of (reverse membership) function Get-GroupMembershipRecursive { param( [string]$GroupId, [int]$Level = 0, [hashtable]$ProcessedMemberships = @{} ) # Check for circular reference in membership chain if ($ProcessedMemberships.ContainsKey($GroupId)) { Write-Warning "Circular membership reference detected for group: $GroupId" return @() } # Check for maximum depth if ($Level -gt 50) { Write-Warning "Maximum membership recursion depth reached for group: $GroupId" return @() } # Mark group as being processed for membership $ProcessedMemberships[$GroupId] = $true $membershipResults = @() try { # Get groups that this group is a member of $memberOfGroups = Get-MgGroupMemberOf -GroupId $GroupId -All foreach ($parentGroup in $memberOfGroups) { if ($parentGroup.AdditionalProperties['@odata.type'] -eq '#microsoft.graph.group') { $parentGroupDetails = Get-MgGroup -GroupId $parentGroup.Id -Select "DisplayName,Mail,GroupTypes" $membershipObject = [PSCustomObject]@{ ChildGroupId = $GroupId ParentGroupId = $parentGroup.Id ParentGroupName = $parentGroupDetails.DisplayName ParentGroupType = $parentGroupDetails.GroupTypes -join "," ParentGroupEmail = $parentGroupDetails.Mail MembershipLevel = $Level } $membershipResults += $membershipObject # Recursively get parent groups of this parent group if ($parentGroup.Id -ne $GroupId) { $nestedMemberships = Get-GroupMembershipRecursive -GroupId $parentGroup.Id -Level ($Level + 1) -ProcessedMemberships $ProcessedMemberships $membershipResults += $nestedMemberships } } } } catch { Write-Error "Error getting group memberships for $GroupId`: $($_.Exception.Message)" } finally { # Remove from processed memberships when done $ProcessedMemberships.Remove($GroupId) } return $membershipResults } # Initialize collections $groupMembers = @() $processedGroups = @{} $groupStack = @() # Function to get group members recursively # This function will handle circular references and maximum recursion depth function Get-GroupMembersRecursive { param( [string]$GroupId, [int]$Level = 0, [string]$ParentGroupName = "" ) # Check for circular reference if ($processedGroups.ContainsKey($GroupId)) { Write-Warning "Circular reference detected for group: $GroupId" return } # Check for stack overflow (max depth) if ($Level -gt 50) { Write-Warning "Maximum recursion depth reached for group: $GroupId" return } # Mark group as being processed $processedGroups[$GroupId] = $true $groupStack += $GroupId try { # Get the group information $group = Get-MgGroup -GroupId $GroupId -ErrorAction Stop # Get group members $members = Get-MgGroupMember -GroupId $GroupId -All foreach ($member in $members) { # Create custom object for the result $memberObject = [PSCustomObject]@{ ParentGroupId = $GroupId ParentGroupName = $group.DisplayName ParentGroupType = $group.GroupTypes -join "," MemberId = $member.Id MemberType = $member.AdditionalProperties['@odata.type'] -replace '#microsoft.graph.', '' Level = $Level Path = ($groupStack -join " -> ") + " -> " + $member.Id } # Get member details based on type switch ($member.AdditionalProperties['@odata.type']) { '#microsoft.graph.user' { $user = Get-MgUser -UserId $member.Id -Select "DisplayName,UserPrincipalName,Mail" $memberObject | Add-Member -NotePropertyName "MemberName" -NotePropertyValue $user.DisplayName $memberObject | Add-Member -NotePropertyName "MemberUPN" -NotePropertyValue $user.UserPrincipalName $memberObject | Add-Member -NotePropertyName "MemberEmail" -NotePropertyValue $user.Mail } '#microsoft.graph.group' { $memberGroup = Get-MgGroup -GroupId $member.Id -Select "DisplayName,Mail,GroupTypes" $memberObject | Add-Member -NotePropertyName "MemberName" -NotePropertyValue $memberGroup.DisplayName $memberObject | Add-Member -NotePropertyName "MemberUPN" -NotePropertyValue "" $memberObject | Add-Member -NotePropertyName "MemberEmail" -NotePropertyValue $memberGroup.Mail $memberObject | Add-Member -NotePropertyName "MemberGroupTypes" -NotePropertyValue ($memberGroup.GroupTypes -join ",") } default { $memberObject | Add-Member -NotePropertyName "MemberName" -NotePropertyValue "Unknown" $memberObject | Add-Member -NotePropertyName "MemberUPN" -NotePropertyValue "" $memberObject | Add-Member -NotePropertyName "MemberEmail" -NotePropertyValue "" } } $script:groupMembers += $memberObject # If member is a group, recurse into it if ($member.AdditionalProperties['@odata.type'] -eq '#microsoft.graph.group') { # Check if this group is already in the current path to prevent immediate loops if ($member.Id -notin $groupStack) { Get-GroupMembersRecursive -GroupId $member.Id -Level ($Level + 1) -ParentGroupName $group.DisplayName } else { Write-Warning "Immediate circular reference detected. Skipping group: $($memberGroup.DisplayName)" } } } } catch { Write-Error "Error processing group $GroupId`: $($_.Exception.Message)" } finally { # Remove from stack and processed groups when done $groupStack = $groupStack[0..($groupStack.Length-2)] $processedGroups.Remove($GroupId) } } # Start the recursive process Write-Host "Starting recursive group membership scan for group: $GroupId" Get-GroupMembersRecursive -GroupId $GroupId # Export results to CSV if ($groupMembers.Count -gt 0) { $groupMembers | Export-Csv -Path $OutputPath -NoTypeInformation Write-Host "Results exported to: $OutputPath" Write-Host "Total members found: $($groupMembers.Count)" } else { Write-Host "No members found in the specified group." } # Get group memberships (groups this group belongs to) Write-Host "Getting group memberships for group: $GroupId" $groupMemberships = Get-GroupMembershipRecursive -GroupId $GroupId # Export group memberships to separate CSV if any found if ($groupMemberships.Count -gt 0) { $groupMemberships | Export-Csv -Path $membershipOutputPath -NoTypeInformation Write-Host "Group memberships exported to: $membershipOutputPath" Write-Host "Total parent groups found: $($groupMemberships.Count)" } else { Write-Host "No group memberships found for the specified group." }