Public/Get-M365LicenseActivity.ps1

function Get-M365LicenseActivity {
    <#
    .SYNOPSIS
        Retrieves license change audit log entries from Microsoft Graph.
    .OUTPUTS
        [PSCustomObject[]] One object per audit log entry.
    #>

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

        [int]$DaysBack = 30
    )

    Write-M365Log "Retrieving license activity (last $DaysBack days)..."

    $startDate = (Get-Date).AddDays(-$DaysBack).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')

    $filter = "(activityDisplayName eq 'Change user license' or activityDisplayName eq 'Add license to user' or activityDisplayName eq 'Remove license from user') and activityDateTime ge $startDate"
    $encodedFilter = [System.Uri]::EscapeDataString($filter)

    $endpoint = "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?`$filter=$encodedFilter&`$orderby=activityDateTime desc"

    try {
        $auditLogs = Invoke-M365GraphRequest -Uri $endpoint -AccessToken $AccessToken -AllPages
        Write-M365Log "Retrieved $($auditLogs.Count) audit log entries"
    }
    catch {
        if ($_.Exception.Message -like '*403*' -or $_.Exception.Message -like '*Authorization*') {
            Write-M365Log "Permission denied for audit logs. Ensure AuditLog.Read.All permission is granted." -Level Warning
            return
        }
        throw
    }

    if ($auditLogs.Count -eq 0) {
        Write-M365Log "No license activity found in the last $DaysBack days"
        return
    }

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

    foreach ($log in $auditLogs) {
        $targetUser = $log.targetResources | Where-Object { $_.type -eq 'User' } | Select-Object -First 1

        $licenseChanges = $log.targetResources |
            Where-Object { $_.modifiedProperties } |
            ForEach-Object { $_.modifiedProperties } |
            Where-Object { $_.displayName -like '*License*' -or $_.displayName -like '*Sku*' }

        $licenseChangesJson = if ($licenseChanges) {
            $licenseChanges | ForEach-Object {
                @{
                    property = $_.displayName
                    oldValue = $_.oldValue
                    newValue = $_.newValue
                }
            } | ConvertTo-Json -Compress
        } else { '[]' }

        $actorUPN = $null
        $actorDisplayName = $null
        $actorType = 'Unknown'

        if ($log.initiatedBy.user) {
            $actorUPN = $log.initiatedBy.user.userPrincipalName
            $actorDisplayName = $log.initiatedBy.user.displayName
            $actorType = 'User'
        }
        elseif ($log.initiatedBy.app) {
            $actorDisplayName = $log.initiatedBy.app.displayName
            $actorType = 'Application'
        }

        [PSCustomObject]@{
            ActivityId        = $log.id
            ActivityDateTime  = $log.activityDateTime
            ActivityType      = $log.activityDisplayName
            Result            = $log.result
            ResultReason      = $log.resultReason
            TargetUserId      = if ($targetUser) { $targetUser.id } else { $null }
            TargetUserUpn     = if ($targetUser) { $targetUser.userPrincipalName } else { $null }
            TargetUserDisplay = if ($targetUser) { $targetUser.displayName } else { $null }
            ActorUpn          = $actorUPN
            ActorDisplayName  = $actorDisplayName
            ActorType         = $actorType
            LicenseChanges    = $licenseChangesJson
            CorrelationId     = $log.correlationId
            IngestionTime     = $ingestionTime
        }
    }

    Write-M365Log "Emitted license activity records to pipeline"
}