Public/Sync-FDAGovernanceLog.ps1

function Sync-FDAGovernanceLog {
    <#
    .SYNOPSIS
        Pull recent Fabric/Power BI activity events from the M365 Unified
        Audit Log and persist as FDAAuthEvents.

    .DESCRIPTION
        Uses Office 365 Management Activity API
        (https://manage.office.com/api/v1.0/{tenantId}/activity/feed/...).
        Filters to Fabric/Power BI operations. Designed to be invoked on a
        schedule (Azure Function, Automation Runbook, or Register-ScheduledJob).

        Per-tenant subscription to the Audit.General content type must be
        enabled. This cmdlet will subscribe if absent.

    .PARAMETER LookbackHours
        How far back to pull. Default 24.
    #>

    [CmdletBinding()]
    param(
        [int] $LookbackHours = 24,
        [string[]] $Operations = @(
            'GetDataAgent', 'CreateDataAgent', 'UpdateDataAgent', 'DeleteDataAgent',
            'GetDataset', 'ExecuteQueryOnDataset',
            'AnalyzedByExternalApplication'
        )
    )
    if (-not $script:FDAState.Connected) {
        throw 'Not connected. Call Connect-FDAObservability first.'
    }
    $tenantId = $script:FDAState.TenantId
    if (-not $tenantId) { throw 'TenantId is required (set during Connect-FDAObservability).' }

    $base = "https://manage.office.com/api/v1.0/$tenantId/activity/feed"
    $token = Get-FDAAccessToken -Scope 'https://manage.office.com/.default'
    $headers = @{ Authorization = "Bearer $token"; 'Content-Type' = 'application/json' }

    # Ensure subscription exists.
    $startUrl = "$base/subscriptions/start?contentType=Audit.General"
    try { Invoke-RestMethod -Method Post -Uri $startUrl -Headers $headers -ErrorAction Stop | Out-Null }
    catch { Write-Verbose "Subscription start returned: $($_.Exception.Message) (may already be active)." }

    $startTime = (Get-Date).ToUniversalTime().AddHours(-1 * $LookbackHours).ToString('yyyy-MM-ddTHH:mm:ss')
    $endTime   = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss')
    $contentUrl = "$base/subscriptions/content?contentType=Audit.General&startTime=$startTime&endTime=$endTime"

    $pages = @()
    do {
        $resp = Invoke-WebRequest -Method Get -Uri $contentUrl -Headers $headers -ErrorAction Stop
        $pageItems = $resp.Content | ConvertFrom-Json
        if ($pageItems) { $pages += $pageItems }
        $nextLink = $resp.Headers['NextPageUri']
        $contentUrl = if ($nextLink) { [string]$nextLink } else { $null }
    } while ($contentUrl)

    $count = 0
    foreach ($contentRef in $pages) {
        try {
            $events = Invoke-RestMethod -Method Get -Uri $contentRef.contentUri -Headers $headers -ErrorAction Stop
        } catch {
            Write-Warning "Failed to download $($contentRef.contentUri): $($_.Exception.Message)"
            continue
        }
        $batch = foreach ($e in $events) {
            if ($Operations -and $Operations.Count -gt 0 -and ($e.Operation -notin $Operations)) { continue }
            [pscustomobject]@{
                Timestamp         = ([datetime]$e.CreationTime).ToUniversalTime().ToString('o')
                EventId           = [string]$e.Id
                CorrelationId     = [string]$e.CorrelationId
                TenantId          = [string]$e.OrganizationId
                UserPrincipalName = [string]$e.UserId
                ClientApp         = [string]$e.ClientIP
                AuthMethod        = [string]$e.UserType
                Outcome           = if ($e.ResultStatus -in 'Succeeded', 'Success') { 'Success' } else { 'Failure' }
                ConsentStatus     = [string]$e.ConsentStatus
                RLSContext        = $e.RLSContext
                IPAddress         = [string]$e.ClientIP
                UserAgent         = [string]$e.UserAgent
                Source            = 'M365Audit'
                LevelName         = if ($e.ResultStatus -in 'Succeeded', 'Success') { 'Information' } else { 'Warning' }
                LevelNumeric      = if ($e.ResultStatus -in 'Succeeded', 'Success') { 30 } else { 50 }
                Metadata          = @{ Operation = $e.Operation; Workload = $e.Workload }
            }
        }
        if ($batch -and $batch.Count -gt 0) {
            Invoke-EventhouseIngest -TableName 'FDAAuthEventsRaw' -MappingName 'FDAAuthEventsRawMapping' -Records $batch | Out-Null
            $count += $batch.Count
        }
    }

    [pscustomobject]@{
        Synced       = $count
        WindowHours  = $LookbackHours
        StartTimeUtc = $startTime
        EndTimeUtc   = $endTime
    }
}