GCP Vertex AI Caller Impossible Travel


Description

Detects the same Vertex AI authenticated caller (client.user.email) generating aiplatform audit activity from geographically distant source IPs within a short window at an impossible travel speed. That pattern can indicate a stolen user or service-account credential used from multiple regions. Related: GCP Vertex AI Caller Activity From High Number of Countries covers slower multi-country reuse without a high-speed hop.

Query · esql

from logs-gcp_vertexai.auditlogs-*
| where
    data_stream.dataset == "gcp_vertexai.auditlogs" and
    client.user.email is not null and
    source.ip is not null and
    source.geo.location is not null and
    source.geo.country_name is not null and
    (
        event.action like "*PredictionService.GenerateContent*" or
        event.action like "*PredictionService.StreamGenerateContent*" or
        event.action like "*PredictionService.CountTokens*"
    )
| eval
    Esql.source_geo_lat = st_y(source.geo.location),
    Esql.source_geo_lon = st_x(source.geo.location)
| where Esql.source_geo_lat is not null and Esql.source_geo_lon is not null
| stats
    Esql.first_lat = first(Esql.source_geo_lat, @timestamp),
    Esql.first_lon = first(Esql.source_geo_lon, @timestamp),
    Esql.last_lat = last(Esql.source_geo_lat, @timestamp),
    Esql.last_lon = last(Esql.source_geo_lon, @timestamp),
    Esql.event_count = count(*),
    Esql.country_count = count_distinct(source.geo.country_name),
    Esql.source_ip_values = values(source.ip),
    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.event_action_values = values(event.action),
    Esql.user_agent_original_values = values(user_agent.original),
    Esql.timestamp_first_seen = min(@timestamp),
    Esql.timestamp_last_seen = max(@timestamp)
  by
    client.user.email
| where Esql.event_count >= 2 and Esql.country_count >= 2
| eval
    Esql.p1 = to_geopoint(concat("POINT(", to_string(Esql.first_lon), " ", to_string(Esql.first_lat), ")")),
    Esql.p2 = to_geopoint(concat("POINT(", to_string(Esql.last_lon), " ", to_string(Esql.last_lat), ")"))
| eval
    Esql.distance_km = round(st_distance(Esql.p1, Esql.p2) / 1000.0, 0),
    Esql.window_minutes = date_diff("minute", Esql.timestamp_first_seen, Esql.timestamp_last_seen),
    Esql.travel_kmh = case(Esql.window_minutes > 0, round(Esql.distance_km * 60.0 / Esql.window_minutes, 0), null),
    source.ip = MV_FIRST(Esql.source_ip_values)
| where Esql.distance_km >= 500 and Esql.travel_kmh >= 800
| keep
    client.user.email,
    source.ip,
    Esql.source_ip_values,
    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.event_action_values,
    Esql.user_agent_original_values,
    Esql.distance_km,
    Esql.travel_kmh,
    Esql.window_minutes,
    Esql.country_count,
    Esql.event_count,
    Esql.timestamp_first_seen,
    Esql.timestamp_last_seen

Investigation fields

Pivot points the source recommends for triage.

  • client.user.email
  • source.ip
  • Esql.distance_km
  • Esql.travel_kmh
  • Esql.window_minutes
  • Esql.country_count
  • Esql.event_count
  • Esql.source_ip_values
  • 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.event_action_values
  • Esql.user_agent_original_values
  • Esql.timestamp_first_seen
  • Esql.timestamp_last_seen

Implementation guide

Requires GCP Vertex AI auditlogs with source.ip, source.geo.location, source.geo.country_name, and client.user.email (mapped from protoPayload.authenticationInfo.principalEmail).

Known false positives

  • Distributed CI or multi-region automation that shares one service account across distant egress IPs. Prefer per-region identities, or raise distance and speed thresholds.
  • VPN or cloud egress that geolocates incorrectly or jumps between distant POPs. Validate the IP locations before rotating credentials.

Analyst notes

Investigating GCP Vertex AI Caller Impossible Travel

The same authenticated Vertex AI caller (client.user.email) generated aiplatform audit activity from source IPs whose geolocation implies travel faster than the rule thresholds (default at least 500 km and 800 km/h) inside the lookback window. That pattern often indicates stolen user or service-account credentials used promptly from multiple regions.

Possible investigation steps

  • Review Esql.source_ip_values, Esql.source_geo_country_name_values, Esql.source_geo_region_name_values, Esql.source_geo_city_name_values, Esql.distance_km, Esql.travel_kmh, Esql.window_minutes, and Esql.event_action_values on the alert.
  • Determine whether client.user.email is a human user or a shared service account. Shared SAs used by multi-region automation are a common false positive.
  • Timeline-sort raw audit events for that email: compare IP, geo, user agent, and action. Look for privileged or unusual methods around the distant hop.
  • Check GCP IAM / audit logs for key creation, key download, or role grants for that principal near the first distant event.
  • Pivot to logs-gcp_vertexai.prompt_response_logs-* in the same window for prompt content and token volume that might show abuse after credential theft.
  • If countries accumulate to three or more over a longer window without meeting the speed thresholds, review alerts from GCP Vertex AI Caller Activity From High Number of Countries.

