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.

Elastic rule (View on GitHub)

  1[metadata]
  2creation_date = "2026/10/02"
  3integration = ["gcp_vertexai"]
  4maturity = "production"
  5min_stack_comments = "ES|QL FIRST/LAST aggregations and st_distance require 9.4.0+."
  6min_stack_version = "9.4.0"
  7updated_date = "2026/10/07"
  8
  9[rule]
 10author = ["Elastic"]
 11description = """
 12Detects the same Vertex AI authenticated caller (client.user.email) generating aiplatform audit activity from
 13geographically distant source IPs within a short window at an impossible travel speed. That pattern can indicate a
 14stolen user or service-account credential used from multiple regions. Related: GCP Vertex AI Caller Activity From
 15High Number of Countries covers slower multi-country reuse without a high-speed hop.
 16"""
 17false_positives = [
 18    """
 19    Distributed CI or multi-region automation that shares one service account across distant egress IPs. Prefer
 20    per-region identities, or raise distance and speed thresholds.
 21    """,
 22    """
 23    VPN or cloud egress that geolocates incorrectly or jumps between distant POPs. Validate the IP locations before
 24    rotating credentials.
 25    """,
 26]
 27from = "now-60m"
 28interval = "10m"
 29language = "esql"
 30license = "Elastic License v2"
 31name = "GCP Vertex AI Caller Impossible Travel"
 32note = """## Triage and analysis
 33
 34### Investigating GCP Vertex AI Caller Impossible Travel
 35
 36The same authenticated Vertex AI caller (`client.user.email`) generated aiplatform audit activity
 37from source IPs whose geolocation implies travel faster than the rule thresholds (default at least
 38500 km and 800 km/h) inside the lookback window. That pattern often indicates stolen user or
 39service-account credentials used promptly from multiple regions.
 40
 41#### Possible investigation steps
 42
 43- Review `Esql.source_ip_values`, `Esql.source_geo_country_name_values`,
 44  `Esql.source_geo_region_name_values`, `Esql.source_geo_city_name_values`,
 45  `Esql.distance_km`, `Esql.travel_kmh`,
 46  `Esql.window_minutes`, and `Esql.event_action_values` on the alert.
 47- Determine whether `client.user.email` is a human user or a shared service account. Shared SAs
 48  used by multi-region automation are a common false positive.
 49- Timeline-sort raw audit events for that email: compare IP, geo, user agent, and action. Look for
 50  privileged or unusual methods around the distant hop.
 51- Check GCP IAM / audit logs for key creation, key download, or role grants for that principal near
 52  the first distant event.
 53- Pivot to `logs-gcp_vertexai.prompt_response_logs-*` in the same window for prompt content and
 54  token volume that might show abuse after credential theft.
 55- If countries accumulate to three or more over a longer window without meeting the speed
 56  thresholds, review alerts from GCP Vertex AI Caller Activity From High Number of Countries.
 57
 58### False positive analysis
 59
 60- Multi-region CI or automation that shares one service account across distant egress IPs. Prefer
 61  per-region identities, or raise distance/speed thresholds.
 62- VPN or cloud egress that geolocates incorrectly or jumps between distant POPs. Corroborate with
 63  ASN/org (`Esql.source_as_organization_name_values`) and consistent tooling user agents before
 64  rotating credentials.
 65
 66### Response and remediation
 67
 68- If travel is not approved: disable or rotate the caller's keys/tokens, revoke active sessions, and
 69  review IAM bindings for unexpected grants.
 70- Split multi-region workloads onto region-scoped identities; enable alerts on new key creation for
 71  sensitive service accounts.
 72"""
 73references = [
 74    "https://www.elastic.co/docs/reference/integrations/gcp_vertexai",
 75]
 76risk_score = 47
 77rule_id = "9bd6800b-a299-4c2f-978d-8219560bbee5"
 78setup = """## Setup
 79
 80Requires GCP Vertex AI `auditlogs` with `source.ip`, `source.geo.location`, `source.geo.country_name`, and
 81`client.user.email` (mapped from `protoPayload.authenticationInfo.principalEmail`).
 82"""
 83severity = "medium"
 84tags = [
 85    "Domain: GenAI",
 86    "Domain: Cloud",
 87    "Data Source: GCP Vertex AI",
 88    "Data Source: GCP",
 89    "Data Source: Google Cloud Platform",
 90    "Platform: GCP",
 91    "Service: GCP Vertex AI",
 92    "Use Case: Threat Detection",
 93    "Tactic: Credential Access",
 94    "Tactic: Initial Access",
 95    "Threat: Impossible Travel",
 96    "Threat: LLMjacking",
 97    "Threat: Unauthorized AI Usage",
 98    "Mitre Atlas: AML.T0012",
 99    "Mitre Atlas: AML.T0091",
100    "Mitre Atlas: AML.T0091.000",
101    "Resources: Investigation Guide",
102    "Rule Type: ES|QL",
103]
104timestamp_override = "event.ingested"
105type = "esql"
106
107query = '''
108from logs-gcp_vertexai.auditlogs-*
109| where
110    data_stream.dataset == "gcp_vertexai.auditlogs" and
111    client.user.email is not null and
112    source.ip is not null and
113    source.geo.location is not null and
114    source.geo.country_name is not null and
115    (
116        event.action like "*PredictionService.GenerateContent*" or
117        event.action like "*PredictionService.StreamGenerateContent*" or
118        event.action like "*PredictionService.CountTokens*"
119    )
120| eval
121    Esql.source_geo_lat = st_y(source.geo.location),
122    Esql.source_geo_lon = st_x(source.geo.location)
123| where Esql.source_geo_lat is not null and Esql.source_geo_lon is not null
124| stats
125    Esql.first_lat = first(Esql.source_geo_lat, @timestamp),
126    Esql.first_lon = first(Esql.source_geo_lon, @timestamp),
127    Esql.last_lat = last(Esql.source_geo_lat, @timestamp),
128    Esql.last_lon = last(Esql.source_geo_lon, @timestamp),
129    Esql.event_count = count(*),
130    Esql.country_count = count_distinct(source.geo.country_name),
131    Esql.source_ip_values = values(source.ip),
132    Esql.source_geo_country_name_values = values(source.geo.country_name),
133    Esql.source_geo_region_name_values = values(source.geo.region_name),
134    Esql.source_geo_city_name_values = values(source.geo.city_name),
135    Esql.source_as_organization_name_values = values(source.as.organization.name),
136    Esql.event_action_values = values(event.action),
137    Esql.user_agent_original_values = values(user_agent.original),
138    Esql.timestamp_first_seen = min(@timestamp),
139    Esql.timestamp_last_seen = max(@timestamp)
140  by
141    client.user.email
142| where Esql.event_count >= 2 and Esql.country_count >= 2
143| eval
144    Esql.p1 = to_geopoint(concat("POINT(", to_string(Esql.first_lon), " ", to_string(Esql.first_lat), ")")),
145    Esql.p2 = to_geopoint(concat("POINT(", to_string(Esql.last_lon), " ", to_string(Esql.last_lat), ")"))
146| eval
147    Esql.distance_km = round(st_distance(Esql.p1, Esql.p2) / 1000.0, 0),
148    Esql.window_minutes = date_diff("minute", Esql.timestamp_first_seen, Esql.timestamp_last_seen),
149    Esql.travel_kmh = case(Esql.window_minutes > 0, round(Esql.distance_km * 60.0 / Esql.window_minutes, 0), null),
150    source.ip = MV_FIRST(Esql.source_ip_values)
151| where Esql.distance_km >= 500 and Esql.travel_kmh >= 800
152| keep
153    client.user.email,
154    source.ip,
155    Esql.source_ip_values,
156    Esql.source_geo_country_name_values,
157    Esql.source_geo_region_name_values,
158    Esql.source_geo_city_name_values,
159    Esql.source_as_organization_name_values,
160    Esql.event_action_values,
161    Esql.user_agent_original_values,
162    Esql.distance_km,
163    Esql.travel_kmh,
164    Esql.window_minutes,
165    Esql.country_count,
166    Esql.event_count,
167    Esql.timestamp_first_seen,
168    Esql.timestamp_last_seen
169'''
170
171
172[[rule.threat]]
173framework = "MITRE ATT&CK"
174[[rule.threat.technique]]
175id = "T1528"
176name = "Steal Application Access Token"
177reference = "https://attack.mitre.org/techniques/T1528/"
178
179
180[rule.threat.tactic]
181id = "TA0006"
182name = "Credential Access"
183reference = "https://attack.mitre.org/tactics/TA0006/"
184[[rule.threat]]
185framework = "MITRE ATT&CK"
186[[rule.threat.technique]]
187id = "T1078"
188name = "Valid Accounts"
189reference = "https://attack.mitre.org/techniques/T1078/"
190[[rule.threat.technique.subtechnique]]
191id = "T1078.004"
192name = "Cloud Accounts"
193reference = "https://attack.mitre.org/techniques/T1078/004/"
194
195
196
197[rule.threat.tactic]
198id = "TA0001"
199name = "Initial Access"
200reference = "https://attack.mitre.org/tactics/TA0001/"
201[[rule.threat_mappings]]
202framework = "MITRE ATLAS"
203version = "2026.08"
204[[rule.threat_mappings.threat]]
205framework = "MITRE ATLAS"
206[[rule.threat_mappings.threat.technique]]
207id = "AML.T0012"
208name = "Valid Accounts"
209reference = "https://atlas.mitre.org/techniques/AML.T0012/"
210
211
212[rule.threat_mappings.threat.tactic]
213id = "AML.TA0004"
214name = "Initial Access"
215reference = "https://atlas.mitre.org/tactics/AML.TA0004/"
216[[rule.threat_mappings.threat]]
217framework = "MITRE ATLAS"
218[[rule.threat_mappings.threat.technique]]
219id = "AML.T0091"
220name = "Use Alternate Authentication Material"
221reference = "https://atlas.mitre.org/techniques/AML.T0091/"
222[[rule.threat_mappings.threat.technique.subtechnique]]
223id = "AML.T0091.000"
224name = "Application Access Token"
225reference = "https://atlas.mitre.org/techniques/AML.T0091.000/"
226
227
228
229[rule.threat_mappings.threat.tactic]
230id = "AML.TA0015"
231name = "Lateral Movement"
232reference = "https://atlas.mitre.org/tactics/AML.TA0015/"
233
234[rule.investigation_fields]
235field_names = [
236    "client.user.email",
237    "source.ip",
238    "Esql.distance_km",
239    "Esql.travel_kmh",
240    "Esql.window_minutes",
241    "Esql.country_count",
242    "Esql.event_count",
243    "Esql.source_ip_values",
244    "Esql.source_geo_country_name_values",
245    "Esql.source_geo_region_name_values",
246    "Esql.source_geo_city_name_values",
247    "Esql.source_as_organization_name_values",
248    "Esql.event_action_values",
249    "Esql.user_agent_original_values",
250    "Esql.timestamp_first_seen",
251    "Esql.timestamp_last_seen",
252]
253
254[rule.alert_suppression]
255group_by = ["source.ip"]
256missing_fields_strategy = "suppress"
257
258[rule.alert_suppression.duration]
259unit = "h"
260value = 4

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

Related rules

to-top