Microsoft Graph Multi-Category Reconnaissance Burst
Detects Microsoft Graph activity from delegated user tokens (public client, client_auth_method 0) where a single user session and source IP rapidly touches multiple high-value Graph paths indicative of reconnaissance. The query classifies requests into categories such as role discovery, cross-tenant relationship queries, mailbox paths, contact harvesting, and organization or licensing metadata. When three or more distinct categories appear within a short burst window, it suggests a broad enumeration playbook rather than normal application traffic.
Elastic rule (View on GitHub)
1[metadata]
2creation_date = "2026/05/14"
3integration = ["azure"]
4maturity = "production"
5updated_date = "2026/05/14"
6
7[rule]
8author = ["Elastic"]
9description = """
10Detects Microsoft Graph activity from delegated user tokens (public client, client_auth_method 0) where a single user
11session and source IP rapidly touches multiple high-value Graph paths indicative of reconnaissance. The query classifies
12requests into categories such as role discovery, cross-tenant relationship queries, mailbox paths, contact harvesting,
13and organization or licensing metadata. When three or more distinct categories appear within a short burst window, it
14suggests a broad enumeration playbook rather than normal application traffic.
15"""
16false_positives = [
17 """
18 Legitimate first-party or line-of-business applications that use delegated permissions and enumerate several Graph
19 resources during onboarding or sync may match. Baseline known app IDs and tune thresholds or path lists for your
20 tenant.
21 """,
22]
23from = "now-6m"
24language = "esql"
25license = "Elastic License v2"
26name = "Microsoft Graph Multi-Category Reconnaissance Burst"
27note = """## Triage and analysis
28
29### Investigating Microsoft Graph Multi-Category Reconnaissance Burst
30
31This rule uses an aggregation-based ES|QL query. Alert documents contain summarized fields; pivot to raw Graph activity
32logs using user principal object ID, session ID (c_sid), source IP, tenant ID, and timestamps from the alert.
33
34### Possible investigation steps
35
36- Review Esql.categories and Esql.sample_paths to see which Graph endpoints were touched and whether they align with the app purpose.
37- Validate azure.graphactivitylogs.properties.app_id and user_agent.original against approved applications.
38- Correlate with Entra ID sign-in logs for the same user and session for MFA, conditional access, and token issuance context.
39- Check whether failed_calls indicates probing or permission errors versus successful enumeration.
40
41### Response and remediation
42
43- If malicious, revoke refresh tokens for the user, disable or restrict the application consent, and reset credentials per policy.
44- Add conditional access or block rules for high-risk Graph patterns where appropriate.
45"""
46risk_score = 47
47rule_id = "8e66c55f-8db6-4e3e-bf4f-3a3e242bdf66"
48setup = """#### Microsoft Graph Activity Logs
49Requires Microsoft Graph Activity Logs ingested into `logs-azure.graphactivitylogs-*` (for example via Azure Event Hub).
50"""
51severity = "medium"
52tags = [
53 "Domain: Cloud",
54 "Domain: Identity",
55 "Domain: API",
56 "Data Source: Azure",
57 "Data Source: Microsoft Entra ID",
58 "Data Source: Microsoft Graph",
59 "Data Source: Microsoft Graph Activity Logs",
60 "Use Case: Threat Detection",
61 "Tactic: Discovery",
62 "Resources: Investigation Guide",
63]
64timestamp_override = "event.ingested"
65type = "esql"
66
67query = '''
68from logs-azure.graphactivitylogs-* metadata _id, _version, _index
69
70// Graph calls via delegated user tokens (any status, any method)
71| where event.dataset == "azure.graphactivitylogs"
72 and azure.graphactivitylogs.properties.c_idtyp == "user"
73 and azure.graphactivitylogs.properties.client_auth_method == 0
74
75// high-value recon endpoints by url.path
76| eval Esql.is_role_enum = case(
77 url.path like "*roleManagement/directory*"
78 or url.path like "*memberOf/microsoft.graph.directoryRole*"
79 or url.path like "*transitiveRoleAssignments*",
80 true,
81 false
82 )
83| eval Esql.is_cross_tenant_enum = case(
84 url.path like "*tenantRelationships*"
85 or url.path like "*getResourceTenants*",
86 true,
87 false
88 )
89| eval Esql.is_mailbox_recon = case(
90 url.path like "*mailboxSettings*"
91 or url.path like "*mailFolders*"
92 or url.path like "*messages*"
93 or url.path like "*inbox*",
94 true,
95 false
96 )
97| eval Esql.is_contact_harvest = case(
98 url.path like "*contacts*"
99 or url.path like "*contactFolders*",
100 true,
101 false
102 )
103| eval Esql.is_org_recon = case(
104 url.path like "*subscribedSkus*"
105 or url.path like "*appRoleAssign*"
106 or (
107 url.path like "*/organization*"
108 and not url.path like "*branding*"
109 and not url.path like "*localizations*"
110 ),
111 true,
112 false
113 )
114
115// Combine: is this request hitting a high-value endpoint?
116| eval Esql.is_high_value = case(
117 Esql.is_role_enum or Esql.is_cross_tenant_enum or Esql.is_mailbox_recon
118 or Esql.is_contact_harvest or Esql.is_org_recon,
119 true,
120 false
121 )
122| where Esql.is_high_value == true
123
124// Classify each hit into a recon category
125| eval Esql.recon_category = case(
126 Esql.is_role_enum, "role_discovery",
127 Esql.is_cross_tenant_enum, "cross_tenant_recon",
128 Esql.is_mailbox_recon, "mailbox_recon",
129 Esql.is_contact_harvest, "contact_harvesting",
130 Esql.is_org_recon, "org_and_licensing_recon",
131 "other"
132 )
133
134// Flag failed requests (recon that errored is still recon)
135| eval Esql.is_failed_request = case(
136 http.response.status_code >= 400, true, false
137 )
138
139// Aggregate per user + session + source IP
140| stats
141 Esql.total_high_value_calls = count(*),
142 Esql.distinct_categories = count_distinct(Esql.recon_category),
143 Esql.distinct_paths = count_distinct(url.path),
144 Esql.failed_calls = sum(case(Esql.is_failed_request, 1, 0)),
145 Esql.categories = values(Esql.recon_category),
146 Esql.sample_paths = values(url.path),
147 Esql.http_methods = values(http.request.method),
148 Esql.status_codes = values(http.response.status_code),
149 Esql.first_seen = min(@timestamp),
150 Esql.last_seen = max(@timestamp),
151 Esql.user_agents = values(user_agent.original),
152 Esql.app_ids = values(azure.graphactivitylogs.properties.app_id)
153 by
154 azure.graphactivitylogs.properties.user_principal_object_id,
155 source.ip,
156 source.`as`.organization.name,
157 source.`as`.number,
158 azure.graphactivitylogs.properties.c_sid,
159 azure.tenant_id
160
161// Threshold: 3+ distinct recon categories
162| where Esql.distinct_categories >= 4 and Esql.total_high_value_calls >= 20
163
164// Burst duration in seconds
165| eval Esql.burst_duration_seconds = date_diff("seconds", Esql.first_seen, Esql.last_seen)
166| where Esql.burst_duration_seconds <= 60
167
168| keep
169 azure.graphactivitylogs.properties.user_principal_object_id,
170 azure.graphactivitylogs.properties.c_sid,
171 azure.tenant_id,
172 source.ip,
173 source.`as`.organization.name,
174 source.`as`.number,
175 Esql.*
176'''
177
178[[rule.threat]]
179framework = "MITRE ATT&CK"
180
181[[rule.threat.technique]]
182id = "T1526"
183name = "Cloud Service Discovery"
184reference = "https://attack.mitre.org/techniques/T1526/"
185
186[[rule.threat.technique]]
187id = "T1087"
188name = "Account Discovery"
189reference = "https://attack.mitre.org/techniques/T1087/"
190
191[rule.threat.tactic]
192id = "TA0007"
193name = "Discovery"
194reference = "https://attack.mitre.org/tactics/TA0007/"
Triage and analysis
Investigating Microsoft Graph Multi-Category Reconnaissance Burst
This rule uses an aggregation-based ES|QL query. Alert documents contain summarized fields; pivot to raw Graph activity logs using user principal object ID, session ID (c_sid), source IP, tenant ID, and timestamps from the alert.
Possible investigation steps
- Review Esql.categories and Esql.sample_paths to see which Graph endpoints were touched and whether they align with the app purpose.
- Validate azure.graphactivitylogs.properties.app_id and user_agent.original against approved applications.
- Correlate with Entra ID sign-in logs for the same user and session for MFA, conditional access, and token issuance context.
- Check whether failed_calls indicates probing or permission errors versus successful enumeration.
Response and remediation
- If malicious, revoke refresh tokens for the user, disable or restrict the application consent, and reset credentials per policy.
- Add conditional access or block rules for high-risk Graph patterns where appropriate.
Related rules
- Azure Service Principal Sign-In Followed by Arc Cluster Credential Access
- Entra ID Custom Domain Added or Verified
- Entra ID External Authentication Methods (EAM) Modified
- Entra ID OAuth Authorization Code Grant for Unusual User, App, and Resource
- Entra ID OAuth PRT Issuance to Non-Managed Device Detected