private/get-EntraUsersAndGroupsBatch.ps1

function get-EntraUsersAndGroupsBatch {
    <#
        Author = "Jos Lieben (jos@lieben.nu)"
        CompanyName = "Lieben Consultancy"
        Copyright = "https://www.lieben.nu/liebensraum/commercial-use/"
        Parameters:
    #>
        
    Param(
        [parameter(Mandatory=$true)][object[]]$entraUsers
    )

    if ($null -eq $entraUsers) {
        Write-LogMessage -message "Critical error: entraUsers parameter is null" -level 1
        return
    }

    if ($entraUsers.Count -eq 0) {
        Write-LogMessage -message "Warning: entraUsers parameter is an empty array" -level 2
        return
    }

    Write-LogMessage -message "Processing batch with $($entraUsers.Count) users. First user ID: $($entraUsers[0].id)" -level 4

    $global:EntraPermissions = @{}
    [System.Collections.ArrayList]$entraUserRows = @()
    $count = 0

    Write-LogMessage -message "Processing user owned objects in batch mode" -level 4
    $batchOwnedObjectsSplat = @{
        batchItems = $entraUsers
        batchSize = 20
        batchActivity = "Processing user owned objects"
        batchUrlGenerator = {
            param($user)
            return "/users/$($user.id)/ownedObjects?`$select=id,displayName,groupTypes,mailEnabled,securityEnabled,membershipRule&`$top=999"
        }
        batchIdGenerator = {
            param($index)
            return "owned_$index"
        }
        progressId = 2
    }
    $ownedObjectsBatchResults = new-GraphBatchQuery @batchOwnedObjectsSplat

    if ($null -eq $ownedObjectsBatchResults) {
        Write-LogMessage -message "Critical error: ownedObjectsBatchResults is null" -level 1
        return
    }

    Write-LogMessage -message "Processing user group memberships in batch mode" -level 4
    $batchMembershipsSplat = @{
        batchItems = $entraUsers
        batchSize = 20
        batchActivity = "Processing user group memberships"
        batchUrlGenerator = {
            param($user)
            return "/users/$($user.id)/transitiveMemberOf/microsoft.graph.group?`$select=id,displayName,groupTypes,mailEnabled,securityEnabled,membershipRule&`$top=999"
        }
        batchIdGenerator = {
            param($index)
            return "member_$index"
        }
        progressId = 3
    }
    $membershipsBatchResults = new-GraphBatchQuery @batchMembershipsSplat

    if ($null -eq $membershipsBatchResults) {
        Write-LogMessage -message "Critical error: membershipsBatchResults is null" -level 1
        return
    }

    Write-LogMessage -message "Processing batch results" -level 4
    for ($batchIndex = 0; $batchIndex -lt $ownedObjectsBatchResults.Count; $batchIndex++) {
        $ownedBatch = $ownedObjectsBatchResults[$batchIndex]
        $membershipBatch = $membershipsBatchResults[$batchIndex]

        # Calculate the start index for this batch
        $batchStartIndex = $batchIndex * 20

        # Process each user in the batch
        for ($j = 0; $j -lt [Math]::Min(20, $entraUsers.Count - $batchStartIndex); $j++) {
            $entraUser = $entraUsers[$batchStartIndex + $j]
            $count++

            # Determine user type
            if($entraUser.userPrincipalName -like "*#EXT#@*") {
                $principalType = "External User"
            } else {
                $principalType = "Internal User"
            }

            # Process owned objects for this user
            $ownedResponse = $ownedBatch.responses | Where-Object { $_.id -eq "owned_$j" }
            if ($ownedResponse -and $ownedResponse.status -eq 200) {
                $ownedObjects = $ownedResponse.body.value
                if ($null -eq $ownedObjects) { $ownedObjects = @() }

                foreach($ownedObject in $ownedObjects) {
                    if($ownedObject."@odata.type" -eq "#microsoft.graph.group") {
                        $groupType = Get-EntraGroupType -group $ownedObject
                        $entraUserRows.Add([PSCustomObject]@{
                            "GroupName" = $ownedObject.displayName
                            "GroupType" = $groupType
                            "GroupID" = $ownedObject.id
                            "MemberName" = $entraUser.displayName
                            "MemberID" = $entraUser.id
                            "MemberUPN" = $entraUser.userPrincipalName
                            "MemberType" = $principalType
                            "Role" = "Owner"
                        }) > $Null
                    } else {
                        $permissionsSplat = @{
                            targetPath = "/$($ownedObject.displayName)"
                            targetType = $ownedObject."@odata.type".Split(".")[2]
                            targetId = $ownedObject.id
                            principalEntraId = $entraUser.id
                            principalSysName = $entraUser.displayName
                            principalType = $principalType
                            principalRole = "Owner"
                            through = "Direct"
                            parentId = ""
                            accessType = "Allow"
                            tenure = "Permanent"
                            startDateTime = ""
                            endDateTime = ""
                        }
                        New-EntraPermissionEntry @permissionsSplat
                    }
                }
            }else {
                Write-LogMessage -level 2 -message "Failed to get owned objects for user $($entraUser.id), status: $($ownedResponse.status)"
            }

            # Process memberships for this user
            $membershipResponse = $membershipBatch.responses | Where-Object { $_.id -eq "member_$j" }
            if ($membershipResponse -and $membershipResponse.status -eq 200) {
                $memberships = $membershipResponse.body.value
                if ($null -eq $memberships) { $memberships = @() }

                foreach($membership in $memberships) {
                    $groupType = Get-EntraGroupType -group $membership
                    $entraUserRows.Add([PSCustomObject]@{
                        "GroupName" = $membership.displayName
                        "GroupType" = $groupType
                        "GroupID" = $membership.id
                        "MemberName" = $entraUser.displayName
                        "MemberID" = $entraUser.id
                        "MemberUPN" = $entraUser.userPrincipalName
                        "MemberType" = $principalType
                        "Role" = "Member"
                    }) > $Null
                }
            }else {
                Write-LogMessage -level 2 -message "Failed to get memberships for user $($entraUser.id), status: $($membershipResponse.status)"
            }
        }
    }

    Write-Progress -Id 2 -Completed

    [System.GC]::GetTotalMemory($true) | out-null

    Add-ToReportQueue -permissions $entraUserRows -category "GroupsAndMembers"

    $permissionRows = foreach($row in $global:EntraPermissions.Keys){
        foreach($permission in $global:EntraPermissions.$row){
            [PSCustomObject]@{
                "targetPath" = $row
                "targetType" = $permission.targetType
                "targetId" = $permission.targetId
                "principalEntraId" = $permission.principalEntraId
                "principalSysId" = $permission.principalSysId
                "principalSysName" = $permission.principalSysName
                "principalType" = $permission.principalType
                "principalRole" = $permission.principalRole
                "through" = $permission.through
                "parentId" = $permission.parentId
                "accessType" = $permission.accessType
                "tenure" = $permission.tenure
                "startDateTime" = $permission.startDateTime
                "endDateTime" = $permission.endDateTime
                "createdDateTime" = $permission.createdDateTime
                "modifiedDateTime" = $permission.modifiedDateTime
            }
        }
    }

    Add-ToReportQueue -permissions $permissionRows -category "Entra"
}