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/05/22"
  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]
 94timestamp_override = "event.ingested"
 95type = "new_terms"
 96
 97query = '''
 98data_stream.dataset:"azure.aadgraphactivitylogs" and
 99    azure.aadgraphactivitylogs.properties.actor_type : "User" and
100    azure.aadgraphactivitylogs.properties.app_id: (* and not (
101        "74658136-14ec-4630-ad9b-26e160ff0fc6" or
102        "bb8f18b0-9c38-48c9-a847-e1ef3af0602d" or
103        "00000006-0000-0ff1-ce00-000000000000" or
104        "18ed3507-a475-4ccb-b669-d66bc9f2a36e")
105    ) and user.id:*
106'''
107
108[[rule.threat]]
109framework = "MITRE ATT&CK"
110[[rule.threat.technique]]
111id = "T1087"
112name = "Account Discovery"
113reference = "https://attack.mitre.org/techniques/T1087/"
114[[rule.threat.technique.subtechnique]]
115id = "T1087.004"
116name = "Cloud Account"
117reference = "https://attack.mitre.org/techniques/T1087/004/"
118
119[rule.threat.tactic]
120id = "TA0007"
121name = "Discovery"
122reference = "https://attack.mitre.org/tactics/TA0007/"
123
124[[rule.threat]]
125framework = "MITRE ATT&CK"
126[[rule.threat.technique]]
127id = "T1550"
128name = "Use Alternate Authentication Material"
129reference = "https://attack.mitre.org/techniques/T1550/"
130[[rule.threat.technique.subtechnique]]
131id = "T1550.001"
132name = "Application Access Token"
133reference = "https://attack.mitre.org/techniques/T1550/001/"
134
135[rule.threat.tactic]
136id = "TA0005"
137name = "Defense Evasion"
138reference = "https://attack.mitre.org/tactics/TA0005/"
139
140
141[rule.investigation_fields]
142field_names = [
143    "user.id",
144    "source.ip",
145    "source.as.organization.name",
146    "user_agent.original",
147    "azure.aadgraphactivitylogs.properties.app_id",
148    "azure.aadgraphactivitylogs.properties.api_version",
149    "url.path",
150    "azure.tenant_id",
151]
152
153[rule.new_terms]
154field = "new_terms_fields"
155value = ["azure.aadgraphactivitylogs.properties.app_id", "user.id"]
156[[rule.new_terms.history_window_start]]
157field = "history_window_start"
158value = "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}', then DELETE /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}/registeredDevices and GET /v1.0/users/{id}/ownedDevices, then DELETE /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

to-top