Detection rules › Elastic
Entra ID Concurrent Sign-in with Suspicious Properties
Identifies concurrent azure signin events for the same user and 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.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | T1078.004 Valid Accounts: Cloud Accounts, T1566.002 Phishing: Spearphishing Link |
| Stealth | T1078.004 Valid Accounts: Cloud Accounts |
| Credential Access | T1528 Steal Application Access Token |
| Lateral Movement | T1550.001 Use Alternate Authentication Material: Application Access Token |
Rule body elastic
[metadata]
creation_date = "2025/04/28"
integration = ["azure"]
maturity = "production"
updated_date = "2026/04/10"
[rule]
author = ["Elastic"]
description = """
Identifies concurrent azure signin events for the same user and 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.
"""
false_positives = [
"""
Users authenticating from multiple devices and using the deviceCode protocol or the Visual Studio Code client.
""",
]
from = "now-60m"
language = "esql"
license = "Elastic License v2"
name = "Entra ID Concurrent Sign-in with Suspicious Properties"
note = """## Triage and analysis
### Investigating Entra ID Concurrent Sign-in with Suspicious Properties
### Possible investigation steps
- Review the sign-in logs to assess the context and reputation of the source.ip address.
- Investigate the user account associated with the successful sign-in to determine if the activity aligns with expected behavior or if it appears suspicious.
- Check for any recent changes or anomalies in the user's account settings or permissions that could indicate compromise.
- Review the history of sign-ins for the user to identify any patterns or unusual access times that could suggest unauthorized access.
- Assess the device from which the sign-in was attempted to ensure it is a recognized and authorized device for the user.
### Response and remediation
- Immediately revoke the compromised Primary Refresh Tokens (PRTs) to prevent further unauthorized access. This can be done through the Azure portal by navigating to the user's account and invalidating all active sessions.
- Enforce a password reset for the affected user accounts to ensure that any credentials potentially compromised during the attack are no longer valid.
- Implement additional Conditional Access policies that require device compliance checks and restrict access to trusted locations or devices only, to mitigate the risk of future PRT abuse.
- Conduct a thorough review of the affected accounts' recent activity logs to identify any unauthorized actions or data access that may have occurred during the compromise.
- Escalate the incident to the security operations team for further investigation and to determine if there are any broader implications or additional compromised accounts.
- Enhance monitoring by configuring alerts for unusual sign-in patterns or device code authentication attempts from unexpected locations or devices, to improve early detection of similar threats.
- Coordinate with the incident response team to perform a post-incident analysis and update the incident response plan with lessons learned from this event."""
references = [
"https://learn.microsoft.com/en-us/entra/identity/",
"https://learn.microsoft.com/en-us/entra/identity/monitoring-health/concept-sign-ins",
"https://docs.microsoft.com/en-us/azure/active-directory/reports-monitoring/reference-azure-monitor-sign-ins-log-schema",
"https://www.volexity.com/blog/2025/04/22/phishing-for-codes-russian-threat-actors-target-microsoft-365-oauth-workflows/",
]
risk_score = 73
rule_id = "e3bd85e9-7aff-46eb-b60e-20dfc9020d98"
setup = """#### Required Azure Entra Sign-In Logs
This rule requires the Azure logs integration be enabled and configured to collect all logs, including sign-in logs from Entra. In Entra, sign-in logs must be enabled and streaming to the Event Hub used for the Azure logs integration.
"""
severity = "high"
tags = [
"Domain: Cloud",
"Domain: SaaS",
"Data Source: Azure",
"Data Source: Entra ID",
"Data Source: Entra ID Sign-in",
"Use Case: Identity and Access Audit",
"Use Case: Threat Detection",
"Tactic: Credential Access",
"Resources: Investigation Guide",
]
timestamp_override = "event.ingested"
type = "esql"
query = '''
from logs-azure.signinlogs-* metadata _id, _version, _index
// Scheduled to run every hour, reviewing events from past hour
| where
@timestamp > now() - 1 hours
and data_stream.dataset == "azure.signinlogs"
and source.ip is not null
and azure.signinlogs.identity is not null
and to_lower(event.outcome) == "success"
// keep relevant raw fields
| keep
@timestamp,
azure.signinlogs.identity,
source.ip,
azure.signinlogs.properties.authentication_requirement,
azure.signinlogs.properties.app_id,
azure.signinlogs.properties.resource_display_name,
azure.signinlogs.properties.authentication_protocol,
azure.signinlogs.properties.app_display_name
// case classifications for identity usage
| eval
Esql.azure_signinlogs_properties_authentication_device_code_case = case(
azure.signinlogs.properties.authentication_protocol == "deviceCode"
and azure.signinlogs.properties.authentication_requirement != "multiFactorAuthentication",
azure.signinlogs.identity,
null),
Esql.azure_signinlogs_auth_visual_studio_case = case(
azure.signinlogs.properties.app_id == "aebc6443-996d-45c2-90f0-388ff96faa56"
and azure.signinlogs.properties.resource_display_name == "Microsoft Graph",
azure.signinlogs.identity,
null),
Esql.azure_signinlogs_auth_other_case = case(
azure.signinlogs.properties.authentication_protocol != "deviceCode"
and azure.signinlogs.properties.app_id != "aebc6443-996d-45c2-90f0-388ff96faa56",
azure.signinlogs.identity,
null)
// Aggregate metrics by user identity
| stats
Esql.event_count = count(*),
Esql.azure_signinlogs_properties_authentication_device_code_case_count_distinct = count_distinct(Esql.azure_signinlogs_properties_authentication_device_code_case),
Esql.azure_signinlogs_properties_auth_visual_studio_count_distinct = count_distinct(Esql.azure_signinlogs_auth_visual_studio_case),
Esql.azure_signinlogs_properties_auth_other_count_distinct = count_distinct(Esql.azure_signinlogs_auth_other_case),
Esql.azure_signinlogs_properties_source_ip_count_distinct = count_distinct(source.ip),
Esql.azure_signinlogs_properties_source_ip_values = values(source.ip),
Esql.azure_signinlogs_properties_client_app_values = values(azure.signinlogs.properties.app_display_name),
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)
by azure.signinlogs.identity
// Detect multiple unique IPs for one user with signs of deviceCode or VSC OAuth usage
| where
Esql.azure_signinlogs_properties_source_ip_count_distinct >= 2
and (
Esql.azure_signinlogs_properties_authentication_device_code_case_count_distinct > 0
or Esql.azure_signinlogs_properties_auth_visual_studio_count_distinct > 0
)
'''
[[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 = "T1566"
name = "Phishing"
reference = "https://attack.mitre.org/techniques/T1566/"
[[rule.threat.technique.subtechnique]]
id = "T1566.002"
name = "Spearphishing Link"
reference = "https://attack.mitre.org/techniques/T1566/002/"
[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 = "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.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/"
Stages and Predicates
Stage 1: from
from logs-azure.signinlogs-* metadata _id, _version, _index
Stage 2: where
| where
@timestamp > now() - 1 hours
and data_stream.dataset == "azure.signinlogs"
and source.ip is not null
and azure.signinlogs.identity is not null
and to_lower(event.outcome) == "success"
Stage 3: keep
| keep
@timestamp,
azure.signinlogs.identity,
source.ip,
azure.signinlogs.properties.authentication_requirement,
azure.signinlogs.properties.app_id,
azure.signinlogs.properties.resource_display_name,
azure.signinlogs.properties.authentication_protocol,
azure.signinlogs.properties.app_display_name
Stage 4: eval
| eval
Esql.azure_signinlogs_properties_authentication_device_code_case = case(
azure.signinlogs.properties.authentication_protocol == "deviceCode"
and azure.signinlogs.properties.authentication_requirement != "multiFactorAuthentication",
azure.signinlogs.identity,
null),
Esql.azure_signinlogs_auth_visual_studio_case = case(
azure.signinlogs.properties.app_id == "aebc6443-996d-45c2-90f0-388ff96faa56"
and azure.signinlogs.properties.resource_display_name == "Microsoft Graph",
azure.signinlogs.identity,
null),
Esql.azure_signinlogs_auth_other_case = case(
azure.signinlogs.properties.authentication_protocol != "deviceCode"
and azure.signinlogs.properties.app_id != "aebc6443-996d-45c2-90f0-388ff96faa56",
azure.signinlogs.identity,
null)
Esql.azure_signinlogs_auth_other_case =azure.signinlogs.properties.authentication_protocol != "deviceCode"
and azure.signinlogs.properties.app_id != "aebc6443-996d-45c2-90f0-388ff96faa56"azure.signinlogs.identitynullEsql.azure_signinlogs_auth_visual_studio_case =azure.signinlogs.properties.app_id == "aebc6443-996d-45c2-90f0-388ff96faa56"
and azure.signinlogs.properties.resource_display_name == "Microsoft Graph"azure.signinlogs.identitynullEsql.azure_signinlogs_properties_authentication_device_code_case =azure.signinlogs.properties.authentication_protocol == "deviceCode"
and azure.signinlogs.properties.authentication_requirement != "multiFactorAuthentication"azure.signinlogs.identitynullStage 5: stats
| stats
Esql.event_count = count(*),
Esql.azure_signinlogs_properties_authentication_device_code_case_count_distinct = count_distinct(Esql.azure_signinlogs_properties_authentication_device_code_case),
Esql.azure_signinlogs_properties_auth_visual_studio_count_distinct = count_distinct(Esql.azure_signinlogs_auth_visual_studio_case),
Esql.azure_signinlogs_properties_auth_other_count_distinct = count_distinct(Esql.azure_signinlogs_auth_other_case),
Esql.azure_signinlogs_properties_source_ip_count_distinct = count_distinct(source.ip),
Esql.azure_signinlogs_properties_source_ip_values = values(source.ip),
Esql.azure_signinlogs_properties_client_app_values = values(azure.signinlogs.properties.app_display_name),
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)
by azure.signinlogs.identity
Stage 6: where
| where
Esql.azure_signinlogs_properties_source_ip_count_distinct >= 2
and (
Esql.azure_signinlogs_properties_authentication_device_code_case_count_distinct > 0
or Esql.azure_signinlogs_properties_auth_visual_studio_count_distinct > 0
)
Indicators
Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.
| Field | Kind | Values |
|---|---|---|
Esql.azure_signinlogs_properties_auth_visual_studio_count_distinct | gt |
|
Esql.azure_signinlogs_properties_authentication_device_code_case_count_distinct | gt |
|
Esql.azure_signinlogs_properties_source_ip_count_distinct | ge |
|
azure.signinlogs.identity | is_not_null | |
data_stream.dataset | eq |
|
source.ip | is_not_null |
Output fields
Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.
| Field | Source |
|---|---|
Esql.event_count | STATS Esql.event_count = count(*) |
Esql.azure_signinlogs_properties_authentication_device_code_case_count_distinct | STATS Esql.azure_signinlogs_properties_authentication_device_code_case_count_distinct = count_distinct(Esql.azure_signinlogs_properties_authentication_device_code_case) |
Esql.azure_signinlogs_properties_auth_visual_studio_count_distinct | STATS Esql.azure_signinlogs_properties_auth_visual_studio_count_distinct = count_distinct(Esql.azure_signinlogs_auth_visual_studio_case) |
Esql.azure_signinlogs_properties_auth_other_count_distinct | STATS Esql.azure_signinlogs_properties_auth_other_count_distinct = count_distinct(Esql.azure_signinlogs_auth_other_case) |
Esql.azure_signinlogs_properties_source_ip_count_distinct | STATS Esql.azure_signinlogs_properties_source_ip_count_distinct = count_distinct(source.ip) |
Esql.azure_signinlogs_properties_source_ip_values | STATS Esql.azure_signinlogs_properties_source_ip_values = values(source.ip) |
Esql.azure_signinlogs_properties_client_app_values | STATS Esql.azure_signinlogs_properties_client_app_values = values(azure.signinlogs.properties.app_display_name) |
Esql.azure_signinlogs_properties_resource_display_name_values | STATS Esql.azure_signinlogs_properties_resource_display_name_values = values(azure.signinlogs.properties.resource_display_name) |
Esql.azure_signinlogs_properties_auth_requirement_values | STATS Esql.azure_signinlogs_properties_auth_requirement_values = values(azure.signinlogs.properties.authentication_requirement) |
azure.signinlogs.identity | STATS BY |