Techniques
Sample rules
Entra ID Managed Device Client Sign-In to Graph Enumeration
- source: elastic
- technicques:
- T1087
- T1526
- T1550
Description
Identifies a successful non-interactive Microsoft Entra ID client sign-in for Microsoft Graph that carries managed, joined device context and reports an Edge client when the exact access token is subsequently used by a scripted client to query at least three distinct Microsoft Graph directory-enumeration paths. The rule correlates the Entra sign-in token identifier with Microsoft Graph activity rather than relying on the broader session identifier, which can be shared by legitimate application activity and multiple tokens.
Detection logic
FROM logs-azure.signinlogs-*, logs-azure.graphactivitylogs-* METADATA _id, _version, _index
// normalize the token, user, app, device and session ids
| EVAL
Esql.token_id = COALESCE(
azure.signinlogs.properties.unique_token_identifier,
token.id
),
Esql.session_id = COALESCE(
azure.signinlogs.properties.session_id,
azure.graphactivitylogs.properties.c_sid
),
Esql.user_id = COALESCE(
azure.signinlogs.properties.user_id,
azure.graphactivitylogs.properties.user_principal_object_id
),
Esql.app_id = COALESCE(
azure.signinlogs.properties.app_id,
azure.graphactivitylogs.properties.app_id
),
Esql.device_id = COALESCE(
azure.signinlogs.properties.device_detail.device_id,
azure.graphactivitylogs.properties.c_device_id
),
Esql.user_agent_original_lower = TO_LOWER(user_agent.original),
Esql.url_path_lower = TO_LOWER(url.path)
// filter for Entra ID client sign-in and token acquisition context
| WHERE
Esql.token_id IS NOT NULL AND
Esql.session_id IS NOT NULL AND
Esql.user_id IS NOT NULL AND
Esql.app_id IS NOT NULL AND
Esql.device_id IS NOT NULL AND
Esql.device_id != ""
| EVAL Esql.azure_signinlogs_is_client_signin =
event.dataset == "azure.signinlogs" AND
azure.signinlogs.properties.status.error_code == 0 AND
azure.signinlogs.properties.device_detail.trust_type == "Azure AD joined" AND
azure.signinlogs.properties.device_detail.device_id IS NOT NULL AND
azure.signinlogs.properties.device_detail.device_id != "" AND
azure.signinlogs.properties.device_detail.is_managed == true AND
azure.signinlogs.properties.is_interactive == false AND
azure.signinlogs.properties.incoming_token_type IN ("refreshToken", "primaryRefreshToken") AND
azure.signinlogs.properties.client_app_used == "Mobile Apps and Desktop clients" AND
azure.signinlogs.properties.resource_id == "00000003-0000-0000-c000-000000000000" AND
user_agent.name == "Edge"
// filter for scripted Graph enumeration for a user identity
// based on red teaming tooling and in-the-wild observations
| EVAL Esql.azure_graphactivitylogs_is_scripted_enum =
event.dataset == "azure.graphactivitylogs" AND
azure.graphactivitylogs.properties.c_idtyp == "user" AND
azure.graphactivitylogs.properties.client_auth_method == 0 AND
http.request.method == "GET" AND
(
Esql.user_agent_original_lower LIKE "*powershell*" OR
Esql.user_agent_original_lower LIKE "*python-requests/*" OR
Esql.user_agent_original_lower LIKE "*python-httpx/*" OR
Esql.user_agent_original_lower LIKE "*curl/*" OR
Esql.user_agent_original_lower LIKE "*aiohttp/*" OR
Esql.user_agent_original_lower LIKE "*fasthttp*" OR
Esql.user_agent_original_lower LIKE "*go-http-client/*" OR
Esql.user_agent_original_lower LIKE "*go-resty/*" OR
Esql.user_agent_original_lower LIKE "*okhttp/*" OR
Esql.user_agent_original_lower LIKE "*restsharp/*" OR
Esql.user_agent_original_lower LIKE "*postmanruntime/*" OR
Esql.user_agent_original_lower LIKE "*axios/*" OR
Esql.user_agent_original_lower == "node" OR
Esql.user_agent_original_lower LIKE "*node-fetch/*" OR
Esql.user_agent_original_lower LIKE "*undici*" OR
Esql.user_agent_original_lower LIKE "*azurehound/*" OR
Esql.user_agent_original_lower LIKE "*sharphound/*" OR
Esql.user_agent_original_lower LIKE "*bloodhound/*" OR
Esql.user_agent_original_lower LIKE "*aadinternals*" OR
Esql.user_agent_original_lower LIKE "*bav2ropc*" OR
Esql.user_agent_original_lower LIKE "*roadtools*" OR
Esql.user_agent_original_lower LIKE "*roadrecon*" OR
Esql.user_agent_original_lower LIKE "*roadtx*" OR
Esql.user_agent_original_lower LIKE "*graphpython*" OR
Esql.user_agent_original_lower LIKE "*msgraphx*"
) AND
(
// Documented collection reads. Query parameters are not part of url.path.
Esql.url_path_lower IN (
"/v1.0/users",
"/beta/users",
"/v1.0/groups",
"/beta/groups",
"/v1.0/applications",
"/beta/applications",
"/v1.0/serviceprincipals",
"/beta/serviceprincipals",
"/v1.0/devices",
"/beta/devices",
"/v1.0/directoryroles",
"/beta/directoryroles",
"/v1.0/organization",
"/beta/organization",
"/v1.0/subscribedskus",
"/beta/subscribedskus",
"/v1.0/rolemanagement/directory/roledefinitions",
"/beta/rolemanagement/directory/roledefinitions",
"/v1.0/rolemanagement/directory/roleassignments",
"/beta/rolemanagement/directory/roleassignments",
"/v1.0/rolemanagement/directory/roleeligibilityschedules",
"/beta/rolemanagement/directory/roleeligibilityschedules",
"/v1.0/rolemanagement/directory/roleeligibilityscheduleinstances",
"/beta/rolemanagement/directory/roleeligibilityscheduleinstances"
)
)
| WHERE Esql.azure_signinlogs_is_client_signin OR Esql.azure_graphactivitylogs_is_scripted_enum
// aggregate the events for the rule by session, user, app, device, token and tenant
| STATS
Esql.azure_signinlogs_properties_id_count_distinct = COUNT_DISTINCT(
azure.signinlogs.properties.id
) WHERE Esql.azure_signinlogs_is_client_signin,
Esql.azure_graphactivitylogs_properties_operation_id_count_distinct = COUNT_DISTINCT(
azure.graphactivitylogs.properties.operation_id
) WHERE Esql.azure_graphactivitylogs_is_scripted_enum,
Esql.url_path_lower_count_distinct = COUNT_DISTINCT(
Esql.url_path_lower
) WHERE Esql.azure_graphactivitylogs_is_scripted_enum,
Esql.timestamp_client_signin_min = MIN(
CASE(Esql.azure_signinlogs_is_client_signin, @timestamp, NULL)
),
Esql.timestamp_scripted_enum_min = MIN(
CASE(Esql.azure_graphactivitylogs_is_scripted_enum, @timestamp, NULL)
),
Esql.user_agent_original_client_signin_values = VALUES(
user_agent.original
) WHERE Esql.azure_signinlogs_is_client_signin,
Esql.user_agent_original_scripted_enum_values = VALUES(
user_agent.original
) WHERE Esql.azure_graphactivitylogs_is_scripted_enum,
Esql.url_path_values = VALUES(url.path) WHERE Esql.azure_graphactivitylogs_is_scripted_enum,
Esql.url_original_values = VALUES(url.original) WHERE Esql.azure_graphactivitylogs_is_scripted_enum,
Esql.source_ip_values = VALUES(source.ip),
Esql.azure_signinlogs_properties_id_values = VALUES(
azure.signinlogs.properties.id
) WHERE Esql.azure_signinlogs_is_client_signin,
Esql.azure_graphactivitylogs_properties_operation_id_values = VALUES(
azure.graphactivitylogs.properties.operation_id
) WHERE Esql.azure_graphactivitylogs_is_scripted_enum,
Esql.azure_signinlogs_properties_app_display_name_values = VALUES(
azure.signinlogs.properties.app_display_name
) WHERE Esql.azure_signinlogs_is_client_signin,
Esql_priv.azure_signinlogs_properties_user_principal_name_values = VALUES(
azure.signinlogs.properties.user_principal_name
) WHERE Esql.azure_signinlogs_is_client_signin
BY
Esql.token_id,
Esql.session_id,
Esql.user_id,
Esql.app_id,
Esql.device_id,
azure.tenant_id
// filter by client sign-in and scripted Graph enumeration events
| WHERE
Esql.azure_signinlogs_properties_id_count_distinct > 0 AND
Esql.azure_graphactivitylogs_properties_operation_id_count_distinct > 0 AND
Esql.url_path_lower_count_distinct >= 3 AND
Esql.timestamp_client_signin_min IS NOT NULL AND
Esql.timestamp_scripted_enum_min IS NOT NULL AND
Esql.timestamp_scripted_enum_min >= Esql.timestamp_client_signin_min
| KEEP Esql.*, Esql_priv.*, azure.tenant_id