Public/Get-AACEntraGroupMembership.ps1
|
function Get-AACEntraGroupMembership { <# .EXTERNALHELP Azure.Admin.Console-help.xml .SYNOPSIS Who is in your Entra ID groups - direct members and everyone in the groups nested in them - flattened to one row per group and member, with a Spectre.Console view, objects, and CSV, PDF and interactive HTML reports. .DESCRIPTION Reads the groups and their members from Microsoft Graph, following nested groups to the end (a loop is followed once): -GroupName these groups, by exact display name -GroupNameStartsWith every group whose name starts with this neither every group in the tenant Both can be given together. It uses the Connect-AAC sign-in - no second prompt: a Microsoft Graph token is taken from it silently, as other APIs' tokens are. Your account needs to be allowed to read groups in Entra ID, which members of the tenant are by default. With an App Registration of your own (Connect-AAC -ClientId), give it Microsoft Graph's delegated Group.Read.All and User.Read.All permissions. Nothing is written to Entra ID. One row per member of each group (AAC.EntraGroupMember): GroupName, GroupType (Microsoft 365, Security, Mail-enabled security, Distribution - dynamic, role-assignable), GroupSource (Cloud, or synced from on-premises AD), MemberName, MemberType (User, Group, Device, Service principal, Contact), UserPrincipalName, Mail, UserType (Member or Guest), AccountEnabled, JobTitle, Department, Membership (Direct, Nested, or Empty for a group with no members), Via (the nested path, 'Group > Nested group'), Depth, GroupId and MemberId. The same rows go to CSV, HTML and PDF. What you get depends on where the command runs: at the prompt tiles, each group's type, source and counts, and each group's members as a tree (nested groups under their group) - a page at a time piped onward the rows, with no view -PassThru the view and the rows -NoDisplay the rows only -CsvPath writes the rows. -HtmlPath writes an interactive report: tiles and charts that filter the tables, a table of groups and one of every membership - searchable, filterable by group, member type, guest or member, direct or nested, and downloadable as CSV. -PdfPath writes a PDF: the summary, the groups, and each group's members. With any of them, the console shows only the progress and the files written. .PARAMETER GroupName The display names of the groups to report on (exact matches). Alias: GroupNames. .PARAMETER GroupNameStartsWith Report on every group whose display name starts with this. .PARAMETER CsvPath Write every row to this CSV file. Alias: OutputPath. .PARAMETER PdfPath Write a PDF report to this file. .PARAMETER HtmlPath Write an interactive HTML report to this file. .PARAMETER Title The PDF and HTML reports' title. .PARAMETER PassThru Show the view and also return the rows. .PARAMETER NoDisplay Return the rows without showing the view. .PARAMETER NoPaging Show the whole view at once instead of a page at a time. .EXAMPLE Get-AACEntraGroupMembership -GroupName 'grp-finance', 'grp-hr' Two groups, with everyone in them, direct or nested. .EXAMPLE Get-AACEntraGroupMembership -GroupNameStartsWith 'grp-azure-' -HtmlPath .\out\Groups.html -CsvPath .\out\Groups.csv Every group whose name starts with 'grp-azure-', as an interactive HTML report and a CSV file. .EXAMPLE Get-AACEntraGroupMembership -GroupNameStartsWith 'grp-' -NoDisplay | Where-Object { $_.UserType -eq 'Guest' } The guests in those groups, and through which group. .EXAMPLE Connect-AAC Get-AACEntraGroupMembership -PdfPath .\out\Groups.pdf Every group in the tenant you signed in to, as a PDF report. .OUTPUTS AAC.EntraGroupMember (piped onward, or with -PassThru or -NoDisplay) #> [CmdletBinding()] [OutputType('AAC.EntraGroupMember')] param( [Alias('GroupNames')] [ValidateNotNullOrEmpty()] [string[]] $GroupName, [ValidateNotNullOrEmpty()] [string] $GroupNameStartsWith, [Alias('OutputPath')] [string] $CsvPath, [string] $PdfPath, [string] $HtmlPath, [string] $Title = 'Entra ID group membership', [switch] $PassThru, [switch] $NoDisplay, [switch] $NoPaging ) # A failure anywhere below ends as a Spectre.Console error panel and this # command's own terminating error, not a line inside the module. A stopped # pipeline (Select-Object -First, Ctrl+C) is no failure: just return - a # rethrow would stop the caller's whole script, not only this command. trap { if ($_.Exception -is [System.Management.Automation.PipelineStoppedException]) { return }; $PSCmdlet.ThrowTerminatingError((Show-AACError -ErrorRecord $_ -Cmdlet $PSCmdlet)) } $pipedOnward = $MyInvocation.PipelinePosition -lt $MyInvocation.PipelineLength $interactive = -not $NoDisplay -and -not $pipedOnward $showView = $interactive -and -not ($CsvPath -or $PdfPath -or $HtmlPath) $returnObjects = $PassThru -or $NoDisplay -or $pipedOnward $resolve = { param([string] $Path) if ($Path) { $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) } } $csvFullPath = & $resolve $CsvPath $pdfFullPath = & $resolve $PdfPath $htmlFullPath = & $resolve $HtmlPath if ($interactive) { Write-AACRule -Title 'Azure Admin Console :: Entra ID group membership' -Color 'deepskyblue3_1' } # The Connect-AAC sign-in, for Microsoft Graph: fails here, clearly, when # there is none or it can't give a Graph token. $null = Get-AACAccessToken -Resource 'https://graph.microsoft.com' $state = Invoke-AACProgress -ScriptBlock { $read = Read-AACEntraGroup -GroupName @($GroupName | Where-Object { $_ }) -GroupNameStartsWith $GroupNameStartsWith foreach ($name in $read.Missing) { Write-Warning "No group named '$name' was found; it's left out." } if (-not $read.Groups.Count) { $asked = @(@($GroupName | Where-Object { $_ } | ForEach-Object { "'$_'" }) + @(if ($GroupNameStartsWith) { "starting with '$GroupNameStartsWith'" })) throw $(if ($asked.Count) { "No group $($asked -join ' or ') was found. Group names are matched exactly (-GroupName) or by their start (-GroupNameStartsWith)." } else { 'No groups were found in the tenant.' }) } Update-AACProgress -Id 'flatten' -Indeterminate -Description 'Flattening the memberships' $membership = ConvertTo-AACGroupMembership -Group $read.Groups -Member $read.Members -MemberError $read.MemberErrors $stats = $membership.Stats Update-AACProgress -Id 'flatten' -Complete -Description ('{0:N0} group(s): {1:N0} membership row(s), {2:N0} unique user(s) - {3:N0} guest(s), {4:N0} disabled' -f $stats.Groups, $stats.Rows, $stats.Users, $stats.Guests, $stats.Disabled) $scope = [ordered]@{} if ($script:AACSession) { $scope['Signed in as'] = [string]$script:AACSession.Account; $scope['Tenant'] = [string]$script:AACSession.TenantId } $scope['Groups'] = @( if ($GroupName) { "named $(($GroupName | ForEach-Object { "'$_'" }) -join ', ')" } if ($GroupNameStartsWith) { "starting with '$GroupNameStartsWith'" } ) -join '; ' if (-not $scope['Groups']) { $scope['Groups'] = 'every group in the tenant' } $null = Invoke-AACExport -CsvPath $csvFullPath -CsvObject @($membership.Rows) -Noun 'membership row' -PdfPath $pdfFullPath -WritePdf { Write-AACGroupMembershipPdf -Membership $membership -Path $pdfFullPath -Title $Title -Detail $scope } -HtmlPath $htmlFullPath -WriteHtml { Write-AACGroupMembershipHtml -Membership $membership -Path $htmlFullPath -Title $Title -Detail $scope } @{ Membership = $membership; Scope = $scope } } if ($showView) { Invoke-AACPagedOutput -NoPaging:$NoPaging -ScriptBlock { Show-AACGroupMembershipView -Membership $state.Membership -Scope $state.Scope } } if ($returnObjects) { $state.Membership.Rows } } |