From 5d80866981b6bd434fa1740739a54742233ae3e8 Mon Sep 17 00:00:00 2001 From: Terrance DeJesus Date: Fri, 22 May 2026 12:36:45 -0400 Subject: [PATCH 1/2] [New Rule] Azure AD Graph Access with Unusual Client and User --- ...ery_aad_graph_unusual_client_for_user.toml | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 rules/integrations/azure/discovery_aad_graph_unusual_client_for_user.toml diff --git a/rules/integrations/azure/discovery_aad_graph_unusual_client_for_user.toml b/rules/integrations/azure/discovery_aad_graph_unusual_client_for_user.toml new file mode 100644 index 00000000000..2ccbec303fa --- /dev/null +++ b/rules/integrations/azure/discovery_aad_graph_unusual_client_for_user.toml @@ -0,0 +1,167 @@ +[metadata] +creation_date = "2026/05/20" +integration = ["azure"] +maturity = "production" +updated_date = "2026/05/20" + +[rule] +author = ["Elastic"] +description = """ +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 the previous 14 days. 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. This is +a new-terms style detection that fires on first observation of a (client, user) pair against AAD Graph. +""" +false_positives = [ + """ + First-time use of a legitimate first-party or sanctioned client by the user (newly installed app, first + sign-in to a workload, fresh PowerShell module install). Validate by `azure.aadgraphactivitylogs.properties.app_id` + and the user's history. + """, + """ + Authorized red team or audit activity. Add exceptions on the calling user after review. + """, +] +from = "now-9m" +index = ["logs-azure.aadgraphactivitylogs-*"] +language = "kuery" +license = "Elastic License v2" +name = "Azure AD Graph Access with Unusual Client and User" +note = """## Triage and analysis + +### Investigating Entra ID AAD Graph Access by 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 = [ + "https://github.com/secureworks/family-of-client-ids-research", + "https://learn.microsoft.com/en-us/graph/migrate-azure-ad-graph-overview", + "https://dirkjanm.io/phishing-for-microsoft-entra-primary-refresh-tokens/", +] +risk_score = 47 +rule_id = "fd9d2933-f0f9-4aac-810c-a31f6a4a7890" +setup = """#### Azure AD Graph Activity Logs +Requires Azure AD Graph Activity Logs ingested into `logs-azure.aadgraphactivitylogs-*` via the Elastic Azure +integration. Enable the `AzureADGraphActivityLogs` diagnostic-settings category on Entra ID. +""" +severity = "medium" +tags = [ + "Domain: Cloud", + "Data Source: Azure", + "Data Source: Azure AD Graph", + "Data Source: Azure AD Graph Activity Logs", + "Use Case: Threat Detection", + "Tactic: Defense Evasion", + "Tactic: Discovery", + "Resources: Investigation Guide", +] +timestamp_override = "event.ingested" +type = "new_terms" + +query = ''' +data_stream.dataset:"azure.aadgraphactivitylogs" and + azure.aadgraphactivitylogs.properties.actor_type : "User" and + azure.aadgraphactivitylogs.properties.app_id: (* and not ( + "74658136-14ec-4630-ad9b-26e160ff0fc6" or + "bb8f18b0-9c38-48c9-a847-e1ef3af0602d" or + "00000006-0000-0ff1-ce00-000000000000" or + "18ed3507-a475-4ccb-b669-d66bc9f2a36e") + ) and user.id:* +''' + +[rule.investigation_fields] +field_names = [ + "user.id", + "source.ip", + "source.as.organization.name", + "user_agent.original", + "azure.aadgraphactivitylogs.properties.app_id", + "azure.aadgraphactivitylogs.properties.api_version", + "url.path", + "azure.tenant_id", +] + +[[rule.threat]] +framework = "MITRE ATT&CK" + +[[rule.threat.technique]] +id = "T1550" +name = "Use Alternate Authentication Material" +reference = "https://attack.mitre.org/techniques/T1550/" +[[rule.threat.technique.subtechnique]] +id = "T1550.001" +name = "Application Access Token" +reference = "https://attack.mitre.org/techniques/T1550/001/" + + +[rule.threat.tactic] +id = "TA0005" +name = "Defense Evasion" +reference = "https://attack.mitre.org/tactics/TA0005/" + + +[[rule.threat]] +framework = "MITRE ATT&CK" + +[[rule.threat.technique]] +id = "T1087" +name = "Account Discovery" +reference = "https://attack.mitre.org/techniques/T1087/" +[[rule.threat.technique.subtechnique]] +id = "T1087.004" +name = "Cloud Account" +reference = "https://attack.mitre.org/techniques/T1087/004/" + + +[rule.threat.tactic] +id = "TA0007" +name = "Discovery" +reference = "https://attack.mitre.org/tactics/TA0007/" + +[rule.new_terms] +field = "new_terms_fields" +value = [ + "azure.aadgraphactivitylogs.properties.app_id", + "user.id", +] + +[[rule.new_terms.history_window_start]] +field = "history_window_start" +value = "now-7d" From 4b7340420478e2bb4a8cb2878ce17b880de2beb6 Mon Sep 17 00:00:00 2001 From: Terrance DeJesus Date: Fri, 22 May 2026 12:54:30 -0400 Subject: [PATCH 2/2] rule updates --- ...ery_aad_graph_unusual_client_for_user.toml | 59 +++++++++---------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/rules/integrations/azure/discovery_aad_graph_unusual_client_for_user.toml b/rules/integrations/azure/discovery_aad_graph_unusual_client_for_user.toml index 2ccbec303fa..9e87e671aaf 100644 --- a/rules/integrations/azure/discovery_aad_graph_unusual_client_for_user.toml +++ b/rules/integrations/azure/discovery_aad_graph_unusual_client_for_user.toml @@ -1,28 +1,26 @@ [metadata] -creation_date = "2026/05/20" +creation_date = "2026/05/22" integration = ["azure"] maturity = "production" -updated_date = "2026/05/20" +updated_date = "2026/05/22" [rule] author = ["Elastic"] description = """ 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 the previous 14 days. 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. This is -a new-terms style detection that fires on first observation of a (client, user) pair against AAD Graph. +(`azure.aadgraphactivitylogs.properties.app_id`) and signed-in user (`user.id`) has not been observed in the tenant in +the previous 14 days. 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. This is a new-terms style detection that fires on first +observation of a (client, user) pair against AAD Graph. """ false_positives = [ """ - First-time use of a legitimate first-party or sanctioned client by the user (newly installed app, first - sign-in to a workload, fresh PowerShell module install). Validate by `azure.aadgraphactivitylogs.properties.app_id` - and the user's history. - """, - """ - Authorized red team or audit activity. Add exceptions on the calling user after review. + First-time use of a legitimate first-party or sanctioned client by the user (newly installed app, first sign-in to a + workload, fresh PowerShell module install). Validate by `azure.aadgraphactivitylogs.properties.app_id` and the + user's history. """, + " Authorized red team or audit activity. Add exceptions on the calling user after review.\n ", ] from = "now-9m" index = ["logs-azure.aadgraphactivitylogs-*"] @@ -106,21 +104,9 @@ data_stream.dataset:"azure.aadgraphactivitylogs" and ) and user.id:* ''' -[rule.investigation_fields] -field_names = [ - "user.id", - "source.ip", - "source.as.organization.name", - "user_agent.original", - "azure.aadgraphactivitylogs.properties.app_id", - "azure.aadgraphactivitylogs.properties.api_version", - "url.path", - "azure.tenant_id", -] [[rule.threat]] framework = "MITRE ATT&CK" - [[rule.threat.technique]] id = "T1550" name = "Use Alternate Authentication Material" @@ -131,15 +117,13 @@ name = "Application Access Token" reference = "https://attack.mitre.org/techniques/T1550/001/" + [rule.threat.tactic] id = "TA0005" name = "Defense Evasion" reference = "https://attack.mitre.org/tactics/TA0005/" - - [[rule.threat]] framework = "MITRE ATT&CK" - [[rule.threat.technique]] id = "T1087" name = "Account Discovery" @@ -150,18 +134,29 @@ name = "Cloud Account" reference = "https://attack.mitre.org/techniques/T1087/004/" + [rule.threat.tactic] id = "TA0007" name = "Discovery" reference = "https://attack.mitre.org/tactics/TA0007/" -[rule.new_terms] -field = "new_terms_fields" -value = [ - "azure.aadgraphactivitylogs.properties.app_id", +[rule.investigation_fields] +field_names = [ "user.id", + "source.ip", + "source.as.organization.name", + "user_agent.original", + "azure.aadgraphactivitylogs.properties.app_id", + "azure.aadgraphactivitylogs.properties.api_version", + "url.path", + "azure.tenant_id", ] +[rule.new_terms] +field = "new_terms_fields" +value = ["azure.aadgraphactivitylogs.properties.app_id", "user.id"] [[rule.new_terms.history_window_start]] field = "history_window_start" value = "now-7d" + +