Entra ID Managed Device Client Sign-In to Graph Enumeration

edit
IMPORTANT: This documentation is no longer updated. Refer to Elastic's version policy and the latest documentation.

Entra ID Managed Device Client Sign-In to Graph Enumeration

edit

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.

Rule type: esql

Rule indices: None

Severity: medium

Risk score: 47

Runs every: 5m

Searches indices from: now-60m (Date Math format, see also Additional look-back time)

Maximum alerts per execution: 100

References:

Tags:

  • Domain: Cloud
  • Domain: Identity
  • Data Source: Azure
  • Data Source: Microsoft Graph Activity Logs
  • Platform: Entra ID
  • Use Case: Identity and Access Audit
  • Use Case: Threat Detection
  • Resources: Investigation Guide
  • Rule Type: ESQL
  • Tactic: Defense Evasion
  • Tactic: Discovery

Version: 1

Rule authors:

  • Elastic

Rule license: Elastic License v2

Investigation guide

edit

Triage and analysis

Investigating Entra ID Managed Device Client Sign-In to Graph Enumeration

This rule joins a non-interactive Entra sign-in to Microsoft Graph activity using the exact token identifier: azure.signinlogs.properties.unique_token_identifier on the sign-in and token.id on Graph activity. The shared session ID, user ID, and application ID provide supporting context but are not sufficient correlation keys by themselves.

The sign-in half requires an Azure AD joined trust type, a non-empty device ID, managed device status, an incoming refresh token or Primary Refresh Token, the Microsoft Graph resource, a mobile or desktop client, and a parsed Edge user agent. These values describe the device and client context asserted in the sign-in; they do not prove the request originated from that physical device or that WAM/WinRT generated it.

Possible investigation steps

  • Review Esql.token_id, Esql.azure_signinlogs_properties_id_values, and Esql.azure_graphactivitylogs_properties_operation_id_values to pivot to the original events.
  • Compare Esql.user_agent_original_client_signin_values with Esql.user_agent_original_scripted_enum_values. A client sign-in followed by PowerShell, Python, AzureHound, a generic HTTP library, or another scripted Graph client using the same token can indicate unauthorized access-token use.
  • A browser-like Graph user agent does not clear the activity. GraphRunner and Graphpython can forge browser strings; this rule intentionally avoids matching broad Mozilla, Chrome, Edge, or Safari values because they are too noisy.
  • Review Esql.url_original_values and Esql.url_path_values to determine what identities, groups, applications, service principals, roles, or tenant metadata were enumerated. Esql.url_path_lower_count_distinct must be at least three. The count uses the lowercased path, so casing variants of the same path count once. Query parameters and pagination are not part of url.path and do not increase this count.
  • Validate Esql.app_id, Esql.user_id, Esql.session_id, Esql.device_id, and source addresses.
  • Review adjacent Endpoint telemetry for unusual access to Microsoft\TokenBroker\Cache\*.tbres, WAM-related call stacks, or untrusted processes loading vaultcli.dll.
  • Do not require azure.graphactivitylogs.properties.token_issued_at to equal the sign-in time. Access-token issue times can be backdated for clock-skew tolerance; the token identifier is the precise join key.

False positive analysis

  • First-party Microsoft desktop applications commonly satisfy the sign-in half of this rule. The suspicious behavior is the same access token subsequently appearing from a scripted client against a directory-enumeration path.
  • Confirm whether the user routinely uses Microsoft Graph PowerShell, custom administration scripts, or development tools that acquire and use delegated access tokens.
  • Determine whether the requested paths align with an approved administrative workflow.
  • Prefer narrowly scoped exceptions for reviewed user and application combinations rather than excluding a scripted user agent globally.

Response and remediation

  • Revoke active user sessions and refresh tokens if the activity is unauthorized.
  • Review all Graph activity associated with the token ID and session ID for additional discovery or data access.
  • Investigate the originating endpoint for token theft, export, or unauthorized scripted use.
  • Review Conditional Access and token-protection controls for the affected user and application.

Setup

edit

Required Microsoft Entra ID and Microsoft Graph logs

Enable Microsoft Entra ID sign-in logs and Microsoft Graph activity logs through the Azure integration. The rule requires azure.signinlogs.properties.unique_token_identifier and the normalized Graph token.id field so it can join issuance and use of the same access token.

Rule query

edit
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

Framework: MITRE ATT&CKTM