Public/Export-EGIGroupSnapshot.ps1
|
function Export-EGIGroupSnapshot { <# .SYNOPSIS Exports all dynamic membership groups and their rules to a JSON snapshot file. .DESCRIPTION Intended to be run on a schedule (e.g. a daily Azure Automation runbook or scheduled task) and the output committed to a Git repository. Each commit then becomes a reviewable version of "what dynamic group rules looked like on this date" - the same policy-as-code pattern Microsoft recommends for Conditional Access, applied to dynamic groups. .PARAMETER Path File path to write the JSON snapshot to. .EXAMPLE Export-EGIGroupSnapshot -Path './snapshots/dynamic-groups-2026-07-21.json' #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Path ) # Filtering on groupTypes/any(...) is a Graph "advanced query": it requires # $count=true plus the ConsistencyLevel:eventual header on every page. $uri = "https://graph.microsoft.com/v1.0/groups?`$filter=groupTypes/any(c:c eq 'DynamicMembership')&`$count=true&`$select=id,displayName,membershipRule,membershipRuleProcessingState,isAssignableToRole" $groups = Invoke-EGIGraphPaged -Uri $uri -Headers @{ ConsistencyLevel = 'eventual' } $snapshot = [pscustomobject]@{ CapturedAtUtc = (Get-Date).ToUniversalTime().ToString('o') GroupCount = @($groups).Count Groups = @($groups | ForEach-Object { [pscustomobject]@{ GroupId = $_.id DisplayName = $_.displayName MembershipRule = $_.membershipRule ProcessingState = $_.membershipRuleProcessingState RoleAssignable = [bool]$_.isAssignableToRole } } | Sort-Object GroupId) } # BOM-less UTF-8 regardless of PowerShell edition (5.1's Set-Content writes a # BOM), so snapshots committed from different hosts diff cleanly in Git. $json = ($snapshot | ConvertTo-Json -Depth 6) + "`n" $resolvedPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($Path) [System.IO.File]::WriteAllText($resolvedPath, $json, [System.Text.UTF8Encoding]::new($false)) Write-Verbose "Wrote snapshot of $($snapshot.GroupCount) dynamic group(s) to $Path" return $snapshot } |