EXPLORE
← Back to Explore
elasticcriticalTTP

GCP Vertex AI Caller Activity From High Number of Countries

Detects Vertex AI authenticated caller activity for the same client.user.email from at least three countries and three source IPs within a four-hour interval. That pattern can indicate stolen user or service-account credentials reused from multiple regions, including VPN or cloud egress and return trips where first and last locations are close.

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*"
    )
| stats
    Esql.country_count = count_distinct(source.geo.country_name),
    Esql.source_ip_count = count_distinct(source.ip),
    Esql.user_agent_count = count_distinct(user_agent.original),
    Esql.event_count = count(*),
    Esql.event_action_values = values(event.action),
    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.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.country_count >= 3 and Esql.source_ip_count >= 3
| eval source.ip = MV_FIRST(Esql.source_ip_values)
| keep
    client.user.email,
    source.ip,
    Esql.country_count,
    Esql.source_ip_count,
    Esql.user_agent_count,
    Esql.event_count,
    Esql.event_action_values,
    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.user_agent_original_values,
    Esql.timestamp_first_seen,
    Esql.timestamp_last_seen

Author

Elastic

Created

2026/10/05

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/05"
integration = ["gcp_vertexai"]
maturity = "production"
min_stack_comments = "Requires GCP Vertex AI auditlogs enriched with source.geo fields."
min_stack_version = "9.4.0"
updated_date = "2026/10/06"

[rule]
author = ["Elastic"]
description = """
Detects Vertex AI authenticated caller activity for the same client.user.email from at least three countries and three
source IPs within a four-hour interval. That pattern can indicate stolen user or service-account credentials reused
from multiple regions, including VPN or cloud egress and return trips where first and last locations are close.
"""
false_positives = [
    """
    Shared service accounts used by multi-region CI, build agents, or dual-homed VPN/cloud egress can span several
    countries. Prefer per-region identities, or exclude known automation principals.
    """,
    """
    VPN or proxy egress that geo-resolves through hubs far from the caller's physical location. Corroborate with ASN
    and user-agent diversity before rotating credentials.
    """,
]
from = "now-4h"
interval = "15m"
language = "esql"
license = "Elastic License v2"
name = "GCP Vertex AI Caller Activity From High Number of Countries"
note = """## Triage and analysis

### Investigating GCP Vertex AI Caller Activity From High Number of Countries

Successful Vertex AI GenerateContent / CountTokens audit activity for the same
`client.user.email` spans at least three countries and three source IPs inside four hours.
That pattern can indicate leaked or stolen credentials reused from VPN or cloud egress in
multiple regions, including return trips where the first and last locations are close.

Escalate when countries differ, user agents diverge (SDK vs curl/browser), privileged methods
appear on the distant IPs, or IAM key activity precedes the burst. Close as a false positive for
documented multi-region automation with consistent corporate tooling.

#### Possible investigation steps

- Use `Esql.source_geo_country_name_values`, `Esql.source_geo_region_name_values`, `Esql.source_ip_values`, and
  `Esql.user_agent_original_values` to map the geographic and client spread.
- Use `Esql.event_action_values` and `Esql.event_count` to see what the caller did.
- Contact the owner of `client.user.email` (or the owning team for a service account) to confirm
  whether VPN or multi-region automation is expected.
- Pivot to `logs-gcp_vertexai.prompt_response_logs-*` in the same window for prompt abuse,
  refusals, or token volume after the distant events.
- Check GCP IAM / audit logs for key creation, key download, or role grants near the first
  distant country.

### False positive analysis

- Multi-region CI and dual-homed VPN/cloud egress are the main FPs — require ASN/UA
  corroboration and known-principal allowlists before rotating credentials.
- Shared org-wide service accounts used from many regions by design. Split those identities
  per region or exclude them.

### Response and remediation

- On suspected compromise: disable or rotate the caller's keys/tokens, revoke active sessions,
  hunt concurrent sessions from the listed IPs, and review IAM bindings after the distant events.
- Prefer region-scoped service accounts for automation so legitimate multi-region traffic does
  not share one email principal.
"""
references = [
    "https://www.elastic.co/docs/reference/integrations/gcp_vertexai",
]
risk_score = 99
rule_id = "b29f8041-5da1-40e4-aeee-607422074b29"
setup = """## Setup

Requires GCP Vertex AI `auditlogs` with `source.ip`, `source.geo.location`, `source.geo.country_name`,
`client.user.email`, and preferably `user_agent.original`.
"""
severity = "critical"
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*"
    )
| stats
    Esql.country_count = count_distinct(source.geo.country_name),
    Esql.source_ip_count = count_distinct(source.ip),
    Esql.user_agent_count = count_distinct(user_agent.original),
    Esql.event_count = count(*),
    Esql.event_action_values = values(event.action),
    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.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.country_count >= 3 and Esql.source_ip_count >= 3
| eval source.ip = MV_FIRST(Esql.source_ip_values)
| keep
    client.user.email,
    source.ip,
    Esql.country_count,
    Esql.source_ip_count,
    Esql.user_agent_count,
    Esql.event_count,
    Esql.event_action_values,
    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.user_agent_original_values,
    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.country_count",
    "Esql.source_ip_count",
    "Esql.user_agent_count",
    "Esql.event_count",
    "Esql.event_action_values",
    "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.user_agent_original_values",
    "Esql.timestamp_first_seen",
    "Esql.timestamp_last_seen",
]

[rule.alert_suppression]
group_by = ["client.user.email"]
missing_fields_strategy = "suppress"

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