Azure AD Graph High 4xx Error Ratio from User
Detects an unusually high ratio of 4xx HTTP responses from Azure AD Graph (graph.windows.net) per calling identity in a short window. Post-identity compromise leading to recon often leaves a tail of 403s and 404s as tooling walks endpoints it does not have permission for, asks for object IDs it does not have, or uses an OAuth client that has been pulled off the AAD Graph allow-list. Surges or an unexpected ratio of 4xx responses concentrated on a single (user and ASN) pair are characteristic of automated tooling rather than human or first-party traffic.
Elastic rule (View on GitHub)
1[metadata]
2creation_date = "2026/05/20"
3integration = ["azure"]
4maturity = "production"
5updated_date = "2026/05/20"
6
7[rule]
8author = ["Elastic"]
9description = """
10Detects an unusually high ratio of 4xx HTTP responses from Azure AD Graph (graph.windows.net) per calling identity in a
11short window. Post-identity compromise leading to recon often leaves a tail of 403s and 404s as tooling walks endpoints
12it does not have permission for, asks for object IDs it does not have, or uses an OAuth client that has been pulled off
13the AAD Graph allow-list. Surges or an unexpected ratio of 4xx responses concentrated on a single (user and ASN) pair are characteristic of
14automated tooling rather than human or first-party traffic.
15"""
16false_positives = [
17 """
18 Legitimate first-party clients occasionally hit 4xx responses as part of conditional access flows, transient
19 permission changes, or stale token retries. Tune the threshold for your tenant baseline.
20 """,
21 """
22 Authorized red team activity. Document and add exceptions on the user, app ID, or source IP.
23 """,
24 """
25 Legacy tooling may still be using AAD Graph. Validate and add exceptions on the calling app ID after review.
26 """,
27]
28from = "now-8h"
29interval = "1h"
30language = "esql"
31license = "Elastic License v2"
32name = "Azure AD Graph High 4xx Error Ratio from User"
33note = """## Triage and analysis
34
35### Investigating Azure AD Graph High 4xx Error Ratio from User
36
37A high 4xx rate on AAD Graph from a single calling identity is consistent with automated permission probing,
38recon against endpoints the caller is not authorized for, or a token whose client has been blocked from AAD
39Graph. The pattern is structurally distinct from sparse 4xx in first-party traffic.
40
41### Possible investigation steps
42
43- Confirm the surge volume and ratio.
44 - Review `Esql.error_rate` (4xx as a fraction of total) and `Esql.total_calls` to assess the magnitude.
45- Identify the caller and calling client.
46 - `user.id` for the calling identity, `source.ip` for the egress, and `Esql.app_ids` (from `azure.aadgraphactivitylogs.properties.app_id`) for the OAuth client.
47- Review which endpoints produced the errors.
48 - `Esql.sample_paths` captures the distinct `url.path` values that 4xx'd.
49- Correlate with successful calls from the same user / source to understand what reached AAD Graph.
50- Pivot to sign-in logs (`logs-azure.signinlogs-*`) for the same user / source for token-mint context.
51- Confirm the activity is not attributable to authorized testing (red team engagement, penetration test, internal tooling validation) before treating as malicious.
52
53### Response and remediation
54
55- Revoke refresh tokens and active sessions for the calling user if the surge indicates unauthorized recon.
56 - `POST /v1.0/users/{id}/revokeSignInSessions`.
57- Temporarily disable the user if the alert is high-confidence or you need to halt activity while investigation continues.
58 - `PATCH /v1.0/users/{id}` with body `{"accountEnabled": false}`.
59- If the calling application has no legitimate AAD Graph dependency, block further use by that app.
60 - `PATCH /beta/applications/{id}` with body `{"authenticationBehaviors": {"blockAzureADGraphAccess": true}}`.
61 - This property lives on the Graph beta endpoint, not v1.0.
62- Apply Conditional Access targeting the AAD Graph audience for the affected user population.
63"""
64references = [
65 "https://github.com/dirkjanm/ROADtools",
66 "https://learn.microsoft.com/en-us/graph/migrate-azure-ad-graph-overview",
67 "https://aadinternals.com/",
68]
69risk_score = 47
70rule_id = "8cbc7793-9ce4-4b7d-9c20-a30afbde2a05"
71setup = """#### Azure AD Graph Activity Logs
72Requires Azure AD Graph Activity Logs ingested into `logs-azure.aadgraphactivitylogs-*` via the Elastic Azure
73integration. Enable the `AzureADGraphActivityLogs` diagnostic-settings category on Entra ID.
74"""
75severity = "medium"
76tags = [
77 "Domain: Cloud",
78 "Data Source: Azure",
79 "Data Source: Azure AD Graph",
80 "Data Source: Azure AD Graph Activity Logs",
81 "Use Case: Threat Detection",
82 "Tactic: Discovery",
83 "Resources: Investigation Guide",
84]
85timestamp_override = "event.ingested"
86type = "esql"
87
88query = '''
89from logs-azure.aadgraphactivitylogs-* metadata _id, _version, _index
90
91| where data_stream.dataset == "azure.aadgraphactivitylogs"
92| eval Esql.is_4xx = case(
93 http.response.status_code >= 400 and
94 http.response.status_code < 500, 1, 0
95 )
96| eval Esql.time_window = date_trunc(2 minutes, @timestamp)
97| stats
98 Esql.total_calls = count(*),
99 Esql.azure_tenants = values(azure.tenant_id),
100 Esql.errors = sum(Esql.is_4xx),
101 Esql.url_path_count = count_distinct(url.path),
102 Esql.api_versions = values(azure.aadgraphactivitylogs.properties.api_version),
103 Esql.app_ids = values(azure.aadgraphactivitylogs.properties.app_id),
104 Esql.source_ips = values(source.ip),
105 Esql.source_asn_name = values(source.as.organization.name),
106 Esql.user_agents = values(user_agent.original),
107 Esql.first_seen = min(@timestamp),
108 Esql.last_seen = max(@timestamp)
109 by
110 user.id,
111 source.as.number,
112 Esql.time_window
113| eval Esql.error_rate = round(Esql.errors * 1.0 / Esql.total_calls, 2)
114| where
115 Esql.total_calls > 20 and Esql.errors >= 10 and
116 Esql.error_rate >= 0.4 and Esql.url_path_count >= 15
117| keep
118 user.id,
119 source.as.number,
120 Esql.*
121'''
122
123
124[[rule.threat]]
125framework = "MITRE ATT&CK"
126[[rule.threat.technique]]
127id = "T1087"
128name = "Account Discovery"
129reference = "https://attack.mitre.org/techniques/T1087/"
130[[rule.threat.technique.subtechnique]]
131id = "T1087.004"
132name = "Cloud Account"
133reference = "https://attack.mitre.org/techniques/T1087/004/"
134
135
136[[rule.threat.technique]]
137id = "T1526"
138name = "Cloud Service Discovery"
139reference = "https://attack.mitre.org/techniques/T1526/"
140
141
142[rule.threat.tactic]
143id = "TA0007"
144name = "Discovery"
145reference = "https://attack.mitre.org/tactics/TA0007/"
146
147[rule.investigation_fields]
148field_names = ["user.id", "user_agent.original"]
Triage and analysis
Investigating Azure AD Graph High 4xx Error Ratio from User
A high 4xx rate on AAD Graph from a single calling identity is consistent with automated permission probing, recon against endpoints the caller is not authorized for, or a token whose client has been blocked from AAD Graph. The pattern is structurally distinct from sparse 4xx in first-party traffic.
Possible investigation steps
- Confirm the surge volume and ratio.
- Review
Esql.error_rate(4xx as a fraction of total) andEsql.total_callsto assess the magnitude.
- Review
- Identify the caller and calling client.
user.idfor the calling identity,source.ipfor the egress, andEsql.app_ids(fromazure.aadgraphactivitylogs.properties.app_id) for the OAuth client.
- Review which endpoints produced the errors.
Esql.sample_pathscaptures the distincturl.pathvalues that 4xx'd.
- Correlate with successful calls from the same user / source to understand what reached AAD Graph.
- Pivot to sign-in logs (
logs-azure.signinlogs-*) for the same user / source for token-mint context. - Confirm the activity is not attributable to authorized testing (red team engagement, penetration test, internal tooling validation) before treating as malicious.
Response and remediation
- Revoke refresh tokens and active sessions for the calling user if the surge indicates unauthorized recon.
POST /v1.0/users/{id}/revokeSignInSessions.
- Temporarily disable the user if the alert is high-confidence or you need to halt activity while investigation continues.
PATCH /v1.0/users/{id}with body{"accountEnabled": false}.
- If the calling application has no legitimate AAD Graph dependency, block further use by that app.
PATCH /beta/applications/{id}with body{"authenticationBehaviors": {"blockAzureADGraphAccess": true}}.- This property lives on the Graph beta endpoint, not v1.0.
- Apply Conditional Access targeting the AAD Graph audience for the affected user population.
References
Related rules
- Azure AD Graph Access with Suspicious User-Agent
- Entra ID OAuth Device Code Sign-in to Azure AD Graph Enumeration
- Azure AD Graph Potential Enumeration (ROADrecon)
- Microsoft Graph Multi-Category Reconnaissance Burst
- Entra ID Sign-in BloodHound Suite User-Agent Detected