memberOf-Nutzung ermitteln
Der memberOf-Operator in Entra ID konnte einige Zeit lang genutzt werden, um Gruppenmitgliedschaften als Attribute für dynamische Gruppen, Administrative Units und Access Package Assignments zu nutzen. Nach langer Preview-Phase hat Microsoft nun angekündigt, den Operator im November 2026 nach jahrelanger Preview-Phase wieder zu entfernen. In diesem Video erfährst du die Hintergründe, wie du betroffene Objekte ermittelst und was du anschließend zu tun hast.
Benötigst du Unterstützung?
Melde dich bei uns!
Hier findest du das Script, mit dem du betroffene Objekte in deinem Tenant auslesen kannst.
Get-MemberOfUsage.ps1
<#
.SYNOPSIS
Ermittelt alle Entra-Objekte, die den memberOf-Rule-Operator verwenden.
.DESCRIPTION
Der memberOf-Rule-Operator wird am 03.11.2026 zurückgezogen (MC1448379).
Betroffen sind drei Objekttypen:
1. Dynamic Membership Groups (User und Device)
2. Dynamic Administrative Units (inkl. Restricted Management AUs)
3. Entitlement Management Auto-Assignment Policies
Das Skript liest ausschließlich (keine Schreibzugriffe) und gibt die
Treffer als Objekte auf die Pipeline aus. Die referenzierten Quellgruppen
werden in Klarnamen aufgelöst. Optional CSV-Export.
.PARAMETER CsvPath
Optionaler Pfad für einen CSV-Export der Ergebnisse.
.EXAMPLE
.\Get-MemberOfUsage.ps1 | Format-Table ObjectType, DisplayName, ProcessingState, SourceGroupCount, SourceGroups -AutoSize -Wrap
.EXAMPLE
.\Get-MemberOfUsage.ps1 -CsvPath .\memberOf-Findings.csv
.NOTES
Benötigt: Microsoft.Graph.Authentication
Scopes: Group.Read.All, AdministrativeUnit.Read.All, EntitlementManagement.Read.All
#>
#Requires -Modules Microsoft.Graph.Authentication
[CmdletBinding()]
param(
[string]$CsvPath
)
#Verbindung
$requiredScopes = @(
'Group.Read.All'
'AdministrativeUnit.Read.All'
'EntitlementManagement.Read.All'
)
if (-not (Get-MgContext)) {
Connect-MgGraph -Scopes $requiredScopes -NoWelcome
}
#Erkennungsmuster
$memberOfPattern = '(?i)\.memberof\b'
#Quellgruppen-GUIDs für die Remediation
$guidPattern = '[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}'
#Cache für die Namensauflösung
$script:NameCache = @{}
#Graph-Collection inkl. Paging auslesen
function Get-GraphCollection {
param([Parameter(Mandatory)][string]$Uri)
$result = [System.Collections.Generic.List[object]]::new()
$next = $Uri
while ($next) {
$page = Invoke-MgGraphRequest -Method GET -Uri $next -OutputType PSObject
if ($page.value) { $result.AddRange(@($page.value)) }
$next = $page.'@odata.nextLink'
}
return $result
}
#Quellgruppe auflösen, inkl. Prüfung auf gelöschte Objekte
function Resolve-SourceGroupName {
param([Parameter(Mandatory)][string]$Id)
if ($script:NameCache.ContainsKey($Id)) { return $script:NameCache[$Id] }
try {
$obj = Invoke-MgGraphRequest -Method GET -OutputType PSObject `
-Uri ('https://graph.microsoft.com/v1.0/groups/' + $Id + '?$select=id,displayName')
$name = $obj.displayName
}
catch {
try {
$del = Invoke-MgGraphRequest -Method GET -OutputType PSObject `
-Uri ('https://graph.microsoft.com/v1.0/directory/deletedItems/' + $Id)
$name = '{0} [SOFT-DELETED]' -f $del.displayName
}
catch {
$name = '[NICHT AUFLÖSBAR]'
}
}
$script:NameCache[$Id] = $name
return $name
}
#GUIDs aus dem Rule ziehen und auflösen
function Get-RuleSourceGroups {
param([string]$Rule)
$ids = @(([regex]::Matches($Rule, $guidPattern)).Value | Select-Object -Unique)
$names = foreach ($id in $ids) { Resolve-SourceGroupName -Id $id }
[pscustomobject]@{
Count = $ids.Count
Ids = ($ids -join '; ')
Names = ($names -join '; ')
}
}
$findings = [System.Collections.Generic.List[object]]::new()
#Dynamische Gruppen
Write-Verbose 'Lese Dynamic Membership Groups...'
$groupUri = 'https://graph.microsoft.com/v1.0/groups' +
'?$filter=groupTypes/any(c:c eq ''DynamicMembership'')' +
'&$select=id,displayName,membershipRule,membershipRuleProcessingState,groupTypes,mailEnabled,securityEnabled,onPremisesSyncEnabled' +
'&$top=999'
foreach ($g in Get-GraphCollection -Uri $groupUri) {
if ($g.membershipRule -match $memberOfPattern) {
$src = Get-RuleSourceGroups -Rule $g.membershipRule
$findings.Add([pscustomobject]@{
ObjectType = 'DynamicGroup'
DisplayName = $g.displayName
Id = $g.id
Detail = $(if ($g.groupTypes -contains 'Unified') { 'Microsoft 365' } else { 'Security' })
ProcessingState = $g.membershipRuleProcessingState
SourceGroupCount = $src.Count
SourceGroups = $src.Names
SourceGroupIds = $src.Ids
Rule = $g.membershipRule
})
}
}
#Administrative Units
Write-Verbose 'Lese Administrative Units...'
$auUri = 'https://graph.microsoft.com/v1.0/directory/administrativeUnits' +
'?$select=id,displayName,membershipType,membershipRule,membershipRuleProcessingState,isMemberManagementRestricted' +
'&$top=999'
foreach ($au in Get-GraphCollection -Uri $auUri) {
if ($au.membershipType -eq 'Dynamic' -and $au.membershipRule -match $memberOfPattern) {
$src = Get-RuleSourceGroups -Rule $au.membershipRule
$findings.Add([pscustomobject]@{
ObjectType = 'DynamicAdministrativeUnit'
DisplayName = $au.displayName
Id = $au.id
Detail = $(if ($au.isMemberManagementRestricted) { 'Restricted Management AU' } else { 'Standard AU' })
ProcessingState = $au.membershipRuleProcessingState
SourceGroupCount = $src.Count
SourceGroups = $src.Names
SourceGroupIds = $src.Ids
Rule = $au.membershipRule
})
}
}
#Entitlement Management
Write-Verbose 'Lese Entitlement Management Assignment Policies...'
$policyUri = 'https://graph.microsoft.com/v1.0/identityGovernance/entitlementManagement/assignmentPolicies' +
'?$expand=accessPackage($select=id,displayName)'
try {
foreach ($p in Get-GraphCollection -Uri $policyUri) {
if (-not $p.automaticRequestSettings) { continue }
foreach ($target in @($p.specificAllowedTargets)) {
if ([string]$target.'@odata.type' -notlike '*attributeRuleMembers*') { continue }
if ($target.membershipRule -notmatch $memberOfPattern) { continue }
$src = Get-RuleSourceGroups -Rule $target.membershipRule
$findings.Add([pscustomobject]@{
ObjectType = 'EntitlementAutoAssignmentPolicy'
DisplayName = $p.displayName
Id = $p.id
Detail = "Access Package: $($p.accessPackage.displayName)"
ProcessingState = 'n/a'
SourceGroupCount = $src.Count
SourceGroups = $src.Names
SourceGroupIds = $src.Ids
Rule = $target.membershipRule
})
}
}
}
catch {
#kein Entitlement Management
Write-Warning "Entitlement Management konnte nicht ausgelesen werden: $($_.Exception.Message)"
}
#Ausgabe
if ($findings.Count -eq 0) {
Write-Host 'Keine Objekte mit memberOf-Rule gefunden.' -ForegroundColor Green
}
else {
Write-Host "Gefundene Objekte mit memberOf-Rule: $($findings.Count)" -ForegroundColor Yellow
$findings | Group-Object ObjectType |
ForEach-Object { Write-Host (" {0,-34} {1}" -f $_.Name, $_.Count) }
#Rules mit toten Referenzen
$broken = @($findings | Where-Object { $_.SourceGroups -match 'SOFT-DELETED|NICHT AUFLÖSBAR' })
if ($broken.Count -gt 0) {
Write-Warning "$($broken.Count) Rule(s) verweisen auf gelöschte oder nicht lesbare Quellgruppen."
}
}
if ($CsvPath) {
$findings | Export-Csv -Path $CsvPath -NoTypeInformation -Encoding UTF8 -Delimiter ';'
Write-Host "Export: $CsvPath"
}
$findings