Private/Core/Test-ImpossibleTravel.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-ImpossibleTravel { [CmdletBinding()] param( [hashtable[]]$LoginEvents = @(), [hashtable]$GeoData = @{}, [double]$MaxSpeedKmh = 900 ) $results = [System.Collections.Generic.List[PSCustomObject]]::new() if ($LoginEvents.Count -lt 2) { return @($results) } # Filter to events with IP and geo data that has coordinates $geoEvents = [System.Collections.Generic.List[hashtable]]::new() foreach ($event in $LoginEvents) { $ip = $event.IpAddress if (-not $ip) { continue } if (-not $GeoData.ContainsKey($ip) -or -not $GeoData[$ip]) { continue } $geo = $GeoData[$ip] if ($null -eq $geo.Latitude -or $null -eq $geo.Longitude) { continue } if ($geo.Latitude -eq 0 -and $geo.Longitude -eq 0) { continue } $geoEvents.Add($event) } if ($geoEvents.Count -lt 2) { return @($results) } # Sort by timestamp $sorted = @($geoEvents | Sort-Object { $_.Timestamp }) # Compare consecutive logins for ($i = 0; $i -lt $sorted.Count - 1; $i++) { $eventA = $sorted[$i] $eventB = $sorted[$i + 1] $ipA = $eventA.IpAddress $ipB = $eventB.IpAddress if ($ipA -eq $ipB) { continue } $geoA = $GeoData[$ipA] $geoB = $GeoData[$ipB] $distanceKm = Get-HaversineDistance -Lat1 $geoA.Latitude -Lon1 $geoA.Longitude ` -Lat2 $geoB.Latitude -Lon2 $geoB.Longitude if ($distanceKm -lt 100) { continue } $tsA = if ($eventA.Timestamp -is [datetime]) { $eventA.Timestamp } else { try { [datetime]::Parse($eventA.Timestamp) } catch { continue } } $tsB = if ($eventB.Timestamp -is [datetime]) { $eventB.Timestamp } else { try { [datetime]::Parse($eventB.Timestamp) } catch { continue } } $hoursDiff = [Math]::Abs(($tsB - $tsA).TotalHours) if ($hoursDiff -lt 0.01) { $hoursDiff = 0.01 } $requiredSpeed = $distanceKm / $hoursDiff if ($requiredSpeed -gt $MaxSpeedKmh) { $results.Add([PSCustomObject]@{ FromIp = $ipA ToIp = $ipB FromCountry = $geoA.CountryCode ToCountry = $geoB.CountryCode FromTime = $tsA ToTime = $tsB DistanceKm = [Math]::Round($distanceKm, 0) TimeDiffHours = [Math]::Round($hoursDiff, 2) RequiredSpeedKmh = [Math]::Round($requiredSpeed, 0) }) } } return @($results) } function Get-HaversineDistance { [CmdletBinding()] param( [double]$Lat1, [double]$Lon1, [double]$Lat2, [double]$Lon2 ) $R = 6371.0 $dLat = ($Lat2 - $Lat1) * [Math]::PI / 180.0 $dLon = ($Lon2 - $Lon1) * [Math]::PI / 180.0 $a = [Math]::Sin($dLat / 2) * [Math]::Sin($dLat / 2) + [Math]::Cos($Lat1 * [Math]::PI / 180.0) * [Math]::Cos($Lat2 * [Math]::PI / 180.0) * [Math]::Sin($dLon / 2) * [Math]::Sin($dLon / 2) $c = 2 * [Math]::Atan2([Math]::Sqrt($a), [Math]::Sqrt(1 - $a)) return $R * $c } |