Skip to main content

Introduction


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.


What is Impossible Travel?


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.


Prevention & Mitigation


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



  • Suspicious Login Detection: Leverages machine learning to identify and block login attempts from unusual locations or devices, serving as an initial line of defense.


Google Cloud Platform



Building the Impossible Travel Detection Rule 



The Impossible Travel YARA-L Rule as viewed in SecOps SOAR


Capturing the First Login Event



  • Event Variable: We start by creating a placeholder called $e1 to store information about the first login event.

  • Event Filtering: The rule then filters for specific authentication events. In this example, we're using Google Workspace as the Identity Provider (IdP). You'll need to adjust these values to match your environment's IdP.

  • Match Variables:  We create variables to store the user's email ($user) and the latitude and longitude coordinates of the first login ($e1_lat and $e1_long).


 


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

 


Capturing the Second Login Event


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.


Ensure Distinct Locations


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

 



  1. Event Sequencing: The first line checks that the timestamp of the first event ($e1) is earlier than the second event ($e2). This establishes a clear timeline and prevents the rule from accidentally matching the same event twice (a self-join).

  2. Distinct Locations: The next two lines compare the latitude and longitude coordinates of the two events ($e1_lat,$e1_long,$e2_lat, and $e2_long). By ensuring they are not equal, we guarantee that the logins originated from different geographical points.


Grouping Events


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.


Calculating Impossible with Outcomes


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.


Calculating Distance with math.geo_distance


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.


Setting the Alert Threshold and Finalizing the Rule


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:



  • Both events ($e1 and $e2) are present, meaning we have two successful logins from the same user

  • The calculated $risk_score is equal to or greater than the defined $risk_score_threshold


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


The Impossible Travel YARA-L Rule


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.fieldsd"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.fieldsd"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
}

 

Will this be a part of google's curated rule set that comes with part of the product in the future?


I tried to copy/paste this example into my environment, however I am getting Rules Editor errors that the cast.as_int(), math.geo_distance() and math.ceil() YARA-L functions are not found.


Hi @stein1


the new YL2 functions are rolling out at present, and I understand these should be available to all regions by the end of the week.  Apologies, I should have included that in the above.  The other note, the above example includes using the Dynamic Fields preview (extracted fields) and so unless on that preview program that would also generate an error.  


Best Regards,


Chris


Hi @nc2 ,


I've provided that feedback, but one thing to consider is that given this is a nuanced type of detection, and best targeted specific to your environment, it may not make for the best Curated Detection option as you will always need to customize it; however, there is a roadmap item that some Curated Detection logic will be made viewable at a future date, and then you would be able to copy and customize the rule as needed.  For right now, I would work on the basis it's best implemented as a custom rule.


Best Regards,


Chris


Curious if you have you ever run into a log source that did not use the ISO 6709 standard format for lat log or maybe combined latitude with longitude? If so what's your advice for  making sure math.geo_distance() inputs are formatted correctly


Hey fantastic example ! Can you share the html widget from the impossible travel summary and google maps ?

Regards,

George


Hi @gsec 


I'm working on getting Part 2 of the blog out for next week, and that includes all the information for the SOAR Playbook relating to this Alert.


Best Regards,


Chris


Hi @ScottieJ 


>> Curious if you have you ever run into a log source that did not use the ISO 6709 standard format for lat log or maybe combined latitude with longitude? If so what's your advice for making sure math.geo_distance() inputs are formatted correctly


The math.geo_distance requires 4 params of type float - https://cloud.google.com/chronicle/docs/preview/detection-engine/yara-l-2-0-functions/math-geo_distance, so using some of the other YL2 functions that have recently been released, and existing functions you could use regex.capture to extract coordinates as needed into placeholder variables, and then you could use cast.as_float before passing to math.geo_distance.


To-date I've only used this with the native coordinates as enriched by SecOps own GeoIP enrichment, but in theory the above should work.


Best Regards,


Chris


 


Chris,


I hope you will continue to entertain my questions as I hope they not only help me but others as well.


Question regarding match


The $user variable I assume is a transitive join with the email values from $e1 and $e2 and the match operation would only group entries where both values in the $user variable are equal? My initial thought was that this rule was missing a conditional statement like if($e1.user = $e2.user, $user=$e1.user). I wonder if a conditional statement would add efficiencies to the match operation?

Assumption regarding lat / long data:  I'm  assuming that as long as there is an external IP in the log, SecOps enrichment's should create lat / long entries in the "principal" or "src" or "observer" or "target" UDM fields?


@ScottieJ 


In YL2, as I understand it, you can a join a multi-event rule in multiple ways, so both:


$e1.x = $e2.x

and


$foo = $e1.udm.bar

$foo = $e2.udm.bar

are the same (see https://cloud.google.com/chronicle/docs/detection/yara-l-2-0-syntax for transitive joins for more info)


For consistency, in the example rule I have mixed both, but they achieve the same end result.


And yes, re: GeoIP enrichment that's my understanding.  As this rule targets Workspace Activity logs, and those always have an external IP, no additional checking is needed, but if changing the rule to other sources adding validation checking that the IP is not null, and that the coordinates are not 0 would be recommended (albeit opening up a small risk of an attack from https://en.wikipedia.org/wiki/Null_Island not being detected)


Best Regards,


Chris


 


https://cloud.google.com/chronicle/docs/detection/yara-l-2-0-syntax has a brief description of this but 


Great post! I have been looking for a solution to impossible travel with Chronicle. I ran into an issue when the login events were under one hour apart. I would get 0 for "$duration_hours" which would then give me 0 for "$kph" causing the alert not to trigger. To resolve this, I did the hour conversion after determining the speed.

// Calculate the time difference in seconds
$time_difference_seconds = min(

($e2.metadata.event_timestamp.seconds - $e1.metadata.event_timestamp.seconds)

)


// Calculate the distance between login events in meters
$distance_meters = math.ceil(
max(
math.geo_distance(
$e1_long,
$e1_lat,
$e2_long,
$e2_lat
)
)
)
// Calculate the speed in meters per second
$meters_per_second = $distance_meters / $time_difference_seconds

// Convert speed to kilometers per hour (KPH)
$kph = math.ceil($meters_per_second * 3.6)

// Convert KPH to MPH
$mph = math.ceil($kph * 0.621371)

// Convert the distance to kilometers
$distance_kilometers = $distance_meters / 1000

// Convert the kilometers to miles
$miles = math.ceil($distance_kilometers * 0.621371)

Hope this helps anyone else that might experience this. I added MPH as well.


Hi all, I know some of you were asking when the second part of the blog series related to SOAR would be available. If you haven't seen it already, check it out here. In this blog we'll take the next step and focus on building an associated SecOps SOAR Playbook to provide security analysts with an actionable alert. Enjoy!


Reply