Skip to main content

So I have this code written to detect a landspeed anomaly


rule  impossible_travel_activity{

  meta:
    author = "Anurag Singh"
    description = "Detects potential account compromise by identifying logon attempts from two different geo locations within a short span of time, indicating impossible travel between the locations."
    severity = "High"

  events:
    $e1.metadata.event_type = "USER_LOGIN"
    $e1.metadata.product_event_type = "UserLoggedIn"
    $user = $e1.target.user.userid
    $e1_lat = $e1.principal.location.region_coordinates.latitude
    $e1_long = $e1.principal.location.region_coordinates.longitude
    $location1 = $e1.principal.ip_geo_artifact.location.country_or_region

    $e2.metadata.event_type = "USER_LOGIN"    
    $e2.metadata.product_event_type = "UserLoggedIn"
    // match variables
    $user = $e2.target.user.userid
    $e2_lat = $e2.principal.location.region_coordinates.latitude
    $e2_long = $e2.principal.location.region_coordinates.longitude
    $location2 = $e2.principal.ip_geo_artifact.location.country_or_region



  match:
    $user over 1h

  outcome:
    $distance_kilometers = math.ceil(
            max(math.geo_distance($e1_long, $e1_lat, $e2_long,$e2_lat)) / 1000
    )


    $risk_score = (
        if($e1.principal.ip_geo_artifact.location.country_or_region != $e2.principal.ip_geo_artifact.location.country_or_region nocase, 90) +
        if($distance_kilometers > 100 and $distance_kilometers <= 500, 20) +
        if($distance_kilometers > 500 and $distance_kilometers <= 1000, 30) +
        if($distance_kilometers > 1000, 50)
    )          


  condition:
        $e1 and $e2 and $risk_score >= 20
}



But the part where I am assigning the risk score is showing an error which says 
validating intermediate representation: repeated values in outcome assignment must be aggregated
Can someone help me on this?

Sure. Anytime you have an outcome variable in a search or rule with a match section, the events are aggregated or grouped. Because of that fields in the outcome section must also be aggregated. Risk scores that are derived from fields must also be aggregated. A function like max, count, count_distinct, array, array_distinct and many more are good ways to aggregate the value.  


Sure. Anytime you have an outcome variable in a search or rule with a match section, the events are aggregated or grouped. Because of that fields in the outcome section must also be aggregated. Risk scores that are derived from fields must also be aggregated. A function like max, count, count_distinct, array, array_distinct and many more are good ways to aggregate the value.  


So I just want to check if the parameters 
$e1.principal.ip_geo_artifact.location.country_or_region  and  $e2.principal.ip_geo_artifact.location.country_or_region nocase are equal or not and assign risk score on the basis of that. how to do that?

 



Sorry for the overly quick response I saw the UDM fields on my phone and didn't see the rest. OK, so the guidance about needing an aggregation function for the UDM fields still stands so we do need to encapsulate that portion of the risk calculation with the max function. However, the remainder of the mathematical operation was already aggregated in the previous statement to calculate the distance, so we can then just use mathematical operations with the conditional statement, like below. I think this should work.

 

$risk_score = max(
if($e1.principal.ip_geo_artifact.location.country_or_region = $e2.principal.ip_geo_artifact.location.country_or_region nocase, 90)) +
if($distance_kilometers > 100 and $distance_kilometers <= 500, 20) +
if($distance_kilometers > 500 and $distance_kilometers <= 1000, 30) +
if($distance_kilometers > 1000, 50)

 



Thanks for the quick support


As per my understanding,

 

$risk_score = max(
if($e1.principal.ip_geo_artifact.location.country_or_region = $e2.principal.ip_geo_artifact.location.country_or_region nocase, 90)) +
if($distance_kilometers > 100 and $distance_kilometers <= 500, 20) +
if($distance_kilometers > 500 and $distance_kilometers <= 1000, 30) +
if($distance_kilometers > 1000, 50)

This piece of code will assign the risk score as 90 if both the locations are same. and if not then 20, 30 or 50 based on the condition.

correct me if I am wrong.


That is how I interpreted it but you may want a nocase on both events to ensure those values are the same. Actually a strings.to_upper or strings.to_lower would be better for a comparison but tune it as you see fit. If the first if is not met, you are assigning a 0 and because the next 3 values that can be summed together do not cover a distance of less than 100, you are opening yourself to having a 0 value in there as well. 


