Public/Get-M365UserSegments.ps1

function Get-M365UserSegments {
    <#
    .SYNOPSIS
        Retrieves license-assignment group memberships from Microsoft Graph for user segmentation.
    .DESCRIPTION
        Finds all Entra ID groups that have licenses assigned (group-based licensing),
        retrieves each group's assigned licenses and members, and outputs one row per
        user-group pair. This enables segment-based license optimization analysis in Kusto.
    .PARAMETER AccessToken
        Microsoft Graph API access token.
    .OUTPUTS
        [PSCustomObject[]] One object per user per segment group.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$AccessToken
    )

    # Build SKU cache for friendly names
    Write-M365Log "Building SKU reference cache for segment licenses..."
    $skuCache = @{}
    try {
        $skus = Invoke-M365GraphRequest -AccessToken $AccessToken -Uri "https://graph.microsoft.com/v1.0/subscribedSkus" -AllPages
        foreach ($sku in $skus) {
            $skuCache[$sku.skuId] = $sku.skuPartNumber
        }
        Write-M365Log "Cached $($skuCache.Count) SKUs"
    }
    catch {
        Write-M365Log "Warning: Could not retrieve SKUs: $_" -Level Warning
    }

    # Retrieve all groups with assignedLicenses property, then filter client-side.
    # Server-side filtering on assignedLicenses/$count requires advanced query capabilities
    # which may not be available in all tenants.
    $groupUri = "https://graph.microsoft.com/v1.0/groups?`$select=id,displayName,assignedLicenses&`$top=999"

    Write-M365Log "Retrieving groups from Microsoft Graph..."
    $allGroups = Invoke-M365GraphRequest -AccessToken $AccessToken -Uri $groupUri -AllPages
    Write-M365Log "Retrieved $($allGroups.Count) total groups"

    # Keep only groups that have at least one license assigned
    $groups = $allGroups | Where-Object { $_.assignedLicenses -and $_.assignedLicenses.Count -gt 0 }
    Write-M365Log "Found $($groups.Count) groups with license assignments"

    $ingestionTime = (Get-Date).ToUniversalTime().ToString('o')
    $totalMembers = 0

    foreach ($group in $groups) {
        $groupId = $group.id
        $groupDisplayName = $group.displayName

        # Resolve assigned licenses to SKU part numbers
        $groupLicenses = @()
        if ($group.assignedLicenses) {
            foreach ($lic in $group.assignedLicenses) {
                $skuName = $skuCache[$lic.skuId]
                if (-not $skuName) { $skuName = $lic.skuId }
                $groupLicenses += @{
                    skuId = $lic.skuId
                    skuPartNumber = $skuName
                }
            }
        }
        $licensesJson = if ($groupLicenses.Count -gt 0) {
            ($groupLicenses | ConvertTo-Json -Compress -Depth 5)
        } else { "[]" }

        # Get group members (users only)
        $memberUri = "https://graph.microsoft.com/v1.0/groups/$groupId/members/microsoft.graph.user?`$select=id,userPrincipalName&`$top=999"
        $members = Invoke-M365GraphRequest -AccessToken $AccessToken -Uri $memberUri -AllPages

        Write-M365Log " $groupDisplayName : $($members.Count) members, $($groupLicenses.Count) licenses"

        foreach ($member in $members) {
            $totalMembers++
            [PSCustomObject]@{
                GroupId           = $groupId
                GroupDisplayName  = $groupDisplayName
                GroupLicenses     = $licensesJson
                UserId            = $member.id
                UserPrincipalName = $member.userPrincipalName
                IngestionTime     = $ingestionTime
            }
        }
    }

    Write-M365Log "Emitted $totalMembers user-segment records to pipeline"
}