Private/EntraMonitor/Detections/Test-EntraAuditLogGap.ps1

# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0
# https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/
# AI/LLM use: see AI-USAGE.md for required attribution
function Test-EntraAuditLogGap {
    [CmdletBinding()]
    param(
        [hashtable[]]$AuditEvents = @(),

        [int]$GapThresholdHours = 24
    )

    $results = [System.Collections.Generic.List[PSCustomObject]]::new()

    if ($AuditEvents.Count -lt 2) { return @($results) }

    # Parse and sort all timestamps
    $timestamps = [System.Collections.Generic.List[datetime]]::new()
    foreach ($event in $AuditEvents) {
        $ts = $event.Timestamp
        if (-not $ts) { continue }
        try {
            $dt = if ($ts -is [datetime]) { $ts.ToUniversalTime() } else { [datetime]::Parse($ts).ToUniversalTime() }
            $timestamps.Add($dt)
        } catch {
            continue
        }
    }

    if ($timestamps.Count -lt 2) { return @($results) }

    $timestamps.Sort()

    # Check for gaps exceeding threshold
    for ($i = 0; $i -lt $timestamps.Count - 1; $i++) {
        $gap = $timestamps[$i + 1] - $timestamps[$i]
        if ($gap.TotalHours -ge $GapThresholdHours) {
            $results.Add([PSCustomObject]@{
                GapStart     = $timestamps[$i]
                GapEnd       = $timestamps[$i + 1]
                GapHours     = [Math]::Round($gap.TotalHours, 1)
                GapDays      = [Math]::Round($gap.TotalDays, 1)
                EventsBefore = $i + 1
                EventsAfter  = $timestamps.Count - $i - 1
            })
        }
    }

    return @($results)
}