| name | impossible-travel |
| description | Investigate whether an account logged in from two locations too far apart to have been physically traveled in the elapsed time between logons (impossible travel). Use when given geolocated authentication logs and asked to check for account compromise, credential sharing, or VPN/proxy abuse. |
Impossible Travel
MITRE ATT&CK: T1078 (Valid Accounts) — impossible travel is a
detection heuristic for compromised or shared credentials, not a technique
in itself.
1. Which question am I trying to answer?
Did account X log in from location A and then location B, where
the distance between A and B could not plausibly have been traveled
in the time elapsed between the two logons?
2. Which direct evidence do I need?
Per successful authentication event: Time, User, and a resolved
geolocation of the Source (latitude/longitude, or at minimum
city-level). Logon type is useful context (e.g., ruling out VPN split-
tunnel weirdness) but not required for the core calculation.
3. Where do I get that data?
- Identity provider / SSO logs are the best source when available —
Azure AD / Entra ID
SigninLogs and Okta System Log already include a
resolved location (city/country, sometimes lat/long) per sign-in.
- VPN concentrator logs for the source public IP if federation logs
aren't available.
- Geo-IP enrichment (MaxMind GeoLite2/GeoIP2, or your SIEM's built-in
enrichment) applied to the source IP from Security.evtx
4624/4625 when
no IdP is in the path — note that internal/RFC1918 source IPs cannot be
geolocated this way and should be excluded or treated as "on-network."
- This skill's script does not perform live geo-IP lookups — resolve
location as a data-prep step and feed the script pre-resolved
source_lat/source_lon columns. Keeping resolution out of the script
avoids depending on network access/an API key at analysis time.
4. How do I analyze that data to answer the question?
Extend the shared schema with source_lat and source_lon (decimal
degrees) per event, then run:
python3 scripts/analyze.py --input auth_events.csv --max-speed-kmh 900
Logic: for each user, take consecutive successful logons in time order,
compute the great-circle (haversine) distance between their source
coordinates, divide by the elapsed time, and flag any pair where the implied
speed exceeds --max-speed-kmh.
Tuning: 900 km/h approximates commercial air travel — a reasonable
"cannot possibly be true" ceiling. Lower it (e.g. 120 km/h) if you want to
catch travel that's merely implausible rather than physically impossible,
at the cost of more false positives from legitimate mobile/VPN roaming.
5. What answer does the analysis provide?
Pairs of logons per user with implied travel speed exceeding the threshold,
including both locations and the time delta. This indicates the two logons
are unlikely to originate from the same physical person moving between
them — it does not by itself distinguish between:
- Compromised credentials used concurrently from two locations.
- Corporate VPN/proxy egress changes — an account can appear to "jump"
countries between requests if traffic egresses from different VPN exit
nodes; check whether the source IPs belong to known corporate VPN ranges
before escalating.
- Cloud provider / CDN quirks in the IdP's own geo-resolution — mobile
carrier NAT and satellite ISPs are common false-positive sources.
- Shared service accounts used by automation in multiple regions, which
will trip this constantly and should generally be excluded from this
analytic rather than tuned around.
A flag that survives ruling out VPN/service-account explanations is strong
evidence of concurrent use by two different actors and warrants immediate
credential reset and session revocation alongside further investigation.