Private/M365Monitor/Detections/Test-M365BulkFileExfiltration.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-M365BulkFileExfiltration { [CmdletBinding()] param( [PSCustomObject[]]$Events = @(), [int]$Threshold = 100, [int]$WindowMinutes = 30 ) $results = [System.Collections.Generic.List[PSCustomObject]]::new() if ($Events.Count -lt $Threshold) { return @($results) } # Group events by actor for per-user burst detection $eventsByActor = @{} foreach ($event in $Events) { $actor = $event.Actor ?? 'Unknown' if (-not $eventsByActor.ContainsKey($actor)) { $eventsByActor[$actor] = [System.Collections.Generic.List[PSCustomObject]]::new() } $eventsByActor[$actor].Add($event) } foreach ($actor in $eventsByActor.Keys) { $actorEvents = @($eventsByActor[$actor]) if ($actorEvents.Count -lt $Threshold) { continue } # Sort by timestamp $sorted = @($actorEvents | Sort-Object { try { [datetime]::Parse($_.Timestamp) } catch { [datetime]::MinValue } }) # Sliding window detection with 5-minute bucket deduplication $detectedWindows = [System.Collections.Generic.HashSet[string]]::new() for ($i = 0; $i -lt $sorted.Count; $i++) { try { $windowStart = [datetime]::Parse($sorted[$i].Timestamp) } catch { continue } $windowEnd = $windowStart.AddMinutes($WindowMinutes) # Count events in window $windowEvents = [System.Collections.Generic.List[PSCustomObject]]::new() for ($j = $i; $j -lt $sorted.Count; $j++) { try { $evtTime = [datetime]::Parse($sorted[$j].Timestamp) } catch { continue } if ($evtTime -gt $windowEnd) { break } $windowEvents.Add($sorted[$j]) } if ($windowEvents.Count -ge $Threshold) { # Deduplicate overlapping windows using 5-minute bucketing $bucketKey = "$actor|$($windowStart.ToString('yyyyMMddHH'))$([Math]::Floor($windowStart.Minute / 5) * 5)" if ($detectedWindows.Contains($bucketKey)) { continue } [void]$detectedWindows.Add($bucketKey) # Gather file details $uniqueFiles = @($windowEvents | ForEach-Object { $_.TargetName ?? 'unknown' } | Sort-Object -Unique) $activityBreakdown = @{} foreach ($evt in $windowEvents) { $act = $evt.Activity ?? 'Unknown' $activityBreakdown[$act] = ($activityBreakdown[$act] ?? 0) + 1 } # Severity assessment $severity = if ($windowEvents.Count -ge ($Threshold * 3)) { 'Critical' } elseif ($windowEvents.Count -ge ($Threshold * 2)) { 'High' } else { 'Medium' } $results.Add([PSCustomObject]@{ Timestamp = $windowStart.ToString('o') Actor = $actor DetectionType = 'm365BulkFileExfiltration' Description = "Bulk file operation: $($windowEvents.Count) files in $WindowMinutes min by $actor ($($uniqueFiles.Count) unique files)" Details = @{ WindowStart = $windowStart.ToString('o') WindowEnd = $windowEnd.ToString('o') FileCount = $windowEvents.Count UniqueFileCount = $uniqueFiles.Count SampleFiles = @($uniqueFiles | Select-Object -First 10) ActivityBreakdown = $activityBreakdown } Severity = $severity }) # Skip ahead past this window to avoid duplicate detections for ($k = $i + 1; $k -lt $sorted.Count; $k++) { try { if ([datetime]::Parse($sorted[$k].Timestamp) -gt $windowEnd) { $i = $k - 1 break } } catch { continue } if ($k -eq $sorted.Count - 1) { $i = $k } } } } } return @($results) } |