False positive analysis

  • Multi-region CI or automation that shares one service account across distant egress IPs. Prefer per-region identities, or raise distance/speed thresholds.
  • VPN or cloud egress that geolocates incorrectly or jumps between distant POPs. Corroborate with ASN/org (Esql.source_as_organization_name_values) and consistent tooling user agents before rotating credentials.

Response and remediation

  • If travel is not approved: disable or rotate the caller's keys/tokens, revoke active sessions, and review IAM bindings for unexpected grants.
  • Split multi-region workloads onto region-scoped identities; enable alerts on new key creation for sensitive service accounts.
Raw source GCP Vertex AI Caller Impossible Travel · Elastic TOML
Esc
Published by elastic/detection-rules ↗, licensed under Elastic License 2.0 ↗. Reproduced here unmodified.
[metadata]
creation_date = "2026/10/02"
integration = ["gcp_vertexai"]
maturity = "production"
min_stack_comments = "ES|QL FIRST/LAST aggregations and st_distance require 9.4.0+."
min_stack_version = "9.4.0"
updated_date = "2026/10/07"

[rule]
author = ["Elastic"]
description = """
Detects the same Vertex AI authenticated caller (client.user.email) generating aiplatform audit activity from
geographically distant source IPs within a short window at an impossible travel speed. That pattern can indicate a
stolen user or service-account credential used from multiple regions. Related: GCP Vertex AI Caller Activity From
High Number of Countries covers slower multi-country reuse without a high-speed hop.
"""
false_positives = [
    """
    Distributed CI or multi-region automation that shares one service account across distant egress IPs. Prefer
    per-region identities, or raise distance and speed thresholds.
    """,
    """
    VPN or cloud egress that geolocates incorrectly or jumps between distant POPs. Validate the IP locations before
    rotating credentials.
    """,
]
from = "now-60m"
interval = "10m"
language = "esql"
license = "Elastic License v2"
name = "GCP Vertex AI Caller Impossible Travel"
note = """## Triage and analysis

### Investigating GCP Vertex AI Caller Impossible Travel

The same authenticated Vertex AI caller (`client.user.email`) generated aiplatform audit activity
from source IPs whose geolocation implies travel faster than the rule thresholds (default at least
500 km and 800 km/h) inside the lookback window. That pattern often indicates stolen user or
service-account credentials used promptly from multiple regions.

#### Possible investigation steps

- Review `Esql.source_ip_values`, `Esql.source_geo_country_name_values`,
  `Esql.source_geo_region_name_values`, `Esql.source_geo_city_name_values`,
  `Esql.distance_km`, `Esql.travel_kmh`,
  `Esql.window_minutes`, and `Esql.event_action_values` on the alert.
- Determine whether `client.user.email` is a human user or a shared service account. Shared SAs
  used by multi-region automation are a common false positive.
- Timeline-sort raw audit events for that email: compare IP, geo, user agent, and action. Look for
  privileged or unusual methods around the distant hop.
- Check GCP IAM / audit logs for key creation, key download, or role grants for that principal near
  the first distant event.
- Pivot to `logs-gcp_vertexai.prompt_response_logs-*` in the same window for prompt content and
  token volume that might show abuse after credential theft.
- If countries accumulate to three or more over a longer window without meeting the speed
  thresholds, review alerts from GCP Vertex AI Caller Activity From High Number of Countries.

### False positive analysis

- Multi-region CI or automation that shares one service account across distant egress IPs. Prefer
  per-region identities, or raise distance/speed thresholds.
- VPN or cloud egress that geolocates incorrectly or jumps between distant POPs. Corroborate with
  ASN/org (`Esql.source_as_organization_name_values`) and consistent tooling user agents before
  rotating credentials.

### Response and remediation

- If travel is not approved: disable or rotate the caller's keys/tokens, revoke active sessions, and
  review IAM bindings for unexpected grants.
- Split multi-region workloads onto region-scoped identities; enable alerts on new key creation for
  sensitive service accounts.
"""
references = [
    "https://www.elastic.co/docs/reference/integrations/gcp_vertexai",
]
risk_score = 47
rule_id = "9bd6800b-a299-4c2f-978d-8219560bbee5"
setup = """## Setup

Requires GCP Vertex AI `auditlogs` with `source.ip`, `source.geo.location`, `source.geo.country_name`, and
`client.user.email` (mapped from `protoPayload.authenticationInfo.principalEmail`).
"""
severity = "medium"
tags = [
    "Domain: GenAI",
    "Domain: Cloud",
    "Data Source: GCP Vertex AI",
    "Data Source: GCP",
    "Data Source: Google Cloud Platform",
    "Platform: GCP",
    "Service: GCP Vertex AI",
    "Use Case: Threat Detection",
    "Tactic: Credential Access",
    "Tactic: Initial Access",
    "Threat: Impossible Travel",
    "Threat: LLMjacking",
    "Threat: Unauthorized AI Usage",
    "Mitre Atlas: AML.T0012",
    "Mitre Atlas: AML.T0091",
    "Mitre Atlas: AML.T0091.000",
    "Resources: Investigation Guide",
    "Rule Type: ES|QL",
]
timestamp_override = "event.ingested"
type = "esql"

