Public/Export-SiSDistributionList.ps1
|
function Export-SiSDistributionList { <# .SYNOPSIS Exports all distribution lists in Entra ID to Excel, as the starting point for reviewing names/descriptions with HR before creating access packages from the file. .DESCRIPTION Read only - nothing is changed in Entra. Gets every distribution list (mail-enabled, not security-enabled, not a Microsoft 365 group) and, via Graph $batch, its member count and owners. Mail-enabled security groups and dynamic distribution lists are not included (dynamic ones don't exist in Entra/Graph at all). Columns written: From Entra : ObjectId, DisplayName, PrimarySmtpAddress, OnPremisesSyncEnabled, MemberCount, Owners Fill in : Include (Yes/No dropdown), AccessPackageDisplayName, AccessPackageDescription, ScopingNotes Written by Sync-SiSAccessPackage : AccessPackageId, Status, StatusMessage .PARAMETER OutputPath Folder for the Excel file. Without it, a folder dialog opens at the start of the run. .PARAMETER BatchSize Requests per Graph $batch call. Default and max 20. .EXAMPLE Export-SiSDistributionList .EXAMPLE # No folder dialog Export-SiSDistributionList -OutputPath "C:\Reports\DistributionLists" .OUTPUTS SiSGovernance.DistributionListRow, one per distribution list - the same rows and columns as in the Excel file. .NOTES Required scopes: GroupMember.Read.All (read only - no Entitlement Management scope) Owners in Graph don't always match ManagedBy in Exchange for distribution lists - verify against Exchange before relying on it. #> [OutputType('SiSGovernance.DistributionListRow')] [CmdletBinding()] param( [Parameter(Mandatory = $false)] [string]$OutputPath, [Parameter(Mandatory = $false)] [ValidateRange(1, 20)] [int]$BatchSize = 20 ) Write-Host "==========================================" -ForegroundColor DarkGray Write-Host " EXPORT DISTRIBUTION LISTS " -ForegroundColor Cyan Write-Host "==========================================" -ForegroundColor DarkGray # --- OUTPUT FOLDER (first, so the run never stops halfway to ask) ------- $folderPath = Resolve-OutputFolder -OutputPath $OutputPath if (-not $folderPath) { return } # Read only: no Entitlement Management scope at all. GroupMember.Read.All # covers listing groups, members and owners - narrower than Group.Read.All, # which also reads M365 group content (conversations, calendar...). if (-not (Confirm-GraphConnection -Scopes 'GroupMember.Read.All')) { return } Write-Host "`n--- DISTRIBUTION LISTS ---" -ForegroundColor Cyan # Distribution lists only: mail-enabled, not security-enabled, not M365 groups. # NOT groupTypes/any(...) is an advanced query -> needs eventual + $count. $uri = "https://graph.microsoft.com/v1.0/groups?`$filter=mailEnabled eq true and securityEnabled eq false and NOT groupTypes/any(c:c eq 'Unified')&`$select=id,displayName,mail,onPremisesSyncEnabled&`$count=true" try { $groups = @(Get-GraphPagedResult -Uri $uri -Eventual) } catch { Write-Error -Message "Failed to read distribution lists. $($_.Exception.Message)" -Category ReadError return } if ($groups.Count -eq 0) { Write-Warning "No distribution lists found." return } Write-Host "Found $($groups.Count) distribution list(s)." -ForegroundColor Green $syncedCount = @($groups | Where-Object { $_.onPremisesSyncEnabled -eq $true }).Count if ($syncedCount -gt 0) { Write-Host " $syncedCount of them are synced from on-premises - Exchange Online can't manage their membership." -ForegroundColor Yellow } Write-Host "`n--- MEMBER COUNT AND OWNERS ---" -ForegroundColor Cyan $requests = foreach ($g in $groups) { @{ CorrelationKey = @{ Id = $g.id; Kind = 'Count' } Method = 'GET' Url = "/groups/$($g.id)/members?`$count=true&`$top=1&`$select=id" Headers = @{ ConsistencyLevel = 'eventual' } } @{ CorrelationKey = @{ Id = $g.id; Kind = 'Owners' } Method = 'GET' Url = "/groups/$($g.id)/owners?`$select=displayName,userPrincipalName,mail" } } $responses = Invoke-GraphBatch -Requests @($requests) -BatchSize $BatchSize $memberCount = @{} $owners = @{} foreach ($r in $responses) { $id = $r.CorrelationKey.Id if ($r.Status -ne 200) { if ($r.CorrelationKey.Kind -eq 'Count') { $memberCount[$id] = 'Error' } else { $owners[$id] = 'Error' } continue } if ($r.CorrelationKey.Kind -eq 'Count') { $memberCount[$id] = $r.Body['@odata.count'] } else { $owners[$id] = (@($r.Body.value) | Where-Object { $_ } | ForEach-Object { if ($_.userPrincipalName) { $_.userPrincipalName } elseif ($_.mail) { $_.mail } else { $_.displayName } }) -join '; ' } } $rows = foreach ($g in ($groups | Sort-Object displayName)) { $row = [ordered]@{} foreach ($c in (Get-DistributionListColumns)) { $row[$c] = $null } $row.ObjectId = $g.id $row.DisplayName = $g.displayName $row.PrimarySmtpAddress = $g.mail $row.OnPremisesSyncEnabled = [bool]$g.onPremisesSyncEnabled $row.MemberCount = $memberCount[$g.id] $row.Owners = $owners[$g.id] [PSCustomObject]$row } Write-Host "`n Preview (first 10 of $(@($rows).Count)):" -ForegroundColor Cyan $rows | Select-Object -First 10 -Property DisplayName, PrimarySmtpAddress, MemberCount, Owners | Format-Table -AutoSize | Out-Host $exportPath = Join-Path $folderPath "DistributionLists-$(Get-Date -Format 'yyyy-MM-dd_HHmmss').xlsx" Export-DistributionListWorkbook -Rows @($rows) -Path $exportPath Write-Host "`n==========================================" -ForegroundColor DarkGray Write-Host " EXPORT COMPLETE" -ForegroundColor Cyan Write-Host "==========================================" -ForegroundColor DarkGray Write-Host " Exported $(@($rows).Count) distribution list(s) to: $exportPath" -ForegroundColor Green Write-Host " Fill in Include, AccessPackageDisplayName, AccessPackageDescription" -ForegroundColor White Write-Host " and ScopingNotes, then run Sync-SiSAccessPackage." -ForegroundColor White # The same rows as in the file, one object per distribution list foreach ($row in $rows) { $row.PSObject.TypeNames.Insert(0, 'SiSGovernance.DistributionListRow') $row } } |