Techniques
Sample rules
Microsoft Foundry Subscription Key Impossible Travel
- source: elastic
- technicques:
- T1078
- T1528
Description
Detects the same API Management subscription key calling Microsoft Foundry from source IPs in two or more countries, far enough apart and fast enough that the hop is not physical travel. Calls are grouped by apim_subscription_id. Requests with no subscription key are ignored, because those are unauthenticated calls and share no key.
Detection logic
from logs-azure_ai_foundry.logs-*
| where
data_stream.dataset == "azure_ai_foundry.logs" and
azure.ai_foundry.category == "GatewayLogs" and
azure.ai_foundry.properties.apim_subscription_id is not null and
source.ip is not null and
source.geo.location is not null and
source.geo.country_name is not null
| 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.azure_ai_foundry_properties_user_agent_values = values(azure.ai_foundry.properties.user_agent),
Esql.azure_ai_foundry_properties_backend_response_code_values = values(azure.ai_foundry.properties.backend_response_code),
Esql.azure_ai_foundry_properties_apim_subscription_id = min(azure.ai_foundry.properties.apim_subscription_id),
Esql.timestamp_first_seen = min(@timestamp),
Esql.timestamp_last_seen = max(@timestamp)
by
azure.ai_foundry.properties.apim_subscription_id,
azure.ai_foundry.properties.api_id,
azure.ai_foundry.properties.operation_id,
azure.resource.name,
azure.resource.group,
url.domain,
url.path
| 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
azure.ai_foundry.properties.apim_subscription_id,
azure.ai_foundry.properties.api_id,
azure.ai_foundry.properties.operation_id,
azure.resource.name,
azure.resource.group,
url.domain,
url.path,
source.ip,
Esql.*