EXPLORE
← Back to Explore
elastichighTTP

Anthropic Session Reuse Impossible Travel

Detects successful Anthropic audit activity for the same user email from source IP addresses whose query-time geo-locations (via IP_LOCATION) span at least two countries, are separated by at least 500 km, and imply travel faster than 800 km/h within a short (~15-minute) lookback. Unlike login-only impossible travel, this rule covers any successful user-actor activity and can surface session cookie replay or concurrent session reuse when no new authentication events appear.

MITRE ATT&CK

credential-accessinitial-access

Detection Query

from logs-anthropic.audit-*
| where
    data_stream.dataset == "anthropic.audit" and
    event.outcome == "success" and
    anthropic.audit.actor.type == "user_actor" and
    user.email is not null and
    source.ip is not null
| IP_LOCATION geo = source.ip with { "properties": ["country_name", "city_name", "location"] }
| eval
    Esql.source_geo_lat = st_y(geo.location),
    Esql.source_geo_lon = st_x(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(geo.country_name),
    Esql.event_id_values = values(event.id),
    Esql.event_action_values = values(event.action),
    Esql.source_ip_values = values(source.ip),
    Esql.source_geo_country_name_values = values(geo.country_name),
    Esql.source_geo_city_name_values = values(geo.city_name),
    Esql.user_agent_original_values = values(user_agent.original),
    Esql.anthropic_audit_actor_type_values = values(anthropic.audit.actor.type),
    Esql.timestamp_first_seen = min(@timestamp),
    Esql.timestamp_last_seen = max(@timestamp)
  by 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)
| where Esql.distance_km >= 500 and Esql.travel_kmh >= 800
| keep user.email, Esql.*

Author

Elastic

Created

2026/09/16

Data Sources

Anthropic Audit Logs

Tags

Domain: GenAIDomain: IdentityPlatform: AnthropicData Source: Anthropic Audit LogsUse Case: Identity and Access AuditUse Case: Threat DetectionResources: Investigation GuideRule Type: ES|QLTactic: Credential AccessTactic: Initial AccessMitre Atlas: AML.T0012
Raw Content
[metadata]
creation_date = "2026/09/16"
integration = ["anthropic"]
maturity = "production"
min_stack_comments = "ES|QL IP_LOCATION requires 9.5.0+. FIRST/LAST aggregations and st_distance require 9.4.0+."
min_stack_version = "9.5.0"
updated_date = "2026/09/25"

