EXPLORE
← 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.

MITRE ATT&CK

credential-accessinitial-access

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