diff --git a/rules/integrations/azure/credential_access_device_code_signin_aad_graph_enum.toml b/rules/integrations/azure/credential_access_device_code_signin_aad_graph_enum.toml new file mode 100644 index 00000000000..9e9d661c8f6 --- /dev/null +++ b/rules/integrations/azure/credential_access_device_code_signin_aad_graph_enum.toml @@ -0,0 +1,224 @@ +[metadata] +creation_date = "2026/05/22" +integration = ["azure"] +maturity = "production" +min_stack_version = "9.2.0" +min_stack_comments = "azure.aadgraphactivitylogs backports to 8.19.10 only." +updated_date = "2026/05/22" + +[rule] +author = ["Elastic"] +description = """ +Correlates a successful Entra ID device-code sign-in to the legacy Azure AD Graph audience +(00000002-0000-0000-c000-000000000000) from an unmanaged device with directory enumeration against graph.windows.net by +the same user within a short window. Device-code phishing is the dominant OAuth phishing variant against Microsoft +tenants: the adversary initiates the flow, relays the user-facing code to the victim, and on redemption walks away with +an access or refresh token bound to the targeted resource without ever handling the user's password or MFA factor. When +the redeemed audience is AAD Graph and the redeeming device is unmanaged, the follow-on Graph traffic is the compromised +cloud account being used by the attacker, not by the user. This rule fires when that token is immediately turned around +against the directory under the same identity to read user, group, service principal, application, role assignment, +directory object, policy, OAuth permission grant, or tenant detail collections. +""" +false_positives = [ + """ + Authorized red team or audit activity (ROADrecon, ROADtools, AADInternals, roadtx). Document the engagement window + and add exceptions on the calling user. + """, + """ + A developer or operator legitimately running first-party tooling under the device-code flow that then enumerates + directory objects during onboarding or troubleshooting. Validate the calling app and source IP and exclude as + appropriate. + """, +] +from = "now-9m" +index = ["logs-azure.signinlogs-*", "logs-azure.aadgraphactivitylogs-*"] +language = "eql" +license = "Elastic License v2" +name = "Entra ID OAuth Device Code Sign-in to Azure AD Graph Enumeration" +note = """## Triage and analysis + +### Investigating Entra ID OAuth Device Code Sign-in to Azure AD Graph Enumeration + +Device-code phishing redeems an OAuth access token directly into the adversary's hands without +ever touching the victim's password or MFA factor. When the redemption targets the legacy AAD +Graph audience from an unmanaged device, the resulting token is overwhelmingly used to drive +directory recon under the compromised identity. ROADrecon / ROADtools, AADInternals +(`Get-AADIntTenantDetails`, `Get-AADIntUsers`), and manual `roadtx` flows all match this shape. + +### Possible investigation steps + +- Confirm the sign-in shape. + - `azure.signinlogs.properties.authentication_protocol` is `deviceCode`. + - `azure.signinlogs.properties.resource_id` is `00000002-0000-0000-c000-000000000000` (legacy AAD Graph audience). + - `azure.signinlogs.properties.device_detail.is_managed` is `false`. +- Identify the calling client used to drive the device-code grant. + - `azure.signinlogs.properties.app_id`, `azure.signinlogs.properties.app_display_name`. + - FOCI / pre-consented Microsoft clients (Teams, Office, Azure CLI, Azure PowerShell) are the canonical ride-along clients for device-code phishing because they bypass app consent. +- Review source posture for the redemption and the Graph follow-on independently. + - `source.ip`, `source.as.organization.name`, `source.geo.country_name`. Residential / VPS / anonymising-network egress raises priority. + - A code redeemed from one IP and Graph driven from another is a strong adversary-in-the-middle signal: the user clicked, the attacker is now driving the session. +- Review what was queried on the Graph side. + - `url.path` on the second event. `applicationRefs`, `eligibleRoleAssignments`, and `directoryObjects` casts (`$/Microsoft.DirectoryServices.ServicePrincipal`) are the textbook ROADrecon signature; `tenantDetails` from an `AADInternals` user-agent is the AADInternals signature. +- Check the API version on the Graph call. + - `azure.aadgraphactivitylogs.properties.api_version`. `1.61-internal` is a strong tooling indicator and returns data the public surface withholds (Conditional Access policies, MFA configuration on user objects). +- Pivot to surrounding sign-ins for the same user. Other device-code redemptions to Microsoft Graph, Azure Resource Manager, or Exchange in the same window suggest the attacker is multi-homing the token harvest. +- Confirm the activity is not attributable to authorized testing before treating as malicious. + +### Response and remediation + +- Revoke refresh tokens and active sessions for the compromised 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}`. +- 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. +- Apply Conditional Access targeting the device-code grant: require a managed / compliant device or block the device-code grant outside of explicitly approved app + user populations. +""" +references = [ + "https://github.com/dirkjanm/ROADtools", + "https://github.com/Gerenios/AADInternals", + "https://learn.microsoft.com/en-us/graph/migrate-azure-ad-graph-overview", +] +risk_score = 73 +rule_id = "aa04377a-19b5-4940-952f-aad173790d23" +setup = """#### Microsoft Entra ID Sign-in Logs and Azure AD Graph Activity Logs +Requires both data streams ingested via the Elastic Azure integration: +- Microsoft Entra ID sign-in logs into `logs-azure.signinlogs-*` (enable the `SignInLogs` diagnostic-settings category on Entra ID). +- Azure AD Graph Activity Logs into `logs-azure.aadgraphactivitylogs-*` (enable the `AzureADGraphActivityLogs` diagnostic-settings category on Entra ID). +""" +severity = "high" +tags = [ + "Domain: Cloud", + "Domain: Identity", + "Data Source: Azure", + "Data Source: Microsoft Entra ID", + "Data Source: Microsoft Entra ID Sign-in Logs", + "Data Source: Azure AD Graph", + "Data Source: Azure AD Graph Activity Logs", + "Use Case: Identity and Access Audit", + "Use Case: Threat Detection", + "Tactic: Credential Access", + "Tactic: Initial Access", + "Tactic: Discovery", + "Resources: Investigation Guide", +] +timestamp_override = "event.ingested" +type = "eql" + +query = ''' +sequence by user.id, azure.tenant_id with maxspan=5m +[authentication where + data_stream.dataset == "azure.signinlogs" and + event.outcome == "success" and + azure.signinlogs.properties.authentication_protocol == "deviceCode" and + azure.signinlogs.properties.device_detail.is_managed == false and + azure.signinlogs.properties.resource_id == "00000002-0000-0000-c000-000000000000"] +[web where + data_stream.dataset == "azure.aadgraphactivitylogs" and + url.path : ( + "*/users*", + "*/groups*", + "*/servicePrincipals*", + "*/applications*", + "*/applicationRefs*", + "*/devices*", + "*/directoryRoles*", + "*/roleAssignments*", + "*/eligibleRoleAssignments*", + "*/roleDefinitions*", + "*/directoryObjects*", + "*/policies*", + "*/oauth2PermissionGrants*", + "*/administrativeUnits*", + "*/tenantDetails*", + "*/directorySettingTemplates*", + "*/me*" + )] +''' + + +[[rule.threat]] +framework = "MITRE ATT&CK" +[[rule.threat.technique]] +id = "T1528" +name = "Steal Application Access Token" +reference = "https://attack.mitre.org/techniques/T1528/" + + +[rule.threat.tactic] +id = "TA0006" +name = "Credential Access" +reference = "https://attack.mitre.org/tactics/TA0006/" +[[rule.threat]] +framework = "MITRE ATT&CK" +[[rule.threat.technique]] +id = "T1078" +name = "Valid Accounts" +reference = "https://attack.mitre.org/techniques/T1078/" +[[rule.threat.technique.subtechnique]] +id = "T1078.004" +name = "Cloud Accounts" +reference = "https://attack.mitre.org/techniques/T1078/004/" + + + +[rule.threat.tactic] +id = "TA0001" +name = "Initial Access" +reference = "https://attack.mitre.org/tactics/TA0001/" +[[rule.threat]] +framework = "MITRE ATT&CK" +[[rule.threat.technique]] +id = "T1069" +name = "Permission Groups Discovery" +reference = "https://attack.mitre.org/techniques/T1069/" +[[rule.threat.technique.subtechnique]] +id = "T1069.003" +name = "Cloud Groups" +reference = "https://attack.mitre.org/techniques/T1069/003/" + + +[[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.technique]] +id = "T1526" +name = "Cloud Service Discovery" +reference = "https://attack.mitre.org/techniques/T1526/" + + +[rule.threat.tactic] +id = "TA0007" +name = "Discovery" +reference = "https://attack.mitre.org/tactics/TA0007/" + +[rule.investigation_fields] +field_names = [ + "user.id", + "azure.tenant_id", + "azure.signinlogs.properties.user_principal_name", + "azure.signinlogs.properties.app_id", + "azure.signinlogs.properties.app_display_name", + "azure.signinlogs.properties.resource_id", + "azure.signinlogs.properties.authentication_protocol", + "azure.signinlogs.properties.device_detail.is_managed", + "azure.aadgraphactivitylogs.properties.app_id", + "azure.aadgraphactivitylogs.properties.api_version", + "url.path", + "user_agent.original", + "source.ip", + "source.as.organization.name", + "source.geo.country_name", +] +