Imagine a user logging in from New York, then minutes later, accessing files from a server in Tokyo. Physically impossible? Absolutely. But in the realm of cybersecurity, this "impossible travel" could signal a compromised account or a sophisticated attack.
In part 1 of this series, we'll delve into the Impossible Travel use case and demonstrate how to implement detection within Google SecOps. We'll create a custom YARA-L Detection rule, leveraging GeoSpatial functions like math.geo_distance, and make use of SecOps SIEM's native GeoIP enrichment. Then, in part 2, we'll showcase how to present the findings in a clear and actionable Case within SecOps SOAR.
Did the user really reach Mach 4, four times the speed of sound? Read on to find out.
Impossible travel occurs when a user's activity appears to originate from geographically distant locations within an unrealistically short time frame. This often signals compromised credentials, enabling unauthorized access from various parts of the world. For instance, logins from New York and Tokyo minutes apart would be flagged as impossible, given the physical limitations of travel.
By analyzing IP addresses and geolocation data, impossible travel detection serves as an early warning system. Organizations can proactively identify and address account compromises, preventing data breaches, system disruptions, and reputational damage. Additionally, it helps uncover unauthorized account sharing, enforcing security policies.
Proactive security measures are crucial for safeguarding against compromised credentials and account takeovers. Before implementing detection controls in Google SecOps, consider these preventative steps:
Google Workspace
Google Cloud Platform
The Impossible Travel YARA-L Rule as viewed in SecOps SOAR
events:
$e1.metadata.log_type = "WORKSPACE_ACTIVITY"
$e1.metadata.event_type = "USER_LOGIN"
$e1.metadata.product_event_type = "login_success"
// match variables
$user = $e1.extracted.fields["actor.email"]
$e1_lat = $e1.principal.location.region_coordinates.latitude
$e1_long = $e1.principal.location.region_coordinates.longitude
The second part of our YARA-L rule, represented by the event variable $e2, mirrors the logic applied to the first event ($e1), and filters for the same type of authentication events.
$e2.metadata.log_type = "WORKSPACE_ACTIVITY"
$e2.metadata.event_type = "USER_LOGIN"
$e2.metadata.product_event_type = "login_success"
// match variables
$user = $e2.extracted.fields["actor.email"]
$e2_lat = $e2.principal.location.region_coordinates.latitude
$e2_long = $e2.principal.location.region_coordinates.longitude
With both login events captured and filtered, we're now ready to calculate the time and distance between them to assess the possibility of impossible travel.
To detect impossible travel, it's crucial to ensure we're comparing two distinct logon events from different geographical locations. To achieve this, we incorporate two key checks in our YARA-L rule:
// ensure consistent event sequencing, i.e., $e1 is before $e2
$e1.metadata.event_timestamp.seconds < $e2.metadata.event_timestamp.seconds
// check the $e1 and $e2 coordinates represent different locations
$e1_lat != $e2_lat
$e1_long != $e2_long
The match criteria in a YARA-L rule serves a similar purpose to a GROUP BY clause in SQL, allowing us to group related events together for further analysis.
In our impossible travel detection scenario, we group successful logon events ($e1 and $e2) that belong to the same user ($user) within a 1-day window. This time frame allows for legitimate travel while still flagging suspicious activity from vastly different locations occurring within a short period.
Crucially, we include the coordinates from both events ($e1_lat, $e1_long, $e2_lat, $e2_long) in the match statement. This ensures we analyze distinct logins from different locations, preventing false negatives. For instance, if a user logs in from Alaska (e1), then Antarctica (e2), and later again from Alaska (e3), grouping solely on the $user could lead us to compare the two Alaskan logins and miss the suspicious activity from Antarctica.
match:
$user,
$e1_lat,
$e1_long,
$e2_lat,
$e2_long
over 1d
By carefully crafting the match criteria, we ensure that our rule effectively groups relevant events, enabling us to focus on identifying genuine impossible travel patterns indicative of potential security threats.
Now that we've captured two geographically distinct login events from the same user, it's time to analyze the time and distance between them to assess the likelihood of impossible travel. The outcome section of our YARA-L rule handles this analysis, and helps prepare the results for analysts' investigation in SecOps SOAR.
By subtracting the timestamps of the two events we can create the interval in seconds, and convert to hours by dividing by 3600.
// calculate the interval between first and last event, in seconds
$duration_hours = cast.as_int(
min(
($e2.metadata.event_timestamp.seconds - $e1.metadata.event_timestamp.seconds)
/ 3600
)
)
This time interval will play a crucial role in our subsequent calculations, as we'll combine it with the distance between the two locations to determine if the travel was realistically possible within that time frame.
To determine if the travel between the two login events is feasible, we need to first calculate the distance separating them. The math.geo_distance YARA-L function in Google SIEM is ideal for this purpose, taking two coordinates parameters and returning the difference in meters:
// calculate distance between login events, and convert results into kilometers
// - math.ceil rounds up a float up to the nearest int
$distance_kilometers = math.ceil(
max(
math.geo_distance(
$e1_long,
$e1_lat,
$e2_long,
$e2_lat
)
)
// convert the math.geo_distance result from meters to kilometers
/ 1000
)
The math.geo_distance function returns the distance as a float value, which can include many decimal places (e.g., 15546.629971089747). However, we don’t need that level of precision, and so the math.ceil function is used to round up the float value up to the nearest integer (e.g., 15547).
_______________________________________________________________________________________________________________________
Tip: If you are familiar with GeoSpatial Analytics then the math.geo_distance is analogous to the SQL ST_DISTANCE function with the Spheroid parameter set to False.
/* Verify the results of the YARA-L Impossible Travel rule using SQL in GCP BigQuery */
SELECT
ST_DISTANCE(
-- ST_GEOGPOINT(longitude, latitude)
ST_GEOGPOINT(-99.9018131, 31.968598800000002),
ST_GEOGPOINT(103.819836, 1.352083),
false -- If use_spheroid is FALSE, the function measures distance on the surface of a perfect spher
)
-- https://cloud.google.com/bigquery/docs/reference/standard-sql/geography_functions#st_geogpoint
-- https://cloud.google.com/bigquery/docs/reference/standard-sql/geography_functions#st_distance
_______________________________________________________________________________________________________________________
Now that we have the distance in kilometers ($distance_kilometers) and the time interval in hours ($duration_hours), we can calculate the speed at which the user would have had to travel to physically login in both locations.
// calculate the speed in KPH
$kph = math.ceil($distance_kilometers / $duration_hours)
To complete the Outcome section of the impossible travel detection rule we'll use a series of conditional statements to assign a $risk_score based on how fast the travel appears to be.
// // generate risk_score based on KPH, i.e., speed over distance travelled
$risk_score = (
if($kph >= 100 and $kph <= 249, 35) +
if($kph > 250 and $kph <= 449, 50) +
if($kph > 500 and $kph <= 999, 75) +
if($kph >= 1000, 90)
)
This code assigns progressively higher risk scores to faster speeds. For instance, speeds between 100 and 249 KPH (typical for high-speed trains or short flights) receive a moderate risk score of 35. In contrast, speeds exceeding 1000 KPH, which are far beyond the capabilities of conventional travel, warrant a high risk score of 90.
Each if statement in the risk score calculation includes both lower and upper boundaries to ensure that the final $risk_score remains within the 0-100 range. Without these upper boundaries, the risk score could become cumulative, potentially exceeding 100 and making it difficult to interpret the severity of the alert.
The specific risk score values used in the rule are somewhat subjective and can be adjusted based on your organization's risk tolerance and typical travel patterns.
The $risk_score we've calculated plays a pivotal role in determining whether our rule triggers an alert. We introduce a configurable $risk_score_threshold to define the level of risk that warrants further investigation.
// change this according
$risk_score_threshold = 90
In this case, we've set the threshold to 90, meaning any pair of login events with a calculated speed of 1000 KPH or higher will trigger an alert.
Finally, we define the condition statement that ties everything together:
condition:
$e1 and $e2 and $risk_score >= $risk_score_threshold
This condition states that an alert will be generated only if:
When these conditions are met, the rule will not only trigger an alert but also create a Case in SecOps SOAR, providing security analysts with the necessary context to investigate the potential impossible travel incident further, which we shall explore further in Part 2 of this series.
Example results of a YARA-L Impossible Travel Detection Alert in Chronicle SIEM
For reference, here is the Impossible Travel YARA-L rule in its entirety. Please note, this is an example, and should be updated to match your environment accordingly.
rule suspicious_auth_unusual_interval_time {
meta:
author = "@Google SecOps Community"
description = "Generates a detection for authentication activity occuring between two locations in an unusual interval of time."
severity = "LOW"
priority = "LOW"
events:
$e1.metadata.log_type = "WORKSPACE_ACTIVITY"
$e1.metadata.event_type = "USER_LOGIN"
$e1.metadata.product_event_type = "login_success"
// match variables
$user = $e1.extracted.fields["actor.email"]
$e1_lat = $e1.principal.location.region_coordinates.latitude
$e1_long = $e1.principal.location.region_coordinates.longitude
// ensure consistent event sequencing, i.e., $e1 is before $e2
$e1.metadata.event_timestamp.seconds < $e2.metadata.event_timestamp.seconds
// check the $e1 and $e2 coordinates represent different locations
$e1_lat != $e2_lat
$e1_long != $e2_long
$e2.metadata.log_type = "WORKSPACE_ACTIVITY"
$e2.metadata.event_type = "USER_LOGIN"
$e2.metadata.product_event_type = "login_success"
// match variables
$user = $e2.extracted.fields["actor.email"]
$e2_lat = $e2.principal.location.region_coordinates.latitude
$e2_long = $e2.principal.location.region_coordinates.longitude
match:
$user,
$e1_lat,
$e1_long,
$e2_lat,
$e2_long
over 1d
outcome:
// calculate the interval between first and last event, in seconds
$duration_hours = cast.as_int(
min(
($e2.metadata.event_timestamp.seconds - $e1.metadata.event_timestamp.seconds)
/ 3600
)
)
// calculate distance between login events, and convert results into kilometers
// - math.ceil rounds up a float up to the nearest int
$distance_kilometers = math.ceil(
max(
math.geo_distance(
$e1_long,
$e1_lat,
$e2_long,
$e2_lat
)
)
// convert the math.geo_distance result from meters to kilometers
/ 1000
)
// calculate the speed in KPH
$kph = math.ceil($distance_kilometers / $duration_hours)
// // generate risk_score based on KPH, i.e., speed over distance travelled
$risk_score = (
if($kph >= 100 and $kph <= 249, 35) +
if($kph > 250 and $kph <= 449, 50) +
if($kph > 500 and $kph <= 999, 75) +
if($kph >= 1000, 90)
)
// change this according to your requirements
$risk_score_threshold = 90
condition:
$e1 and $e2 and $risk_score >= $risk_score_threshold
}