query = '''
from logs-gcp_vertexai.auditlogs-*
| where
    data_stream.dataset == "gcp_vertexai.auditlogs" and
    client.user.email is not null and
    source.ip is not null and
    source.geo.location is not null and
    source.geo.country_name is not null and
    (
        event.action like "*PredictionService.GenerateContent*" or
        event.action like "*PredictionService.StreamGenerateContent*" or
        event.action like "*PredictionService.CountTokens*"
    )
| eval
    Esql.source_geo_lat = st_y(source.geo.location),
    Esql.source_geo_lon = st_x(source.geo.location)
| where Esql.source_geo_lat is not null and Esql.source_geo_lon is not null
| stats
    Esql.first_lat = first(Esql.source_geo_lat, @timestamp),
    Esql.first_lon = first(Esql.source_geo_lon, @timestamp),
    Esql.last_lat = last(Esql.source_geo_lat, @timestamp),
    Esql.last_lon = last(Esql.source_geo_lon, @timestamp),
    Esql.event_count = count(*),
    Esql.country_count = count_distinct(source.geo.country_name),
    Esql.source_ip_values = values(source.ip),
    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.event_action_values = values(event.action),
    Esql.user_agent_original_values = values(user_agent.original),
    Esql.timestamp_first_seen = min(@timestamp),
    Esql.timestamp_last_seen = max(@timestamp)
  by
    client.user.email
| where Esql.event_count >= 2 and Esql.country_count >= 2
| eval
    Esql.p1 = to_geopoint(concat("POINT(", to_string(Esql.first_lon), " ", to_string(Esql.first_lat), ")")),
    Esql.p2 = to_geopoint(concat("POINT(", to_string(Esql.last_lon), " ", to_string(Esql.last_lat), ")"))
| eval
    Esql.distance_km = round(st_distance(Esql.p1, Esql.p2) / 1000.0, 0),
    Esql.window_minutes = date_diff("minute", Esql.timestamp_first_seen, Esql.timestamp_last_seen),
    Esql.travel_kmh = case(Esql.window_minutes > 0, round(Esql.distance_km * 60.0 / Esql.window_minutes, 0), null),
    source.ip = MV_FIRST(Esql.source_ip_values)
| where Esql.distance_km >= 500 and Esql.travel_kmh >= 800
| keep
    client.user.email,
    source.ip,
    Esql.source_ip_values,
    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.event_action_values,
    Esql.user_agent_original_values,
    Esql.distance_km,
    Esql.travel_kmh,
    Esql.window_minutes,
    Esql.country_count,
    Esql.event_count,
    Esql.timestamp_first_seen,
    Esql.timestamp_last_seen
'''


[[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.tactic]
id = "TA0006"
name = "Credential Access"
reference = "https://attack.mitre.org/tactics/TA0006/"
[[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_mappings]]
framework = "MITRE ATLAS"
version = "2026.08"
[[rule.threat_mappings.threat]]
framework = "MITRE ATLAS"
[[rule.threat_mappings.threat.technique]]
id = "AML.T0012"
name = "Valid Accounts"
reference = "https://atlas.mitre.org/techniques/AML.T0012/"


[rule.threat_mappings.threat.tactic]
id = "AML.TA0004"
name = "Initial Access"
reference = "https://atlas.mitre.org/tactics/AML.TA0004/"
[[rule.threat_mappings.threat]]
framework = "MITRE ATLAS"
[[rule.threat_mappings.threat.technique]]
id = "AML.T0091"
name = "Use Alternate Authentication Material"
reference = "https://atlas.mitre.org/techniques/AML.T0091/"
[[rule.threat_mappings.threat.technique.subtechnique]]
id = "AML.T0091.000"
name = "Application Access Token"
reference = "https://atlas.mitre.org/techniques/AML.T0091.000/"



[rule.threat_mappings.threat.tactic]
id = "AML.TA0015"
name = "Lateral Movement"
reference = "https://atlas.mitre.org/tactics/AML.TA0015/"

[rule.investigation_fields]
field_names = [
    "client.user.email",
    "source.ip",
    "Esql.distance_km",
    "Esql.travel_kmh",
    "Esql.window_minutes",
    "Esql.country_count",
    "Esql.event_count",
    "Esql.source_ip_values",
    "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.event_action_values",
    "Esql.user_agent_original_values",
    "Esql.timestamp_first_seen",
    "Esql.timestamp_last_seen",
]

[rule.alert_suppression]
group_by = ["source.ip"]
missing_fields_strategy = "suppress"

[rule.alert_suppression.duration]
unit = "h"
value = 4

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.