|
| 1 | +name: Geographic Improbable Location |
| 2 | +id: 64f91df1-49ec-46aa-81bd-2282d3cea765 |
| 3 | +version: 1 |
| 4 | +date: '2025-06-03' |
| 5 | +author: Marissa Bower, Raven Tait |
| 6 | +status: experimental |
| 7 | +type: Anomaly |
| 8 | +description: Geolocation data can be inaccurate or easily spoofed by Remote Employment Fraud (REF) workers. |
| 9 | + REF actors sometimes slip up and reveal their true location, creating what we call 'improbable travel' |
| 10 | + scenarios — logins from opposite sides of the world within minutes. This identifies situations where these |
| 11 | + travel scenarios occur. |
| 12 | +data_source: |
| 13 | +- Okta |
| 14 | +search: '| tstats summariesonly=true values(Authentication.app) as app from datamodel=Authentication.Authentication |
| 15 | + where (`okta` OR (index="firewall" AND sourcetype="pan:globalprotect")) |
| 16 | + AND Authentication.action="success" AND Authentication.app IN ("Workday", "Slack", "*GlobalProtect", "Jira*", |
| 17 | + "Atlassian Cloud", "Zoom") AND NOT Authentication.user="unknown" by _time index sourcetype host Authentication.user |
| 18 | + Authentication.src span=1s |
| 19 | + | `drop_dm_object_name("Authentication")` |
| 20 | + | fields user,src,app,_time,count,host |
| 21 | + | eval user=lower(replace(user, "((^.*\\\)|(@.*$))", "")) |
| 22 | + | join type=outer user |
| 23 | + [| inputlookup identity_lookup_expanded where user_status=active |
| 24 | + | rex field=email "^(?<user>[a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$" |
| 25 | + | rename email as user_email bunit as user_bunit priority as user_priority work_country as user_work_country work_city as user_work_city |
| 26 | + | fields user user_email user_bunit user_priority user_work_country user_work_city] |
| 27 | + | eventstats dc(src) as src_count by user |
| 28 | + | eventstats dc(user) as user_count by src |
| 29 | + | sort 0 + _time |
| 30 | + | iplocation src |
| 31 | + | lookup local=true asn_lookup_by_cidr ip as src OUTPUT ip asn description |
| 32 | + | eval session_lat=if(isnull(src_lat), lat, src_lat), session_lon=if(isnull(src_long), lon, src_long), |
| 33 | + session_city=if(isnull(src_city), City, src_city), session_country=if(isnull(src_country), Country, src_country), |
| 34 | + session_region=if(isnull(src_region), Region, src_region) |
| 35 | + | eval session_city=if(isnull(session_city) OR match(session_city,"^\s+|^$"), null(), session_city), |
| 36 | + session_country=if(isnull(session_country) OR match(session_country,"^\s+|^$"), null(), session_country), |
| 37 | + session_region=if(isnull(session_region) OR match(session_region,"^\s+|^$"), null(), session_region) |
| 38 | + | where isnotnull(session_lat) and isnotnull(session_lon) |
| 39 | + | eval session_city=if(isnull(session_city),"-",session_city), session_country=if(isnull(session_country),"-",session_country), |
| 40 | + session_region=if(isnull(session_region),"-",session_region) |
| 41 | + | streamstats current=t window=2 earliest(session_region) as prev_region,earliest(session_lat) as prev_lat, |
| 42 | + earliest(session_lon) as prev_lon, earliest(session_city) as prev_city, earliest(session_country) as prev_country, |
| 43 | + earliest(_time) as prev_time, earliest(src) as prev_src, latest(user_bunit) as user_bunit, |
| 44 | + earliest(app) as prev_app values(user_work_country) as user_work_country by user |
| 45 | + | where (src!=prev_src) AND !(prev_city=session_city AND prev_country=session_country) AND ((isnotnull(prev_city) |
| 46 | + AND isnotnull(session_city)) OR prev_country!=session_country) |
| 47 | + | `globedistance(session_lat,session_lon,prev_lat,prev_lon,"m")` |
| 48 | + | eval time_diff=if((_time-prev_time)==0, 1, _time - prev_time) |
| 49 | + | eval speed = round(distance*3600/time_diff,2) |
| 50 | + | eval distance= round(distance,2) |
| 51 | + | eval user_work_country=case(user_work_country="usa","United States", user_work_country="cze","Czechia", |
| 52 | + user_work_country="pol","Poland", user_work_country="ind","India", user_work_country="fra","France", |
| 53 | + user_work_country="can","Canada", user_work_country="mys","Malaysia", user_work_country="kor","South Korea", |
| 54 | + user_work_country="aus","Australia", user_work_country="bel","Belgium", user_work_country="dnk","Denmark", |
| 55 | + user_work_country="bra","Brazil", user_work_country="deu","Germany", user_work_country="jpn","Japan", |
| 56 | + user_work_country="che","Switzerland", user_work_country="swe","Sweden", user_work_country="zaf","South Africa", |
| 57 | + user_work_country="irl","Ireland", user_work_country="ita","Italy", user_work_country="nor","Norway", |
| 58 | + user_work_country="gbr","United Kingdom", user_work_country="hkg","Hong Kong", user_work_country="chn","China", |
| 59 | + user_work_country="esp","Spain", user_work_country="nld", "Netherlands", user_work_country="twn","Taiwan", |
| 60 | + user_work_country="est","Estonia", user_work_country="sgp","Singapore", user_work_country="are","United Arab Emirates", 1=1,"N/A") |
| 61 | + | lookup local=true asn_lookup_by_cidr ip as prev_src OUTPUT ip as prev_ip asn as prev_asn description as prev_description |
| 62 | + | eval suspect=if(!user_work_country==session_country,"Sketchy","Normal") |
| 63 | + | search (speed>500 AND distance>750) |
| 64 | + | table _time,prev_time,user,host,src,prev_src,app,prev_app,distance,speed,suspect,session_city,session_region, |
| 65 | + session_country,prev_city,prev_region,prev_country,user_priority,user_work_*,prev_ip,ip,asn,prev_asn,prev_description,description |
| 66 | + | rename _time as event_time |
| 67 | + | convert ctime(event_time) timeformat="%Y-%m-%d %H:%M:%S" |
| 68 | + | convert ctime(prev_time) timeformat="%Y-%m-%d %H:%M:%S" |
| 69 | + | eval problem=if(!session_country==prev_country AND (!session_country==user_work_country),"Yes","Nope") |
| 70 | + | search NOT (prev_city="-" OR session_city="-") AND NOT |
| 71 | + [inputlookup known_devices_public_ip_filter.csv |
| 72 | + | fields ip |
| 73 | + | rename ip as src] |
| 74 | + | dedup user host prev_src src |
| 75 | + | fillnull value="N/A" |
| 76 | + | search problem="Yes"| `geographic_improbable_location_filter`' |
| 77 | +how_to_implement: The analytic leverages Okta OktaIm2 logs to be ingested using the |
| 78 | + Splunk Add-on for Okta Identity Cloud (https://splunkbase.splunk.com/app/6553). This also utilizes |
| 79 | + Splunk Enterprise Security Suite for several macros and lookups. The known_devices_public_ip_filter |
| 80 | + lookup is a placeholder for known public edge devices in your network. |
| 81 | +known_false_positives: Legitimate usage of some VPNs may cause false positives. Tune as needed. |
| 82 | +drilldown_searches: |
| 83 | +- name: View the detection results for - "$user$" |
| 84 | + search: '%original_detection_search% | search Authentication.user = "$user$"' |
| 85 | + earliest_offset: $info_min_time$ |
| 86 | + latest_offset: $info_max_time$ |
| 87 | +- name: View risk events for the last 7 days for - "$user$" |
| 88 | + search: '| from datamodel Risk.All_Risk | search normalized_risk_object IN ("$user$") |
| 89 | + starthoursago=168 | stats count min(_time) as firstTime max(_time) as lastTime |
| 90 | + values(search_name) as "Search Name" values(risk_message) as "Risk Message" values(analyticstories) |
| 91 | + as "Analytic Stories" values(annotations._all) as "Annotations" values(annotations.mitre_attack.mitre_tactic) |
| 92 | + as "ATT&CK Tactics" by normalized_risk_object | `security_content_ctime(firstTime)` |
| 93 | + | `security_content_ctime(lastTime)`' |
| 94 | + earliest_offset: $info_min_time$ |
| 95 | + latest_offset: $info_max_time$ |
| 96 | +rba: |
| 97 | + message: Improbable travel speed between locations observed for $user$. |
| 98 | + risk_objects: |
| 99 | + - field: user |
| 100 | + type: user |
| 101 | + score: 50 |
| 102 | + threat_objects: [] |
| 103 | +tags: |
| 104 | + analytic_story: |
| 105 | + - Remote Employment Fraud |
| 106 | + asset_type: Identity |
| 107 | + mitre_attack_id: |
| 108 | + - T1078 |
| 109 | + product: |
| 110 | + - Splunk Enterprise |
| 111 | + - Splunk Enterprise Security |
| 112 | + - Splunk Cloud |
| 113 | + security_domain: identity |
0 commit comments