[rule]
author = ["Elastic"]
description = """
Detects successful Anthropic audit activity for the same user email from source IP addresses whose query-time
geo-locations (via IP_LOCATION) span at least two countries, are separated by at least 500 km, and imply travel faster
than 800 km/h within a short (~15-minute) lookback. Unlike login-only impossible travel, this rule covers any successful
user-actor activity and can surface session cookie replay or concurrent session reuse when no new authentication events
appear.
"""
false_positives = [
    """
    Users on VPN or proxy egress that geo-resolves through a region distant from the user's physical location. Mobile
    clients on cellular networks that peer through regional hubs may geo-resolve differently than the user's location.
    """,
    """
    Cloud egress, split-tunnel, or dual-homed clients that present different public IPs for concurrent Anthropic
    sessions (for example browser and API tooling) can look like impossible travel when both resolve far apart.
    """,
]
from = "now-16m"
interval = "5m"
language = "esql"
license = "Elastic License v2"
name = "Anthropic Session Reuse Impossible Travel"
note = """## Triage and analysis

### Investigating Anthropic Session Reuse Impossible Travel

Successful `user_actor` activity for the same email spans distant countries too quickly for travel. Unlike login-only
impossible travel, this covers any successful audit action — useful when cookie replay or concurrent sessions produce
distant activity without a matching login.

A-B-A returns may not alert; sort raw Timeline events. No nearby `magic_link_login_succeeded` /
`sso_login_succeeded` with distant successful non-login actions strengthens a session-reuse hypothesis.

Escalate when countries differ, UAs diverge (browser vs curl/python), login events are absent, or admin/export
actions appear on the distant IP. Close as FP for documented dual-homed VPN/cloud egress with consistent corporate
UAs.

#### Possible investigation steps

- Use `Esql.country_count`, distance/speed, and `Esql.event_action_values` to see whether the distant hop was
  privileged (exports, role changes) vs routine chat views.
- Timeline-sort events; compare IP, geo, UA, and action. Concurrent browser + tooling on far-apart cloud egress often
  explains FP; identical session cookies across continents without login does not.
- Check for nearby logins. Absence + privileged distant actions → treat as cookie replay until proven otherwise.
- Pair with **Anthropic Impossible Travel Login** / auth-failure rules when takeover is suspected.

### False positive analysis

- Split-tunnel / dual-homed clients (browser + API tooling) and VPN geo noise are the main FPs — require ASN/UA
  corroboration.

### Response and remediation

- On suspected replay/compromise: revoke sessions, reset credentials/MFA, hunt concurrent sessions, and review
  compliance/export and role changes for the same `user.email` after the distant events.
"""
references = ["https://platform.claude.com/docs/en/api/compliance/activities/list"]
risk_score = 73
rule_id = "291fc461-0dc8-4583-be5e-929467b3dd2e"
severity = "high"
tags = [
    "Domain: GenAI",
    "Domain: Identity",
    "Platform: Anthropic",
    "Data Source: Anthropic Audit Logs",
    "Use Case: Identity and Access Audit",
    "Use Case: Threat Detection",
    "Resources: Investigation Guide",
    "Rule Type: ES|QL",
    "Tactic: Credential Access",
    "Tactic: Initial Access",
    "Mitre Atlas: AML.T0012",
]
timestamp_override = "event.ingested"
type = "esql"

query = '''
from logs-anthropic.audit-*
| where
    data_stream.dataset == "anthropic.audit" and
    event.outcome == "success" and
    anthropic.audit.actor.type == "user_actor" and
    user.email is not null and
    source.ip is not null
| IP_LOCATION geo = source.ip with { "properties": ["country_name", "city_name", "location"] }
| eval
    Esql.source_geo_lat = st_y(geo.location),
    Esql.source_geo_lon = st_x(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(geo.country_name),
    Esql.event_id_values = values(event.id),
    Esql.event_action_values = values(event.action),
    Esql.source_ip_values = values(source.ip),
    Esql.source_geo_country_name_values = values(geo.country_name),
    Esql.source_geo_city_name_values = values(geo.city_name),
    Esql.user_agent_original_values = values(user_agent.original),
    Esql.anthropic_audit_actor_type_values = values(anthropic.audit.actor.type),
    Esql.timestamp_first_seen = min(@timestamp),
    Esql.timestamp_last_seen = max(@timestamp)
  by 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)
| where Esql.distance_km >= 500 and Esql.travel_kmh >= 800
| keep user.email, Esql.*
'''


[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1539"
name = "Steal Web Session Cookie"
reference = "https://attack.mitre.org/techniques/T1539/"


[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.alert_suppression]
group_by = ["user.email"]
missing_fields_strategy = "suppress"

[rule.investigation_fields]
field_names = [
    "user.email",
    "Esql.distance_km",
    "Esql.travel_kmh",
    "Esql.window_minutes",
    "Esql.country_count",
    "Esql.event_count",
    "Esql.event_id_values",
    "Esql.event_action_values",
    "Esql.source_ip_values",
    "Esql.source_geo_country_name_values",
    "Esql.source_geo_city_name_values",
    "Esql.first_lat",
    "Esql.first_lon",
    "Esql.last_lat",
    "Esql.last_lon",
    "Esql.user_agent_original_values",
    "Esql.anthropic_audit_actor_type_values",
    "Esql.timestamp_first_seen",
    "Esql.timestamp_last_seen",
]

[rule.alert_suppression.duration]
unit = "m"
value = 15