Entra ID Managed Device Client Sign-In to Graph Enumeration

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.

Elastic rule (View on GitHub)

  1[metadata]
  2creation_date = "2026/09/17"
  3integration = ["azure"]
  4maturity = "production"
  5updated_date = "2026/09/17"
  6
  7[rule]
  8author = ["Elastic"]
  9description = """
 10Identifies a successful non-interactive Microsoft Entra ID client sign-in for Microsoft Graph that carries managed,
 11joined device context and reports an Edge client when the exact access token is subsequently used by a scripted client
 12to query at least three distinct Microsoft Graph directory-enumeration paths.
 13The rule correlates the Entra sign-in token identifier with Microsoft Graph activity rather than relying on the broader
 14session identifier, which can be shared by legitimate application activity and multiple tokens.
 15"""
 16false_positives = [
 17    """
 18Administrators and developers may legitimately acquire tokens and use them with PowerShell, command-line tools, or
 19custom scripts to query Microsoft Graph. Confirm that the client application, account, enumeration target, and timing
 20are expected before escalating.
 21    """,
 22]
 23from = "now-60m"
 24language = "esql"
 25license = "Elastic License v2"
 26name = "Entra ID Managed Device Client Sign-In to Graph Enumeration"
 27note = """## Triage and analysis
 28
 29### Investigating Entra ID Managed Device Client Sign-In to Graph Enumeration
 30
 31This rule joins a non-interactive Entra sign-in to Microsoft Graph activity using the exact token identifier:
 32`azure.signinlogs.properties.unique_token_identifier` on the sign-in and `token.id` on Graph activity. The shared
 33session ID, user ID, and application ID provide supporting context but are not sufficient correlation keys by
 34themselves.
 35
 36The sign-in half requires an `Azure AD joined` trust type, a non-empty device ID, managed device status, an incoming
 37refresh token or Primary Refresh Token, the Microsoft Graph resource, a mobile or desktop client, and a parsed Edge
 38user agent. These values describe the device and client context asserted in the sign-in; they do not prove the request
 39originated from that physical device or that WAM/WinRT generated it.
 40
 41### Possible investigation steps
 42
 43- Review `Esql.token_id`, `Esql.azure_signinlogs_properties_id_values`, and
 44  `Esql.azure_graphactivitylogs_properties_operation_id_values` to pivot to the original events.
 45- Compare `Esql.user_agent_original_client_signin_values` with
 46  `Esql.user_agent_original_scripted_enum_values`. A client sign-in followed by PowerShell, Python,
 47  AzureHound, a generic HTTP library, or another scripted Graph client using the same token can indicate unauthorized
 48  access-token use.
 49- A browser-like Graph user agent does not clear the activity. GraphRunner and Graphpython can forge browser strings;
 50  this rule intentionally avoids matching broad Mozilla, Chrome, Edge, or Safari values because they are too noisy.
 51- Review `Esql.url_original_values` and `Esql.url_path_values` to determine what identities, groups, applications,
 52  service principals, roles, or tenant metadata were enumerated. `Esql.url_path_lower_count_distinct` must be at least
 53  three. The count uses the lowercased path, so casing variants of the same path count once. Query parameters and
 54  pagination are not part of `url.path` and do not increase this count.
 55- Validate `Esql.app_id`, `Esql.user_id`, `Esql.session_id`, `Esql.device_id`, and source addresses.
 56- Review adjacent Endpoint telemetry for unusual access to
 57  `Microsoft\\TokenBroker\\Cache\\*.tbres`, WAM-related call stacks, or untrusted processes loading `vaultcli.dll`.
 58- Do not require `azure.graphactivitylogs.properties.token_issued_at` to equal the sign-in time. Access-token issue
 59  times can be backdated for clock-skew tolerance; the token identifier is the precise join key.
 60
 61### False positive analysis
 62
 63- First-party Microsoft desktop applications commonly satisfy the sign-in half of this rule. The suspicious behavior
 64  is the same access token subsequently appearing from a scripted client against a directory-enumeration path.
 65- Confirm whether the user routinely uses Microsoft Graph PowerShell, custom administration scripts, or development
 66  tools that acquire and use delegated access tokens.
 67- Determine whether the requested paths align with an approved administrative workflow.
 68- Prefer narrowly scoped exceptions for reviewed user and application combinations rather than excluding a scripted
 69  user agent globally.
 70
 71### Response and remediation
 72
 73- Revoke active user sessions and refresh tokens if the activity is unauthorized.
 74- Review all Graph activity associated with the token ID and session ID for additional discovery or data access.
 75- Investigate the originating endpoint for token theft, export, or unauthorized scripted use.
 76- Review Conditional Access and token-protection controls for the affected user and application.
 77"""
 78references = [
 79    "https://github.com/dirkjanm/askWAM",
 80    "https://github.com/SpecterOps/AzureHound",
 81    "https://github.com/dafthack/GraphRunner",
 82    "https://github.com/mlcsec/Graphpython",
 83]
 84risk_score = 47
 85rule_id = "8d5d35b3-8c52-4229-964f-df3bab3e2cb5"
 86setup = """#### Required Microsoft Entra ID and Microsoft Graph logs
 87
 88Enable Microsoft Entra ID sign-in logs and Microsoft Graph activity logs through the Azure integration. The rule
 89requires `azure.signinlogs.properties.unique_token_identifier` and the normalized Graph `token.id` field so it can
 90join issuance and use of the same access token.
 91"""
 92severity = "medium"
 93tags = [
 94    "Domain: Cloud",
 95    "Domain: Identity",
 96    "Data Source: Azure",
 97    "Data Source: Microsoft Graph Activity Logs",
 98    "Platform: Entra ID",
 99    "Use Case: Identity and Access Audit",
100    "Use Case: Threat Detection",
101    "Resources: Investigation Guide",
102    "Rule Type: ESQL",
103    "Tactic: Defense Evasion",
104    "Tactic: Discovery",
105]
106timestamp_override = "event.ingested"
107type = "esql"
108
109query = '''
110FROM logs-azure.signinlogs-*, logs-azure.graphactivitylogs-* METADATA _id, _version, _index
111
112// normalize the token, user, app, device and session ids
113| EVAL
114    Esql.token_id = COALESCE(
115        azure.signinlogs.properties.unique_token_identifier,
116        token.id
117    ),
118    Esql.session_id = COALESCE(
119        azure.signinlogs.properties.session_id,
120        azure.graphactivitylogs.properties.c_sid
121    ),
122    Esql.user_id = COALESCE(
123        azure.signinlogs.properties.user_id,
124        azure.graphactivitylogs.properties.user_principal_object_id
125    ),
126    Esql.app_id = COALESCE(
127        azure.signinlogs.properties.app_id,
128        azure.graphactivitylogs.properties.app_id
129    ),
130    Esql.device_id = COALESCE(
131        azure.signinlogs.properties.device_detail.device_id,
132        azure.graphactivitylogs.properties.c_device_id
133    ),
134    Esql.user_agent_original_lower = TO_LOWER(user_agent.original),
135    Esql.url_path_lower = TO_LOWER(url.path)
136
137// filter for Entra ID client sign-in and token acquisition context
138| WHERE
139    Esql.token_id IS NOT NULL AND
140    Esql.session_id IS NOT NULL AND
141    Esql.user_id IS NOT NULL AND
142    Esql.app_id IS NOT NULL AND
143    Esql.device_id IS NOT NULL AND
144    Esql.device_id != ""
145| EVAL Esql.azure_signinlogs_is_client_signin =
146    event.dataset == "azure.signinlogs" AND
147    azure.signinlogs.properties.status.error_code == 0 AND
148    azure.signinlogs.properties.device_detail.trust_type == "Azure AD joined" AND
149    azure.signinlogs.properties.device_detail.device_id IS NOT NULL AND
150    azure.signinlogs.properties.device_detail.device_id != "" AND
151    azure.signinlogs.properties.device_detail.is_managed == true AND
152    azure.signinlogs.properties.is_interactive == false AND
153    azure.signinlogs.properties.incoming_token_type IN ("refreshToken", "primaryRefreshToken") AND
154    azure.signinlogs.properties.client_app_used == "Mobile Apps and Desktop clients" AND
155    azure.signinlogs.properties.resource_id == "00000003-0000-0000-c000-000000000000" AND
156    user_agent.name == "Edge"
157
158// filter for scripted Graph enumeration for a user identity
159// based on red teaming tooling and in-the-wild observations
160| EVAL Esql.azure_graphactivitylogs_is_scripted_enum =
161    event.dataset == "azure.graphactivitylogs" AND
162    azure.graphactivitylogs.properties.c_idtyp == "user" AND
163    azure.graphactivitylogs.properties.client_auth_method == 0 AND
164    http.request.method == "GET" AND
165    (
166        Esql.user_agent_original_lower LIKE "*powershell*" OR
167        Esql.user_agent_original_lower LIKE "*python-requests/*" OR
168        Esql.user_agent_original_lower LIKE "*python-httpx/*" OR
169        Esql.user_agent_original_lower LIKE "*curl/*" OR
170        Esql.user_agent_original_lower LIKE "*aiohttp/*" OR
171        Esql.user_agent_original_lower LIKE "*fasthttp*" OR
172        Esql.user_agent_original_lower LIKE "*go-http-client/*" OR
173        Esql.user_agent_original_lower LIKE "*go-resty/*" OR
174        Esql.user_agent_original_lower LIKE "*okhttp/*" OR
175        Esql.user_agent_original_lower LIKE "*restsharp/*" OR
176        Esql.user_agent_original_lower LIKE "*postmanruntime/*" OR
177        Esql.user_agent_original_lower LIKE "*axios/*" OR
178        Esql.user_agent_original_lower == "node" OR
179        Esql.user_agent_original_lower LIKE "*node-fetch/*" OR
180        Esql.user_agent_original_lower LIKE "*undici*" OR
181        Esql.user_agent_original_lower LIKE "*azurehound/*" OR
182        Esql.user_agent_original_lower LIKE "*sharphound/*" OR
183        Esql.user_agent_original_lower LIKE "*bloodhound/*" OR
184        Esql.user_agent_original_lower LIKE "*aadinternals*" OR
185        Esql.user_agent_original_lower LIKE "*bav2ropc*" OR
186        Esql.user_agent_original_lower LIKE "*roadtools*" OR
187        Esql.user_agent_original_lower LIKE "*roadrecon*" OR
188        Esql.user_agent_original_lower LIKE "*roadtx*" OR
189        Esql.user_agent_original_lower LIKE "*graphpython*" OR
190        Esql.user_agent_original_lower LIKE "*msgraphx*"
191    ) AND
192    (
193        // Documented collection reads. Query parameters are not part of url.path.
194        Esql.url_path_lower IN (
195            "/v1.0/users",
196            "/beta/users",
197            "/v1.0/groups",
198            "/beta/groups",
199            "/v1.0/applications",
200            "/beta/applications",
201            "/v1.0/serviceprincipals",
202            "/beta/serviceprincipals",
203            "/v1.0/devices",
204            "/beta/devices",
205            "/v1.0/directoryroles",
206            "/beta/directoryroles",
207            "/v1.0/organization",
208            "/beta/organization",
209            "/v1.0/subscribedskus",
210            "/beta/subscribedskus",
211            "/v1.0/rolemanagement/directory/roledefinitions",
212            "/beta/rolemanagement/directory/roledefinitions",
213            "/v1.0/rolemanagement/directory/roleassignments",
214            "/beta/rolemanagement/directory/roleassignments",
215            "/v1.0/rolemanagement/directory/roleeligibilityschedules",
216            "/beta/rolemanagement/directory/roleeligibilityschedules",
217            "/v1.0/rolemanagement/directory/roleeligibilityscheduleinstances",
218            "/beta/rolemanagement/directory/roleeligibilityscheduleinstances"
219        )
220    )
221| WHERE Esql.azure_signinlogs_is_client_signin OR Esql.azure_graphactivitylogs_is_scripted_enum
222
223// aggregate the events for the rule by session, user, app, device, token and tenant
224| STATS
225    Esql.azure_signinlogs_properties_id_count_distinct = COUNT_DISTINCT(
226        azure.signinlogs.properties.id
227    ) WHERE Esql.azure_signinlogs_is_client_signin,
228    Esql.azure_graphactivitylogs_properties_operation_id_count_distinct = COUNT_DISTINCT(
229        azure.graphactivitylogs.properties.operation_id
230    ) WHERE Esql.azure_graphactivitylogs_is_scripted_enum,
231    Esql.url_path_lower_count_distinct = COUNT_DISTINCT(
232        Esql.url_path_lower
233    ) WHERE Esql.azure_graphactivitylogs_is_scripted_enum,
234    Esql.timestamp_client_signin_min = MIN(
235        CASE(Esql.azure_signinlogs_is_client_signin, @timestamp, NULL)
236    ),
237    Esql.timestamp_scripted_enum_min = MIN(
238        CASE(Esql.azure_graphactivitylogs_is_scripted_enum, @timestamp, NULL)
239    ),
240    Esql.user_agent_original_client_signin_values = VALUES(
241        user_agent.original
242    ) WHERE Esql.azure_signinlogs_is_client_signin,
243    Esql.user_agent_original_scripted_enum_values = VALUES(
244        user_agent.original
245    ) WHERE Esql.azure_graphactivitylogs_is_scripted_enum,
246    Esql.url_path_values = VALUES(url.path) WHERE Esql.azure_graphactivitylogs_is_scripted_enum,
247    Esql.url_original_values = VALUES(url.original) WHERE Esql.azure_graphactivitylogs_is_scripted_enum,
248    Esql.source_ip_values = VALUES(source.ip),
249    Esql.azure_signinlogs_properties_id_values = VALUES(
250        azure.signinlogs.properties.id
251    ) WHERE Esql.azure_signinlogs_is_client_signin,
252    Esql.azure_graphactivitylogs_properties_operation_id_values = VALUES(
253        azure.graphactivitylogs.properties.operation_id
254    ) WHERE Esql.azure_graphactivitylogs_is_scripted_enum,
255    Esql.azure_signinlogs_properties_app_display_name_values = VALUES(
256        azure.signinlogs.properties.app_display_name
257    ) WHERE Esql.azure_signinlogs_is_client_signin,
258    Esql_priv.azure_signinlogs_properties_user_principal_name_values = VALUES(
259        azure.signinlogs.properties.user_principal_name
260    ) WHERE Esql.azure_signinlogs_is_client_signin
261  BY
262    Esql.token_id,
263    Esql.session_id,
264    Esql.user_id,
265    Esql.app_id,
266    Esql.device_id,
267    azure.tenant_id
268
269// filter by client sign-in and scripted Graph enumeration events
270| WHERE
271    Esql.azure_signinlogs_properties_id_count_distinct > 0 AND
272    Esql.azure_graphactivitylogs_properties_operation_id_count_distinct > 0 AND
273    Esql.url_path_lower_count_distinct >= 3 AND
274    Esql.timestamp_client_signin_min IS NOT NULL AND
275    Esql.timestamp_scripted_enum_min IS NOT NULL AND
276    Esql.timestamp_scripted_enum_min >= Esql.timestamp_client_signin_min
277| KEEP Esql.*, Esql_priv.*, azure.tenant_id
278'''
279
280[rule.alert_suppression]
281group_by = ["Esql.token_id", "azure.tenant_id"]
282missing_fields_strategy = "suppress"
283
284[rule.alert_suppression.duration]
285unit = "m"
286value = 60
287
288[[rule.threat]]
289framework = "MITRE ATT&CK"
290
291[[rule.threat.technique]]
292id = "T1087"
293name = "Account Discovery"
294reference = "https://attack.mitre.org/techniques/T1087/"
295
296[[rule.threat.technique.subtechnique]]
297id = "T1087.004"
298name = "Cloud Account"
299reference = "https://attack.mitre.org/techniques/T1087/004/"
300
301[[rule.threat.technique]]
302id = "T1526"
303name = "Cloud Service Discovery"
304reference = "https://attack.mitre.org/techniques/T1526/"
305
306[rule.threat.tactic]
307id = "TA0007"
308name = "Discovery"
309reference = "https://attack.mitre.org/tactics/TA0007/"
310
311[[rule.threat]]
312framework = "MITRE ATT&CK"
313
314[[rule.threat.technique]]
315id = "T1550"
316name = "Use Alternate Authentication Material"
317reference = "https://attack.mitre.org/techniques/T1550/"
318
319[[rule.threat.technique.subtechnique]]
320id = "T1550.001"
321name = "Application Access Token"
322reference = "https://attack.mitre.org/techniques/T1550/001/"
323
324[rule.threat.tactic]
325id = "TA0005"
326name = "Defense Evasion"
327reference = "https://attack.mitre.org/tactics/TA0005/"
328
329[rule.investigation_fields]
330field_names = [
331    "Esql.token_id",
332    "Esql.session_id",
333    "Esql.user_id",
334    "Esql_priv.azure_signinlogs_properties_user_principal_name_values",
335    "Esql.app_id",
336    "Esql.azure_signinlogs_properties_app_display_name_values",
337    "Esql.user_agent_original_client_signin_values",
338    "Esql.user_agent_original_scripted_enum_values",
339    "Esql.url_original_values",
340    "Esql.url_path_values",
341    "Esql.url_path_lower_count_distinct",
342    "Esql.source_ip_values",
343    "Esql.device_id",
344]

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.

References

Related rules

to-top