LoFP LoFP / administrators and developers may legitimately acquire tokens and use them with powershell, command-line tools, or custom scripts to query microsoft graph. confirm that the client application, account, enumeration target, and timing are expected before escalating.

Techniques

Sample rules

Entra ID Managed Device Client Sign-In to Graph Enumeration

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