:

    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