diff --git a/rules/integrations/google_workspace/initial_access_google_workspace_login_impossible_travel.toml b/rules/integrations/google_workspace/initial_access_google_workspace_login_impossible_travel.toml new file mode 100644 index 00000000000..308fea53b00 --- /dev/null +++ b/rules/integrations/google_workspace/initial_access_google_workspace_login_impossible_travel.toml @@ -0,0 +1,169 @@ +[metadata] +creation_date = "2026/05/14" +integration = ["google_workspace"] +maturity = "production" +updated_date = "2026/05/14" + +[rule] +author = ["Elastic"] +description = """ +Detects successful Google Workspace sign-ins for the same user from two geographically separated locations within a +3-hour window, where the implied travel speed between the two points exceeds what is physically possible (>=800 km/h, +faster than modern commercial airliners) and the geographic separation is at least 500 km. This pattern indicates either +VPN/proxy use or an adversary signing in to a compromised account from a different location than the legitimate user. +""" +false_positives = [ + """ + Users on VPN or proxy egress that geo-resolves through a region distant from the user's physical location. Mobile + clients on cellular carrier networks that peer through regional hubs may geo-resolve to a different region than the + user's physical location. + """, +] +from = "now-3h" +interval = "30m" +language = "esql" +license = "Elastic License v2" +name = "Google Workspace Impossible Travel Login" +note = """## Triage and analysis + +### Investigating Google Workspace Impossible Travel Login + +Google Workspace is accessible globally; legitimate users authenticate from one location at a time. Two successful sign-ins for the same user separated by a distance and time delta implying travel faster than a commercial airliner cannot be the same human being physically moving, and indicate either a VPN/proxy egress mismatch or a compromised account being accessed from a separate location by an adversary. + +### Possible investigation steps + +- Identify the user (`user.email`) and the geographic separation observed: `Esql.distance_km`, `Esql.travel_kmh`, `Esql.window_minutes`, and the set of distinct countries, regions, and cities (`Esql.source_geo_country_name_values`, `Esql.source_geo_region_name_values`, `Esql.source_geo_city_name_values`). +- Pull all `google_workspace.login` events for the user across the alert window. Sort by `@timestamp` and inspect each `source.ip`, `source.as.organization.name`, `source.geo.country_name`, and `user_agent.original` (when present). +- Determine which sign-ins are consistent with the user's baseline (corporate VPN egress, home ISP, mobile carrier) and which are not. +- For each non-baseline sign-in: check the ASN. Hosting-provider ASNs (Clouvider, Host Telecom, Alibaba, cheap-VPS providers) for interactive sign-ins are high-fidelity suspicious because legitimate end users do not typically egress through those networks. +- Cross-reference `logs-google_workspace.token` for `event.action: authorize` events from the same `user.email` around the same time. An OAuth grant minted from a non-baseline ASN immediately after a non-baseline sign-in is the AiTM kit signature. +- Check `logs-google_workspace.user_accounts` for `2sv_enroll`, recovery email/phone additions, or other state changes that an attacker would make to establish persistence. +- Confirm with the user whether the sign-ins are theirs (VPN, travel) or unexpected. + +### False positive analysis + +- Users on VPN or proxy infrastructure egressing through a distant region: validate against the user's known VPN ranges and consider excluding by ASN. +- Mobile carriers that geo-resolve outside the user's home country (cellular providers often peer through regional hubs): validate by user-agent (mobile UA fingerprint) and source ASN (carrier networks). + +### Response and remediation + +- If the pattern is unexpected, suspend the user immediately, then revoke OAuth tokens (`DELETE /admin/directory/v1/users//tokens/`), reset password, and clear recovery email/phone. +- Investigate any `google_workspace.token: authorize` events fired around the same window for tokens minted to the adversary. +- Review `google_workspace.device` for any `DEVICE_REGISTER_UNREGISTER_EVENT` with `account_state: REGISTERED` near the same window: kit-side device registrations are a persistence vector that survives password rotation if the underlying OAuth tokens were not revoked. +- Cross-check `logs-gcp.audit-*` if the tenant exposes any GCP resources to the user: look for `authenticationInfo.principalEmail` matching the user from a non-baseline `callerIp`. +""" +references = [ + "https://www.elastic.co/security-labs/google-workspace-attack-surface-part-one", + "https://security.googlecloudcommunity.com/community-blog-42/detecting-impossible-travel-with-google-secops-part-1-3892", +] +risk_score = 47 +rule_id = "aff74d85-5bfa-4ff1-ace2-4e3995a37cfa" +severity = "medium" +tags = [ + "Domain: Cloud", + "Domain: Identity", + "Data Source: Google Workspace", + "Data Source: Google Workspace Audit Logs", + "Data Source: Google Workspace User log events", + "Use Case: Threat Detection", + "Use Case: Identity and Access Audit", + "Tactic: Initial Access", + "Tactic: Credential Access", + "Resources: Investigation Guide", +] +type = "esql" + +query = ''' +from logs-google_workspace.login-* +| where event.dataset == "google_workspace.login" + and event.action == "login_success" + and event.outcome == "success" + and user.email is not null + and source.geo.location is not null + +| eval Esql.source_geo_lat = st_y(source.geo.location), + Esql.source_geo_lon = st_x(source.geo.location) + +| stats + Esql.source_geo_country_name_values = values(source.geo.country_name), + Esql.source_geo_region_name_values = values(source.geo.region_name), + Esql.source_geo_city_name_values = values(source.geo.city_name), + Esql.source_as_organization_name_values = values(source.`as`.organization.name), + Esql.source_ip_values = values(source.ip), + Esql.min_lat = min(Esql.source_geo_lat), + Esql.max_lat = max(Esql.source_geo_lat), + Esql.min_lon = min(Esql.source_geo_lon), + Esql.max_lon = max(Esql.source_geo_lon), + Esql.source_geo_country_name_count_distinct = count_distinct(source.geo.country_name), + Esql.source_geo_region_name_count_distinct = count_distinct(source.geo.region_name), + Esql.timestamp_first_seen = min(@timestamp), + Esql.timestamp_last_seen = max(@timestamp), + Esql.event_count = count(*) + by user.email + +| where Esql.source_geo_country_name_count_distinct >= 2 + or Esql.source_geo_region_name_count_distinct >= 2 + +| eval Esql.p1 = to_geopoint(concat("POINT(", to_string(Esql.min_lon), " ", to_string(Esql.min_lat), ")")) +| eval Esql.p2 = to_geopoint(concat("POINT(", to_string(Esql.max_lon), " ", to_string(Esql.max_lat), ")")) +| eval Esql.distance_km = round(st_distance(Esql.p1, Esql.p2) / 1000.0, 0) +| eval Esql.window_minutes = date_diff("minute", Esql.timestamp_first_seen, Esql.timestamp_last_seen) +| eval Esql.travel_kmh = case(Esql.window_minutes > 0, round(Esql.distance_km * 60.0 / Esql.window_minutes, 0), null) + +| where Esql.distance_km >= 500 and Esql.travel_kmh >= 800 + +| keep user.email, + Esql.source_geo_country_name_values, + Esql.source_geo_region_name_values, + Esql.source_geo_city_name_values, + Esql.source_as_organization_name_values, + Esql.source_ip_values, + Esql.source_geo_country_name_count_distinct, + Esql.source_geo_region_name_count_distinct, + Esql.timestamp_first_seen, + Esql.timestamp_last_seen, + Esql.window_minutes, + Esql.distance_km, + Esql.travel_kmh, + Esql.event_count +''' + + +[[rule.threat]] +framework = "MITRE ATT&CK" +[[rule.threat.technique]] +id = "T1078" +name = "Valid Accounts" +reference = "https://attack.mitre.org/techniques/T1078/" +[[rule.threat.technique.subtechnique]] +id = "T1078.004" +name = "Cloud Accounts" +reference = "https://attack.mitre.org/techniques/T1078/004/" + + + +[rule.threat.tactic] +id = "TA0001" +name = "Initial Access" +reference = "https://attack.mitre.org/tactics/TA0001/" +[[rule.threat]] +framework = "MITRE ATT&CK" +[[rule.threat.technique]] +id = "T1528" +name = "Steal Application Access Token" +reference = "https://attack.mitre.org/techniques/T1528/" + +[[rule.threat.technique]] +id = "T1557" +name = "Adversary-in-the-Middle" +reference = "https://attack.mitre.org/techniques/T1557/" + + +[rule.threat.tactic] +id = "TA0006" +name = "Credential Access" +reference = "https://attack.mitre.org/tactics/TA0006/" + +[rule.investigation_fields] +field_names = ["user.email"] +