[Rule Tuning] Entra ID OAuth Device Code Flow with Concurrent Sign-ins (#5594)

Fixes #5593
This commit is contained in:
Terrance DeJesus
2026-01-23 16:25:51 -05:00
committed by GitHub
parent 15aacaba70
commit 04b99c8ec1
@@ -2,19 +2,22 @@
creation_date = "2025/12/02"
integration = ["azure"]
maturity = "production"
updated_date = "2025/12/10"
updated_date = "2026/01/21"
[rule]
author = ["Elastic"]
description = """
Identifies concurrent Entra ID sign-in events for the same user and session from multiple sources, and where one of the
authentication event has some suspicious properties often associated to DeviceCode and OAuth phishing. Adversaries may
steal Refresh Tokens (RTs) via phishing to bypass multi-factor authentication (MFA) and gain unauthorized access to
Azure resources.
Identifies Entra ID device code authentication flows where multiple user agents are observed within the same session.
This pattern is indicative of device code phishing, where an attacker's polling client (e.g., Python script) and the
victim's browser both appear in the same authentication session. In legitimate device code flows, the user authenticates
via browser while the requesting application polls for tokens - when these have distinctly different user agents
(e.g., Python Requests vs Chrome), it may indicate the code was phished and redeemed by an attacker.
"""
false_positives = [
"""
Users authenticating from multiple devices and using the deviceCode protocol or the Visual Studio Code client.
Legitimate use of device code flow where a user authenticates via browser for a CLI tool or headless application.
Common legitimate scenarios include Azure CLI, Azure PowerShell, or VS Code remote development. Review the user
agent combinations - browser + known CLI tool from the same user may be expected behavior.
""",
]
from = "now-9m"
@@ -75,27 +78,29 @@ from logs-azure.signinlogs-* metadata _id, _version, _index
| where event.category == "authentication" and event.dataset == "azure.signinlogs" and
azure.signinlogs.properties.original_transfer_method == "deviceCodeFlow"
| Eval Esql.interactive_logon = CASE(azure.signinlogs.category == "SignInLogs", source.ip, null),
Esql.non_interactive_logon = CASE(azure.signinlogs.category == "NonInteractiveUserSignInLogs", source.ip, null)
// Track events with deviceCode authentication protocol (browser auth) vs polling client
| eval is_device_code_auth = case(azure.signinlogs.properties.authentication_protocol == "deviceCode", 1, 0)
| stats Esql.count_logon = count(*),
Esql.device_code_auth_count = sum(is_device_code_auth),
Esql.timestamp_values = values(@timestamp),
Esql.source_ip_count_distinct = count_distinct(source.ip),
Esql.is_interactive = count(Esql.interactive_logon),
Esql.is_non_interactive = count(Esql.non_interactive_logon),
Esql.user_agent_count_distinct = COUNT_DISTINCT(user_agent.original),
Esql.user_agent_values = VALUES(user_agent.original),
Esql.user_agent_count_distinct = count_distinct(user_agent.original),
Esql.user_agent_values = values(user_agent.original),
Esql.authentication_protocol_values = values(azure.signinlogs.properties.authentication_protocol),
Esql.azure_signinlogs_properties_client_app_values = values(azure.signinlogs.properties.app_display_name),
Esql.azure_signinlogs_properties_client_app_values = values(azure.signinlogs.properties.app_id),
Esql.azure_signinlogs_properties_app_id_values = values(azure.signinlogs.properties.app_id),
Esql.azure_signinlogs_properties_resource_display_name_values = values(azure.signinlogs.properties.resource_display_name),
Esql.azure_signinlogs_properties_auth_requirement_values = values(azure.signinlogs.properties.authentication_requirement),
Esql.azure_signinlogs_properties_tenant_id = values(azure.tenant_id),
Esql.azure_signinlogs_properties_status_error_code_values = values(azure.signinlogs.properties.status.error_code),
Esql.message_values = values(message),
Esql.azure_signinlogs_properties_resource_id_values = values(azure.signinlogs.properties.resource_id),
Esql.source_ip_values = VALUES(source.ip) by azure.signinlogs.properties.session_id, azure.signinlogs.identity
Esql.source_ip_values = values(source.ip)
by azure.signinlogs.properties.session_id, azure.signinlogs.identity
| where Esql.is_interactive >= 2 and Esql.is_non_interactive >= 1 and (Esql.source_ip_count_distinct >= 2 or Esql.user_agent_count_distinct >= 2)
// Require: 2+ events, at least one deviceCode auth protocol event, and either 2+ IPs or 2+ user agents
| where Esql.count_logon >= 2 and Esql.device_code_auth_count >= 1 and (Esql.source_ip_count_distinct >= 2 or Esql.user_agent_count_distinct >= 2)
| keep
Esql.*,
azure.signinlogs.properties.session_id,