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, andEsql.azure_graphactivitylogs_properties_operation_id_valuesto pivot to the original events. - Compare
Esql.user_agent_original_client_signin_valueswithEsql.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_valuesandEsql.url_path_valuesto determine what identities, groups, applications, service principals, roles, or tenant metadata were enumerated.Esql.url_path_lower_count_distinctmust 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 ofurl.pathand 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 loadingvaultcli.dll. - Do not require
azure.graphactivitylogs.properties.token_issued_atto 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
- Entra ID OAuth User Impersonation to Microsoft Graph
- Azure AD Graph Access with Unusual Client and User
- Entra ID Conditional Access MFA Bypass with Unusual User, Client and Source ASN
- Entra ID Device-Bound PRT from Unusual Device IP
- Entra ID OAuth Device Code Sign-in to Azure AD Graph Enumeration