yes I don't want the event to trigger for a distance of less than 100 km

rule  impossible_travel_activity_piramal{

  meta:
    author = "Anurag Singh"
    description = "Detects potential account compromise by identifying logon attempts from two different geo locations within a short span of time, indicating impossible travel between the locations."
    severity = "High"

  events:
    $e1.metadata.event_type = "USER_LOGIN"
    $e1.metadata.product_event_type = "UserLoggedIn"
    $user = $e1.target.user.userid
    $e1_lat = $e1.principal.location.region_coordinates.latitude
    $e1_long = $e1.principal.location.region_coordinates.longitude
    $location1 = $e1.principal.ip_geo_artifact.location.country_or_region

    $e2.metadata.event_type = "USER_LOGIN"    
    $e2.metadata.product_event_type = "UserLoggedIn"
    // match variables
    $user = $e2.target.user.userid
    $e2_lat = $e2.principal.location.region_coordinates.latitude
    $e2_long = $e2.principal.location.region_coordinates.longitude
    $location2 = $e2.principal.ip_geo_artifact.location.country_or_region



  match:
    $user over 1h

  outcome:
    $distance_kilometers = math.ceil(
            max(math.geo_distance($e1_long, $e1_lat, $e2_long,$e2_lat)) / 1000
    )


    $risk_score = max(
        if(strings.to_lower($location1) != strings.to_lower($location2), 90)) +
        if($distance_kilometers > 100 and $distance_kilometers <= 500, 20) +
        if($distance_kilometers > 500 and $distance_kilometers <= 1000, 30) +
        if($distance_kilometers > 1000, 50)        
   

  condition:
        $e1 and $e2 and $risk_score >= 20
}


This is my updated code.
Any suggestions are welcomed. 
Thanks for your help. 

On the surface it looks generally fine, are you getting results or do you have any issues? My one comment is that as I look at the risk score that first statement location 1 and 2 not being the same (different country/region) could be less than 100 km away and you still end up with a risk score of 90 due to that inequality.


So, in my case, if I am in the US and cross the bridge to Canada and login, this rule will trigger with a risk of 90 even though I am under 100km. Similar issue in the EU for instance. Again, it's not a bad thing but I do want to mention that as you test and refine.


yes I don't want the event to trigger for a distance of less than 100 km

rule  impossible_travel_activity_piramal{

  meta:
    author = "Anurag Singh"
    description = "Detects potential account compromise by identifying logon attempts from two different geo locations within a short span of time, indicating impossible travel between the locations."
    severity = "High"

  events:
    $e1.metadata.event_type = "USER_LOGIN"
    $e1.metadata.product_event_type = "UserLoggedIn"
    $user = $e1.target.user.userid
    $e1_lat = $e1.principal.location.region_coordinates.latitude
    $e1_long = $e1.principal.location.region_coordinates.longitude
    $location1 = $e1.principal.ip_geo_artifact.location.country_or_region

    $e2.metadata.event_type = "USER_LOGIN"    
    $e2.metadata.product_event_type = "UserLoggedIn"
    // match variables
    $user = $e2.target.user.userid
    $e2_lat = $e2.principal.location.region_coordinates.latitude
    $e2_long = $e2.principal.location.region_coordinates.longitude
    $location2 = $e2.principal.ip_geo_artifact.location.country_or_region



  match:
    $user over 1h

  outcome:
    $distance_kilometers = math.ceil(
            max(math.geo_distance($e1_long, $e1_lat, $e2_long,$e2_lat)) / 1000
    )


    $risk_score = max(
        if(strings.to_lower($location1) != strings.to_lower($location2), 90)) +
        if($distance_kilometers > 100 and $distance_kilometers <= 500, 20) +
        if($distance_kilometers > 500 and $distance_kilometers <= 1000, 30) +
        if($distance_kilometers > 1000, 50)        
   

  condition:
        $e1 and $e2 and $risk_score >= 20
}


This is my updated code.
Any suggestions are welcomed. 
Thanks for your help. 

Hi @anurag.q.singh 

How about the below

