Azure AD Graph Access with Unusual Client and User
Identifies Azure AD Graph (graph.windows.net) requests where the combination of calling OAuth client ("azure.aadgraphactivitylogs.properties.app_id") and signed-in user ("user.id") has not been observed in the tenant in a historical window. A user appearing against AAD Graph under an OAuth client that has not previously authenticated that user is a sign of a FOCI swap, a phished refresh token being redeemed for a new client, or an adversary running tooling under a client identity the user does not normally use.
Elastic rule (View on GitHub)
1[metadata]
2creation_date = "2026/05/22"
3integration = ["azure"]
4maturity = "production"
5updated_date = "2026/09/18"
6
7[rule]
8author = ["Elastic"]
9description = """
10Identifies Azure AD Graph (graph.windows.net) requests where the combination of calling OAuth client
11("azure.aadgraphactivitylogs.properties.app_id") and signed-in user ("user.id") has not been observed in the tenant in
12a historical window. A user appearing against AAD Graph under an OAuth client that has not previously authenticated
13that user is a sign of a FOCI swap, a phished refresh token being redeemed for a new client, or an adversary running
14tooling under a client identity the user does not normally use.
15"""
16false_positives = [
17 """
18 First-time use of a legitimate first-party or sanctioned client by the user (newly installed app, first sign-in to a
19 workload, fresh PowerShell module install). Validate by `azure.aadgraphactivitylogs.properties.app_id` and the
20 user's history.
21 """,
22 """
23 Authorized red team or audit activity. Add exceptions on the calling user after review.
24 """,
25]
26from = "now-9m"
27index = ["logs-azure.aadgraphactivitylogs-*"]
28language = "kuery"
29license = "Elastic License v2"
30name = "Azure AD Graph Access with Unusual Client and User"
31note = """## Triage and analysis
32
33### Investigating Azure AD Graph Access with Unusual Client and User
34
35A (client, user) pair appearing on AAD Graph for the first time in 14 days is a high-signal indicator. Legacy
36AAD Graph traffic is dominated by a small set of recognised first-party callers per user; new combinations
37suggest one of:
38
39- A FOCI refresh-token swap landed under a new client identity.
40- The user newly consented to (or was phished into consenting to) an OAuth application.
41- An adversary is using stolen tokens / credentials under a client identity the user does not normally use.
42
43### Possible investigation steps
44
45- Identify the calling client and confirm whether the app is sanctioned.
46 - `azure.aadgraphactivitylogs.properties.app_id`. Pivot to Azure portal → Enterprise Applications to check whether it is first-party, sanctioned third-party, or unfamiliar.
47- Identify the user and check the surrounding auth context.
48 - `user.id`, then pivot to sign-in logs (`logs-azure.signinlogs-*`) for the same user around the same time. Look for unusual sign-in geography, MFA bypass, or risky session signals.
49- Review source posture.
50 - `user_agent.original`, `source.ip`, `source.as.organization.name`. Residential / VPS / anonymising-network egress raises priority.
51- Review what was queried.
52 - `url.path`. Bulk recon or User-collection access via internal API versions raises triage priority.
53- Check tenant-wide blast radius for the client.
54 - Is the same client ID hitting AAD Graph for many other users? That pattern points to large-scale consent abuse rather than a single account compromise.
55- Confirm the activity is not attributable to authorized testing (red team engagement, penetration test, internal tooling validation) before treating as malicious.
56
57### Response and remediation
58
59- Revoke refresh tokens and active sessions for the calling user.
60 - `POST /v1.0/users/{id}/revokeSignInSessions`.
61- Temporarily disable the user if the alert is high-confidence or you need to halt further activity while investigation continues.
62 - `PATCH /v1.0/users/{id}` with body `{"accountEnabled": false}`.
63- If the client is not a sanctioned application, revoke its OAuth consent.
64 - `GET /v1.0/oauth2PermissionGrants?$filter=clientId eq '{servicePrincipalId}'`, then `DELETE /v1.0/oauth2PermissionGrants/{grantId}`.
65- Check for device registrations created by the user during or around the burst window and remove rogue devices.
66 - `GET /v1.0/users/{id}/registeredDevices` and `GET /v1.0/users/{id}/ownedDevices`, then `DELETE /v1.0/devices/{deviceObjectId}`.
67 - Do this BEFORE session revocation: device-bound PRTs survive `revokeSignInSessions`.
68- If the calling application has no legitimate AAD Graph dependency, block further use by that app.
69 - `PATCH /beta/applications/{id}` with body `{"authenticationBehaviors": {"blockAzureADGraphAccess": true}}`.
70 - This property lives on the Graph beta endpoint, not v1.0.
71"""
72references = [
73 "https://github.com/secureworks/family-of-client-ids-research",
74 "https://learn.microsoft.com/en-us/graph/migrate-azure-ad-graph-overview",
75 "https://dirkjanm.io/phishing-for-microsoft-entra-primary-refresh-tokens/",
76]
77risk_score = 47
78rule_id = "fd9d2933-f0f9-4aac-810c-a31f6a4a7890"
79setup = """#### Azure AD Graph Activity Logs
80Requires Azure AD Graph Activity Logs ingested into `logs-azure.aadgraphactivitylogs-*` via the Elastic Azure
81integration. Enable the `AzureADGraphActivityLogs` diagnostic-settings category on Entra ID.
82"""
83severity = "medium"
84tags = [
85 "Domain: Cloud",
86 "Data Source: Azure",
87 "Data Source: Azure AD Graph",
88 "Data Source: Azure AD Graph Activity Logs",
89 "Use Case: Threat Detection",
90 "Tactic: Defense Evasion",
91 "Tactic: Discovery",
92 "Resources: Investigation Guide",
93 "Noise: Medium",
94 "Performance: Normal",
95 "Rule Type: New Terms",
96 "Platform: Entra ID",
97 "Platform: Azure",
98 "Domain: Identity",
99]
100timestamp_override = "event.ingested"
101type = "new_terms"
102
103query = '''
104data_stream.dataset:"azure.aadgraphactivitylogs" and
105 azure.aadgraphactivitylogs.properties.actor_type : "User" and
106 azure.aadgraphactivitylogs.properties.app_id: (* and not (
107 "74658136-14ec-4630-ad9b-26e160ff0fc6" or
108 "bb8f18b0-9c38-48c9-a847-e1ef3af0602d" or
109 "00000006-0000-0ff1-ce00-000000000000" or
110 "18ed3507-a475-4ccb-b669-d66bc9f2a36e")
111 ) and user.id:*
112'''
113
114[[rule.threat]]
115framework = "MITRE ATT&CK"
116[[rule.threat.technique]]
117id = "T1087"
118name = "Account Discovery"
119reference = "https://attack.mitre.org/techniques/T1087/"
120[[rule.threat.technique.subtechnique]]
121id = "T1087.004"
122name = "Cloud Account"
123reference = "https://attack.mitre.org/techniques/T1087/004/"
124
125[rule.threat.tactic]
126id = "TA0007"
127name = "Discovery"
128reference = "https://attack.mitre.org/tactics/TA0007/"
129
130[[rule.threat]]
131framework = "MITRE ATT&CK"
132[[rule.threat.technique]]
133id = "T1550"
134name = "Use Alternate Authentication Material"
135reference = "https://attack.mitre.org/techniques/T1550/"
136[[rule.threat.technique.subtechnique]]
137id = "T1550.001"
138name = "Application Access Token"
139reference = "https://attack.mitre.org/techniques/T1550/001/"
140
141[rule.threat.tactic]
142id = "TA0005"
143name = "Defense Evasion"
144reference = "https://attack.mitre.org/tactics/TA0005/"
145
146
147[rule.investigation_fields]
148field_names = [
149 "user.id",
150 "source.ip",
151 "source.as.organization.name",
152 "user_agent.original",
153 "azure.aadgraphactivitylogs.properties.app_id",
154 "azure.aadgraphactivitylogs.properties.api_version",
155 "url.path",
156 "azure.tenant_id",
157]
158
159[rule.new_terms]
160field = "new_terms_fields"
161value = ["azure.aadgraphactivitylogs.properties.app_id", "user.id"]
162[[rule.new_terms.history_window_start]]
163field = "history_window_start"
164value = "now-7d"
Triage and analysis
Investigating Azure AD Graph Access with Unusual Client and User
A (client, user) pair appearing on AAD Graph for the first time in 14 days is a high-signal indicator. Legacy AAD Graph traffic is dominated by a small set of recognised first-party callers per user; new combinations suggest one of:
- A FOCI refresh-token swap landed under a new client identity.
- The user newly consented to (or was phished into consenting to) an OAuth application.
- An adversary is using stolen tokens / credentials under a client identity the user does not normally use.
Possible investigation steps
- Identify the calling client and confirm whether the app is sanctioned.
azure.aadgraphactivitylogs.properties.app_id. Pivot to Azure portal → Enterprise Applications to check whether it is first-party, sanctioned third-party, or unfamiliar.
- Identify the user and check the surrounding auth context.
user.id, then pivot to sign-in logs (logs-azure.signinlogs-*) for the same user around the same time. Look for unusual sign-in geography, MFA bypass, or risky session signals.
- Review source posture.
user_agent.original,source.ip,source.as.organization.name. Residential / VPS / anonymising-network egress raises priority.
- Review what was queried.
url.path. Bulk recon or User-collection access via internal API versions raises triage priority.
- Check tenant-wide blast radius for the client.
- Is the same client ID hitting AAD Graph for many other users? That pattern points to large-scale consent abuse rather than a single account compromise.
- 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.
POST /v1.0/users/{id}/revokeSignInSessions.
- Temporarily disable the user if the alert is high-confidence or you need to halt further activity while investigation continues.
PATCH /v1.0/users/{id}with body{"accountEnabled": false}.
- If the client is not a sanctioned application, revoke its OAuth consent.
GET /v1.0/oauth2PermissionGrants?$filter=clientId eq '{servicePrincipalId}', thenDELETE /v1.0/oauth2PermissionGrants/{grantId}.
- Check for device registrations created by the user during or around the burst window and remove rogue devices.
GET /v1.0/users/{id}/registeredDevicesandGET /v1.0/users/{id}/ownedDevices, thenDELETE /v1.0/devices/{deviceObjectId}.- Do this BEFORE session revocation: device-bound PRTs survive
revokeSignInSessions.
- 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.
References
Related rules
- Azure AD Graph Access with Unusual User and ASN
- Entra ID OAuth Device Code Sign-in to Azure AD Graph Enumeration
- Entra ID OAuth user_impersonation Scope for Unusual User and Client
- Azure AD Graph Access with Suspicious User-Agent
- Azure AD Graph High 4xx Error Ratio from User