Private/Core/Test-BruteForce.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-BruteForce { [CmdletBinding()] param( [hashtable[]]$LoginEvents = @(), [int]$FailureThreshold = 5, [int]$WindowMinutes = 10 ) $results = [PSCustomObject]@{ Detected = $false FailureCount = 0 SuccessAfter = $false FailureWindow = $null SuccessEvent = $null AttackingIps = @() } # Separate successes and failures $failures = [System.Collections.Generic.List[hashtable]]::new() $successes = [System.Collections.Generic.List[hashtable]]::new() foreach ($event in $LoginEvents) { $eventName = $event.EventName if ($eventName -eq 'login_failure') { $ts = if ($event.Timestamp -is [datetime]) { $event.Timestamp } else { try { [datetime]::Parse($event.Timestamp) } catch { continue } } $failures.Add(@{ Timestamp = $ts; IpAddress = $event.IpAddress; Event = $event }) } elseif ($eventName -eq 'login_success') { $ts = if ($event.Timestamp -is [datetime]) { $event.Timestamp } else { try { [datetime]::Parse($event.Timestamp) } catch { continue } } $successes.Add(@{ Timestamp = $ts; IpAddress = $event.IpAddress; Event = $event }) } } if ($failures.Count -lt $FailureThreshold) { return $results } # Sort failures by timestamp $sortedFailures = @($failures | Sort-Object { $_.Timestamp }) $window = [TimeSpan]::FromMinutes($WindowMinutes) # Sliding window to find burst of failures $maxBurstCount = 0 $bestBurstStart = $null $bestBurstEnd = $null $bestBurstIps = @() for ($i = 0; $i -lt $sortedFailures.Count; $i++) { $windowStart = $sortedFailures[$i].Timestamp $windowEnd = $windowStart + $window $burstIps = [System.Collections.Generic.HashSet[string]]::new() $burstCount = 0 for ($j = $i; $j -lt $sortedFailures.Count; $j++) { if ($sortedFailures[$j].Timestamp -gt $windowEnd) { break } $burstCount++ if ($sortedFailures[$j].IpAddress) { [void]$burstIps.Add($sortedFailures[$j].IpAddress) } } if ($burstCount -gt $maxBurstCount) { $maxBurstCount = $burstCount $bestBurstStart = $windowStart $bestBurstEnd = $sortedFailures[[Math]::Min($i + $burstCount - 1, $sortedFailures.Count - 1)].Timestamp $bestBurstIps = @($burstIps | Sort-Object) } } if ($maxBurstCount -ge $FailureThreshold) { $results.Detected = $true $results.FailureCount = $maxBurstCount $results.AttackingIps = $bestBurstIps $results.FailureWindow = [PSCustomObject]@{ Start = $bestBurstStart End = $bestBurstEnd Duration = ($bestBurstEnd - $bestBurstStart) } # Check for success after the failure burst $sortedSuccesses = @($successes | Sort-Object { $_.Timestamp }) foreach ($s in $sortedSuccesses) { if ($s.Timestamp -gt $bestBurstStart) { $results.SuccessAfter = $true $results.SuccessEvent = [PSCustomObject]@{ Timestamp = $s.Timestamp IpAddress = $s.IpAddress } break } } } return $results } |