rule impossible_travel_activity_piramal{

meta:
author = "Anurag Singh"
description = "Detects potential account compromise by identifying logon attempts from two different geo locations within a short span of time, indicating impossible travel between the locations."
severity = "High"

events:
$e1.metadata.event_type = "USER_LOGIN"
$e1.metadata.product_event_type = "UserLoggedIn"
$user = $e1.target.user.userid
$e1_lat = $e1.principal.location.region_coordinates.latitude
$e1_long = $e1.principal.location.region_coordinates.longitude
$location1 = $e1.principal.ip_geo_artifact.location.country_or_region

$e2.metadata.event_type = "USER_LOGIN"
$e2.metadata.product_event_type = "UserLoggedIn"
// match variables
$user = $e2.target.user.userid
$e2_lat = $e2.principal.location.region_coordinates.latitude
$e2_long = $e2.principal.location.region_coordinates.longitude
$location2 = $e2.principal.ip_geo_artifact.location.country_or_region



match:
$user over 1h

outcome:
$distance_kilometers = math.ceil(
max(math.geo_distance($e1_long, $e1_lat, $e2_long,$e2_lat)) / 1000
)


$risk_score = max(
if(strings.to_lower($location1) != strings.to_lower($location2), 90)) +
if($distance_kilometers > 100 and $distance_kilometers <= 500, 20) +
if($distance_kilometers > 500 and $distance_kilometers <= 1000, 30) +
if($distance_kilometers > 1000, 50)


condition:
$e1 and $e2 and $risk_score >= 20 and not $distance_kilometers <= 100
}


Kind Regards,


Ayman Charkaui


rule  impossible_travel_activity {

 

  meta:
    author = "Anurag Singh"
    description = "Detects potential account compromise by identifying logon attempts from two different geo locations within a short span of time, indicating impossible travel between the locations."
    severity = "High"

 

  events:
    $e1.metadata.event_type = "USER_LOGIN"
    $e1.metadata.product_event_type = "UserLoggedIn"
    $user = $e1.target.user.userid
    $e1_lat = $e1.principal.location.region_coordinates.latitude
    $e1_long = $e1.principal.location.region_coordinates.longitude
    $location1 = $e1.principal.ip_geo_artifact.location.country_or_region



    $e2.metadata.event_type = "USER_LOGIN"    
    $e2.metadata.product_event_type = "UserLoggedIn"
    $user = $e2.target.user.userid
    $e2_lat = $e2.principal.location.region_coordinates.latitude
    $e2_long = $e2.principal.location.region_coordinates.longitude
    $location2 = $e2.principal.ip_geo_artifact.location.country_or_region




  match:
    $user over 1h

 

  outcome:
    $distance_kilometers = math.ceil(
            max(math.geo_distance($e1_long, $e1_lat, $e2_long,$e2_lat)) / 1000
    )

 

    $duration = cast.as_int(
            min(
                ($e2.metadata.event_timestamp.seconds - $e1.metadata.event_timestamp.seconds)
                / 60
            )
        )



    $risk_score = max(
        if(strings.to_lower($location1) != strings.to_lower($location2), 90)) +    // if logging in from different countries
        if($distance_kilometers > 100 and $distance_kilometers <= 500, 20) +
        if($distance_kilometers > 500 and $distance_kilometers <= 1000, 30) +
        if($distance_kilometers > 1000, 50)    

 

    $impossibleTravelActivity_description = array_distinct(strings.concat("The user '", $user, "' logged in from two locations with a distance of '", $distance_kilometers, "' km within a duration of '", $duration , "minutes"))
   

 

  condition:
        $e1 and $e2 and $risk_score >= 20 and not $distance_kilometers <= 100
}


now the issue I an facing is that $impossibleTravelActivity_description is showing an error 

validating intermediate representation: aggregation cannot refer to outcome variables or contain another aggregation
line: 48
column: 45-225

any suggestions? 
@jstoner  @AymanC 

