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

to-top