← Back to Explore
elasticmediumTTP
GCP Vertex AI Caller Impossible Travel
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.
Detection 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
Author
Elastic
Created
2026/10/02
Data Sources
GCP Vertex AIGCPGoogle Cloud Platform
Tags
Domain: GenAIDomain: CloudData Source: GCP Vertex AIData Source: GCPData Source: Google Cloud PlatformPlatform: GCPService: GCP Vertex AIUse Case: Threat DetectionTactic: Credential AccessTactic: Initial AccessThreat: Impossible TravelThreat: LLMjackingThreat: Unauthorized AI UsageMitre Atlas: AML.T0012Mitre Atlas: AML.T0091Mitre Atlas: AML.T0091.000Resources: Investigation GuideRule Type: ES|QL
Raw Content
[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