rule  impossible_travel_activity {

 

  meta:
    author = "Anurag Singh"
    description = "Detects potential account compromise by identifying logon attempts from two different geo locations within a short span of time, indicating impossible travel between the locations."
    severity = "High"

 

  events:
    $e1.metadata.event_type = "USER_LOGIN"
    $e1.metadata.product_event_type = "UserLoggedIn"
    $user = $e1.target.user.userid
    $e1_lat = $e1.principal.location.region_coordinates.latitude
    $e1_long = $e1.principal.location.region_coordinates.longitude
    $location1 = $e1.principal.ip_geo_artifact.location.country_or_region



    $e2.metadata.event_type = "USER_LOGIN"    
    $e2.metadata.product_event_type = "UserLoggedIn"
    $user = $e2.target.user.userid
    $e2_lat = $e2.principal.location.region_coordinates.latitude
    $e2_long = $e2.principal.location.region_coordinates.longitude
    $location2 = $e2.principal.ip_geo_artifact.location.country_or_region




  match:
    $user over 1h

 

  outcome:
    $distance_kilometers = math.ceil(
            max(math.geo_distance($e1_long, $e1_lat, $e2_long,$e2_lat)) / 1000
    )

 

    $duration = cast.as_int(
            min(
                ($e2.metadata.event_timestamp.seconds - $e1.metadata.event_timestamp.seconds)
                / 60
            )
        )



    $risk_score = max(
        if(strings.to_lower($location1) != strings.to_lower($location2), 90)) +    // if logging in from different countries
        if($distance_kilometers > 100 and $distance_kilometers <= 500, 20) +
        if($distance_kilometers > 500 and $distance_kilometers <= 1000, 30) +
        if($distance_kilometers > 1000, 50)    

 

    $impossibleTravelActivity_description = array_distinct(strings.concat("The user '", $user, "' logged in from two locations with a distance of '", $distance_kilometers, "' km within a duration of '", $duration , "minutes"))
   

 

  condition:
        $e1 and $e2 and $risk_score >= 20 and not $distance_kilometers <= 100
}


now the issue I an facing is that $impossibleTravelActivity_description is showing an error 

validating intermediate representation: aggregation cannot refer to outcome variables or contain another aggregation
line: 48
column: 45-225

any suggestions? 
@jstoner  @AymanC 

Hi @anurag.q.singh 

The reason why, is within your outcome variable 'impossibleTravelActivity_description, you're signifying a 'array_distinct', and then using the variables 'distance_kilometers', and 'duration' which ar using max and mins. However the below should satisfy your use case.

rule impossible_travel_activity {


meta:
author = "Anurag Singh"
description = "Detects potential account compromise by identifying logon attempts from two different geo locations within a short span of time, indicating impossible travel between the locations."
severity = "High"


events:
$e1.metadata.event_type = "USER_LOGIN"
$e1.metadata.product_event_type = "UserLoggedIn"
$user = $e1.target.user.userid
$e1_lat = $e1.principal.location.region_coordinates.latitude
$e1_long = $e1.principal.location.region_coordinates.longitude
$location1 = $e1.principal.ip_geo_artifact.location.country_or_region



$e2.metadata.event_type = "USER_LOGIN"
$e2.metadata.product_event_type = "UserLoggedIn"
$user = $e2.target.user.userid
$e2_lat = $e2.principal.location.region_coordinates.latitude
$e2_long = $e2.principal.location.region_coordinates.longitude
$location2 = $e2.principal.ip_geo_artifact.location.country_or_region





match:
$user over 1h


outcome:
$distance_kilometers = math.ceil(
max(math.geo_distance($e1_long, $e1_lat, $e2_long,$e2_lat)) / 1000
)


$duration = cast.as_int(
min(
($e2.metadata.event_timestamp.seconds - $e1.metadata.event_timestamp.seconds)
/ 60
)
)



$risk_score = max(
if(strings.to_lower($location1) != strings.to_lower($location2), 90)) + // if logging in from different countries
if($distance_kilometers > 100 and $distance_kilometers <= 500, 20) +
if($distance_kilometers > 500 and $distance_kilometers <= 1000, 30) +
if($distance_kilometers > 1000, 50)


$impossibleTravelActivity_description = array_distinct(strings.concat("The user '", $user, "' logged in from two locations with a distance of '", math.ceil(math.geo_distance($e1_long, $e1_lat, $e2_long,$e2_lat) / 1000), " km within a duration of ", cast.as_int(($e2.metadata.event_timestamp.seconds - $e1.metadata.event_timestamp.seconds)
/ 60), " minutes"))


condition:
$e1 and $e2 and $risk_score >= 20 and not $distance_kilometers <= 100
}




There is one more possibility. What if $e1.metadata.event_timestamp.seconds occured after $e2.metadata.event_timestamp.seconds ?

won't it cause issue? 
is there any mod function which I can use?


Hi @anurag.q.singh 

If you only $e1 to be a timestamp before $e2, then you would add something like the below

  $e2.metadata.event_timestamp.seconds > $e1.metadata.event_timestamp.seconds

Kind Regards,

Ayman Charkaui


Hi @anurag.q.singh 

If you only $e1 to be a timestamp before $e2, then you would add something like the below

  $e2.metadata.event_timestamp.seconds > $e1.metadata.event_timestamp.seconds

Kind Regards,

Ayman Charkaui


$impossibleTravelActivity_description = array_distinct(strings.concat("The user '", $user, "' logged in from two locations with a distance of '", math.ceil(math.geo_distance($e1_long, $e1_lat, $e2_long,$e2_lat) / 1000), " km within a duration of ", if($e2.metadata.event_timestamp.seconds > $e1.metadata.event_timestamp.seconds , cast.as_int(($e2.metadata.event_timestamp.seconds - $e1.metadata.event_timestamp.seconds)
/ 60) , cast.as_int(($e1.metadata.event_timestamp.seconds - $e2.metadata.event_timestamp.seconds)
/ 60) ) , " minutes"))


something like this? 
I guess I have made some error which is leading to a warning saying that 

parsing: Only placeholders, event fields, and constants are allowed in then clause

 


$impossibleTravelActivity_description = array_distinct(strings.concat("The user '", $user, "' logged in from two locations with a distance of '", math.ceil(math.geo_distance($e1_long, $e1_lat, $e2_long,$e2_lat) / 1000), " km within a duration of ", if($e2.metadata.event_timestamp.seconds > $e1.metadata.event_timestamp.seconds , cast.as_int(($e2.metadata.event_timestamp.seconds - $e1.metadata.event_timestamp.seconds)
/ 60) , cast.as_int(($e1.metadata.event_timestamp.seconds - $e2.metadata.event_timestamp.seconds)
/ 60) ) , " minutes"))


something like this? 
I guess I have made some error which is leading to a warning saying that 

parsing: Only placeholders, event fields, and constants are allowed in then clause

 


Hi @anurag.q.singh 

If you categorically don't want the rule to fire, unless $e2 occurs after $e1, then the below solution will work.

 

rule impossible_travel_activity {


meta:
author = "Anurag Singh"
description = "Detects potential account compromise by identifying logon attempts from two different geo locations within a short span of time, indicating impossible travel between the locations."
severity = "High"


events:
$e1.metadata.event_type = "USER_LOGIN"
$e1.metadata.product_event_type = "UserLoggedIn"
$user = $e1.target.user.userid
$e1_lat = $e1.principal.location.region_coordinates.latitude
$e1_long = $e1.principal.location.region_coordinates.longitude
$location1 = $e1.principal.ip_geo_artifact.location.country_or_region



$e2.metadata.event_type = "USER_LOGIN"
$e2.metadata.product_event_type = "UserLoggedIn"
$user = $e2.target.user.userid
$e2_lat = $e2.principal.location.region_coordinates.latitude
$e2_long = $e2.principal.location.region_coordinates.longitude
$location2 = $e2.principal.ip_geo_artifact.location.country_or_region

$e2.metadata.event_timestamp.seconds > $e1.metadata.event_timestamp.seconds




match:
$user over 1h


outcome:
$distance_kilometers = math.ceil(
max(math.geo_distance($e1_long, $e1_lat, $e2_long,$e2_lat)) / 1000
)


$duration = cast.as_int(
min(
($e2.metadata.event_timestamp.seconds - $e1.metadata.event_timestamp.seconds)
/ 60
)
)



$risk_score = max(
if(strings.to_lower($location1) != strings.to_lower($location2), 90)) + // if logging in from different countries
if($distance_kilometers > 100 and $distance_kilometers <= 500, 20) +
if($distance_kilometers > 500 and $distance_kilometers <= 1000, 30) +
if($distance_kilometers > 1000, 50)


$impossibleTravelActivity_description = array_distinct(strings.concat("The user '", $user, "' logged in from two locations with a distance of '", math.ceil(math.geo_distance($e1_long, $e1_lat, $e2_long,$e2_lat) / 1000), " km within a duration of ", cast.as_int(($e2.metadata.event_timestamp.seconds - $e1.metadata.event_timestamp.seconds)
/ 60), " minutes"))


condition:
$e1 and $e2 and $risk_score >= 20 and not $distance_kilometers <= 100
}

Kind Regards,

Ayman Charkaui

 


Reply