[Rule Tuning] Expand Scope of Entra ID Brute Force Sign-In Attempts (#4777)

* tuning rule to not be M365 specific

* adjusted rules

* linted

* linted; adjusted descriptions

* tuned rule logic

* adjusted time logic

* adjusted query logic

* removed 50053 from inclusion

* adjusted query
This commit is contained in:
Terrance DeJesus
2025-06-18 10:59:50 -04:00
committed by GitHub
parent fcad19fa18
commit 7b1139b219
3 changed files with 305 additions and 189 deletions
@@ -0,0 +1,240 @@
[metadata]
creation_date = "2024/09/06"
integration = ["azure"]
maturity = "production"
min_stack_comments = "Elastic ES|QL values aggregation is more performant in 8.16.5 and above."
min_stack_version = "8.17.0"
updated_date = "2025/06/05"
[rule]
author = ["Elastic"]
description = """
Identifies potential brute-force attacks targeting user accounts by analyzing failed sign-in patterns in Microsoft Entra
ID Sign-In Logs. This detection focuses on a high volume of failed interactive or non-interactive authentication
attempts within a short time window, often indicative of password spraying, credential stuffing, or password guessing.
Adversaries may use these techniques to gain unauthorized access to applications integrated with Entra ID or to
compromise valid user accounts.
"""
false_positives = [
"""
Automated processes that attempt to authenticate using expired credentials or have misconfigured authentication
settings may lead to false positives.
""",
]
from = "now-60m"
interval = "15m"
language = "esql"
license = "Elastic License v2"
name = "Microsoft Entra ID Sign-In Brute Force Activity"
note = """## Triage and analysis
### Investigating Microsoft Entra ID Sign-In Brute Force Activity
This rule detects brute-force authentication activity in Entra ID sign-in logs. It classifies failed sign-in attempts into behavior types such as password spraying, credential stuffing, or password guessing. The classification (`bf_type`) helps prioritize triage and incident response.
### Possible investigation steps
- Review `bf_type`: Determines the brute-force technique being used (`password_spraying`, `credential_stuffing`, or `password_guessing`).
- Examine `user_id_list`: Identify if high-value accounts (e.g., administrators, service principals, federated identities) are being targeted.
- Review `login_errors`: Repetitive error types like `"Invalid Grant"` or `"User Not Found"` suggest automated attacks.
- Check `ip_list` and `source_orgs`: Investigate if the activity originates from suspicious infrastructure (VPNs, hosting providers, etc.).
- Validate `unique_ips` and `countries`: Geographic diversity and IP volume may indicate distributed or botnet-based attacks.
- Compare `total_attempts` vs `duration_seconds`: High rate of failures in a short time period implies automation.
- Analyze `user_agent.original` and `device_detail_browser`: User agents like `curl`, `Python`, or generic libraries may indicate scripting tools.
- Investigate `client_app_display_name` and `incoming_token_type`: Detect potential abuse of legacy or unattended login mechanisms.
- Inspect `target_resource_display_name`: Understand what application or resource the attacker is trying to access.
- Pivot using `session_id` and `device_detail_device_id`: Determine if a device is targeting multiple accounts.
- Review `conditional_access_status`: If not enforced, ensure Conditional Access policies are scoped correctly.
### False positive analysis
- Legitimate automation (e.g., misconfigured scripts, sync processes) can trigger repeated failures.
- Internal red team activity or penetration tests may mimic brute-force behaviors.
- Certain service accounts or mobile clients may generate repetitive sign-in noise if not properly configured.
### Response and remediation
- Notify your identity security team for further analysis.
- Investigate and lock or reset impacted accounts if compromise is suspected.
- Block offending IPs or ASNs at the firewall, proxy, or using Conditional Access.
- Confirm MFA and Conditional Access are enforced for all user types.
- Audit targeted accounts for credential reuse across services.
- Implement account lockout or throttling for failed sign-in attempts where possible.
"""
references = [
"https://www.microsoft.com/en-us/security/blog/2025/05/27/new-russia-affiliated-actor-void-blizzard-targets-critical-sectors-for-espionage/",
"https://cloud.hacktricks.xyz/pentesting-cloud/azure-security/az-unauthenticated-enum-and-initial-entry/az-password-spraying",
"https://learn.microsoft.com/en-us/security/operations/incident-response-playbook-password-spray",
"https://learn.microsoft.com/en-us/purview/audit-log-detailed-properties",
"https://securityscorecard.com/research/massive-botnet-targets-m365-with-stealthy-password-spraying-attacks/",
"https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes",
"https://github.com/0xZDH/Omnispray",
"https://github.com/0xZDH/o365spray",
]
risk_score = 47
rule_id = "cca64114-fb8b-11ef-86e2-f661ea17fbce"
severity = "medium"
tags = [
"Domain: Cloud",
"Domain: Identity",
"Data Source: Azure",
"Data Source: Entra ID",
"Data Source: Entra ID Sign-in Logs",
"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*
// Define a time window for grouping and maintain the original event timestamp
| EVAL
time_window = DATE_TRUNC(15 minutes, @timestamp),
event_time = @timestamp
// Filter relevant failed authentication events with specific error codes
| WHERE event.dataset == "azure.signinlogs"
AND event.category == "authentication"
AND azure.signinlogs.category IN ("NonInteractiveUserSignInLogs", "SignInLogs")
AND event.outcome == "failure"
AND azure.signinlogs.properties.authentication_requirement == "singleFactorAuthentication"
AND azure.signinlogs.properties.status.error_code IN (
50034, // UserAccountNotFound
50126, // InvalidUsernameOrPassword
50055, // PasswordExpired
50056, // InvalidPassword
50057, // UserDisabled
50064, // CredentialValidationFailure
50076, // MFARequiredButNotPassed
50079, // MFARegistrationRequired
50105, // EntitlementGrantsNotFound
70000, // InvalidGrant
70008, // ExpiredOrRevokedRefreshToken
70043, // BadTokenDueToSignInFrequency
80002, // OnPremisePasswordValidatorRequestTimedOut
80005, // OnPremisePasswordValidatorUnpredictableWebException
50144, // InvalidPasswordExpiredOnPremPassword
50135, // PasswordChangeCompromisedPassword
50142, // PasswordChangeRequiredConditionalAccess
120000, // PasswordChangeIncorrectCurrentPassword
120002, // PasswordChangeInvalidNewPasswordWeak
120020 // PasswordChangeFailure
)
AND azure.signinlogs.properties.user_principal_name IS NOT NULL AND azure.signinlogs.properties.user_principal_name != ""
AND user_agent.original != "Mozilla/5.0 (compatible; MSAL 1.0) PKeyAuth/1.0"
AND source.`as`.organization.name != "MICROSOFT-CORP-MSN-AS-BLOCK"
// Aggregate statistics for behavioral pattern analysis
| STATS
authentication_requirement = VALUES(azure.signinlogs.properties.authentication_requirement),
client_app_id = VALUES(azure.signinlogs.properties.app_id),
client_app_display_name = VALUES(azure.signinlogs.properties.app_display_name),
target_resource_id = VALUES(azure.signinlogs.properties.resource_id),
target_resource_display_name = VALUES(azure.signinlogs.properties.resource_display_name),
conditional_access_status = VALUES(azure.signinlogs.properties.conditional_access_status),
device_detail_browser = VALUES(azure.signinlogs.properties.device_detail.browser),
device_detail_device_id = VALUES(azure.signinlogs.properties.device_detail.device_id),
device_detail_operating_system = VALUES(azure.signinlogs.properties.device_detail.operating_system),
incoming_token_type = VALUES(azure.signinlogs.properties.incoming_token_type),
risk_state = VALUES(azure.signinlogs.properties.risk_state),
session_id = VALUES(azure.signinlogs.properties.session_id),
user_id = VALUES(azure.signinlogs.properties.user_id),
user_principal_name = VALUES(azure.signinlogs.properties.user_principal_name),
result_description = VALUES(azure.signinlogs.result_description),
result_signature = VALUES(azure.signinlogs.result_signature),
result_type = VALUES(azure.signinlogs.result_type),
unique_users = COUNT_DISTINCT(azure.signinlogs.properties.user_id),
user_id_list = VALUES(azure.signinlogs.properties.user_id),
login_errors = VALUES(azure.signinlogs.result_description),
unique_login_errors = COUNT_DISTINCT(azure.signinlogs.result_description),
error_codes = VALUES(azure.signinlogs.properties.status.error_code),
unique_error_codes = COUNT_DISTINCT(azure.signinlogs.properties.status.error_code),
request_types = VALUES(azure.signinlogs.properties.incoming_token_type),
app_names = VALUES(azure.signinlogs.properties.app_display_name),
ip_list = VALUES(source.ip),
unique_ips = COUNT_DISTINCT(source.ip),
source_orgs = VALUES(source.`as`.organization.name),
countries = VALUES(source.geo.country_name),
unique_country_count = COUNT_DISTINCT(source.geo.country_name),
unique_asn_orgs = COUNT_DISTINCT(source.`as`.organization.name),
first_seen = MIN(@timestamp),
last_seen = MAX(@timestamp),
total_attempts = COUNT()
BY time_window
// Determine brute force behavior type based on statistical thresholds
| EVAL
duration_seconds = DATE_DIFF("seconds", first_seen, last_seen),
bf_type = CASE(
// Many users, relatively few distinct login errors, distributed over multiple IPs (but not too many),
// and happens quickly. Often bots using leaked credentials.
unique_users >= 10 AND total_attempts >= 30 AND unique_login_errors <= 3
AND unique_ips >= 5
AND duration_seconds <= 600
AND unique_users > unique_ips,
"credential_stuffing",
// One password against many users. Single error (e.g., "InvalidPassword"), not necessarily fast.
unique_users >= 15 AND unique_login_errors == 1 AND total_attempts >= 15 AND duration_seconds <= 1800,
"password_spraying",
// One user targeted repeatedly (same error), OR extremely noisy pattern from many IPs.
(unique_users == 1 AND unique_login_errors == 1 AND total_attempts >= 30 AND duration_seconds <= 300)
OR (unique_users <= 3 AND unique_ips > 30 AND total_attempts >= 100),
"password_guessing",
// everything else
"other"
)
// Only keep columns necessary for detection output/reporting
| KEEP
time_window, bf_type, duration_seconds, total_attempts, first_seen, last_seen,
unique_users, user_id_list, login_errors, unique_login_errors,
unique_error_codes, error_codes, request_types, app_names,
ip_list, unique_ips, source_orgs, countries,
unique_country_count, unique_asn_orgs,
authentication_requirement, client_app_id, client_app_display_name,
target_resource_id, target_resource_display_name, conditional_access_status,
device_detail_browser, device_detail_device_id, device_detail_operating_system,
incoming_token_type, risk_state, session_id, user_id,
user_principal_name, result_description, result_signature, result_type
// Remove anything not classified as credential attack activity
| WHERE bf_type != "other"
'''
[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1110"
name = "Brute Force"
reference = "https://attack.mitre.org/techniques/T1110/"
[[rule.threat.technique.subtechnique]]
id = "T1110.001"
name = "Password Guessing"
reference = "https://attack.mitre.org/techniques/T1110/001/"
[[rule.threat.technique.subtechnique]]
id = "T1110.003"
name = "Password Spraying"
reference = "https://attack.mitre.org/techniques/T1110/003/"
[[rule.threat.technique.subtechnique]]
id = "T1110.004"
name = "Credential Stuffing"
reference = "https://attack.mitre.org/techniques/T1110/004/"
[rule.threat.tactic]
id = "TA0006"
name = "Credential Access"
reference = "https://attack.mitre.org/tactics/TA0006/"
@@ -1,149 +0,0 @@
[metadata]
creation_date = "2025/03/07"
integration = ["azure"]
maturity = "production"
updated_date = "2025/03/20"
[rule]
author = ["Elastic"]
description = """
Identifies potential brute-force (password spraying) attempts against Azure Entra ID user accounts by detecting a high
number of failed non-interactive single-factor authentication (SFA) login attempts within a 10-minute window. Attackers
may attempt to brute force user accounts to gain unauthorized access to Azure Entra ID services. Non-interactive SFA
login attempts bypass conditional-access policies (CAP) and multi-factor authentication (MFA) requirements, making them
a high-risk vector for unauthorized access. Adversaries may attempt this to identify which accounts are still valid from
acquired credentials via phishing, infostealers, or other means.
"""
false_positives = [
"""
Automated processes that attempt to authenticate using expired credentials or have misconfigured authentication
settings may lead to false positives.
""",
]
from = "now-30m"
interval = "10m"
language = "esql"
license = "Elastic License v2"
name = "Azure Entra ID Password Spraying (Non-Interactive SFA)"
note = """## Triage and analysis
### Investigating Azure Entra ID Password Spraying (Non-Interactive SFA)
This rule identifies repeated failed authentication attempts using non-interactive authentication, which is often leveraged for automated attacks or legacy authentication methods. Successful compromise of an account could lead to unauthorized access, privilege escalation, or lateral movement within the environment.
**This is an ES|QL rule with aggregations that truncate results in the alert document. It is recommended to pivot investigation into the raw documents for further triage and analysis.**
### Possible investigation steps
- Identify the source IP address from which the failed login attempts originated by reviewing `source.ip`. Determine if the IP is associated with known malicious activity using threat intelligence sources or if it belongs to a corporate VPN, proxy, or automation process.
- Analyze affected user accounts by reviewing `azure.signinlogs.properties.user_principal_name` to determine if they belong to privileged roles or high-value users. Look for patterns indicating multiple failed attempts across different users, which could suggest a password spraying attempt.
- Examine the authentication method used in `azure.signinlogs.properties.authentication_details` to identify which authentication protocols were attempted and why they failed. Legacy authentication methods may be more susceptible to brute-force attacks.
- Review the authentication error codes found in `azure.signinlogs.properties.status.error_code` to understand why the login attempts failed. Common errors include `50126` for invalid credentials, `50053` for account lockouts, `50055` for expired passwords, and `50056` for users without a password.
- Correlate failed logins with other sign-in activity by looking at `event.outcome`. Identify if there were any successful logins from the same user shortly after multiple failures or if there are different geolocations or device fingerprints associated with the same account.
- Review `azure.signinlogs.properties.app_id` to identify which applications were initiating the authentication attempts. Determine if these applications are Microsoft-owned, third-party, or custom applications and if they are authorized to access the resources.
- Check for any conditional access policies that may have been triggered by the failed login attempts by reviewing `azure.signinlogs.properties.authentication_requirement`. This can help identify if the failed attempts were due to policy enforcement or misconfiguration.
## False positive analysis
### Common benign scenarios
- Automated scripts or applications using non-interactive authentication may trigger this detection, particularly if they rely on legacy authentication protocols recorded in `azure.signinlogs.properties.authentication_protocol`.
- Corporate proxies or VPNs may cause multiple users to authenticate from the same IP, appearing as repeated failed attempts under `source.ip`.
- User account lockouts from forgotten passwords or misconfigured applications may show multiple authentication failures in `azure.signinlogs.properties.status.error_code`.
### How to reduce false positives
- Exclude known trusted IPs, such as corporate infrastructure, from alerts by filtering `source.ip`.
- Exlcude known custom applications from `azure.signinlogs.properties.app_id` that are authorized to use non-interactive authentication.
- Ignore principals with a history of failed logins due to legitimate reasons, such as expired passwords or account lockouts, by filtering `azure.signinlogs.properties.user_principal_name`.
- Correlate sign-in failures with password reset events or normal user behavior before triggering an alert.
## Response and remediation
### Immediate actions
- Block the source IP address in `source.ip` if determined to be malicious.
- Reset passwords for all affected user accounts listed in `azure.signinlogs.properties.user_principal_name` and enforce stronger password policies.
- Ensure basic authentication is disabled for all applications using legacy authentication protocols listed in `azure.signinlogs.properties.authentication_protocol`.
- Enable multi-factor authentication (MFA) for impacted accounts to mitigate credential-based attacks.
- Review conditional access policies to ensure they are correctly configured to block unauthorized access attempts recorded in `azure.signinlogs.properties.authentication_requirement`.
- Review Conditional Access policies to enforce risk-based authentication and block unauthorized access attempts recorded in `azure.signinlogs.properties.authentication_requirement`.
### Long-term mitigation
- Implement a zero-trust security model by enforcing least privilege access and continuous authentication.
- Regularly review and update conditional access policies to ensure they are effective against evolving threats.
- Restrict the use of legacy authentication protocols by disabling authentication methods listed in `azure.signinlogs.properties.client_app_used`.
- Regularly audit authentication logs in `azure.signinlogs` to detect abnormal login behavior and ensure early detection of potential attacks.
- Regularly rotate client credentials and secrets for applications using non-interactive authentication to reduce the risk of credential theft.
"""
references = ["https://securityscorecard.com/wp-content/uploads/2025/02/MassiveBotnet-Report_022125_03.pdf"]
risk_score = 47
rule_id = "cca64114-fb8b-11ef-86e2-f661ea17fbce"
severity = "medium"
tags = [
"Domain: Cloud",
"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*
| keep
@timestamp,
event.dataset,
event.category,
azure.signinlogs.properties.is_interactive,
azure.signinlogs.properties.authentication_requirement,
azure.signinlogs.properties.resource_display_name,
azure.signinlogs.properties.status.error_code,
azure.signinlogs.properties.resource_service_principal_id,
azure.signinlogs.category,
event.outcome,
azure.signinlogs.properties.user_principal_name,
source.ip
// truncate the timestamp to a 10-minute window
| eval target_time_window = DATE_TRUNC(10 minutes, @timestamp)
| WHERE
event.dataset == "azure.signinlogs"
and event.category == "authentication"
and azure.signinlogs.properties.is_interactive == false
and azure.signinlogs.properties.authentication_requirement == "singleFactorAuthentication"
and event.outcome != "success"
and azure.signinlogs.properties.status.error_code in (50053, 50126, 50055, 50056, 50064, 50144)
// for tuning review azure.signinlogs.properties.status.error_code
// https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes
// count the number of unique user login attempts
| stats
unique_user_login_count = count_distinct(azure.signinlogs.properties.resource_service_principal_id) by
target_time_window,
azure.signinlogs.properties.user_principal_name,
azure.signinlogs.properties.status.error_code
// filter for >= 20 failed SFA auth attempts with the same error codes
| where unique_user_login_count >= 20
'''
[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1110"
name = "Brute Force"
reference = "https://attack.mitre.org/techniques/T1110/"
[[rule.threat.technique.subtechnique]]
id = "T1110.003"
name = "Password Spraying"
reference = "https://attack.mitre.org/techniques/T1110/003/"
[rule.threat.tactic]
id = "TA0006"
name = "Credential Access"
reference = "https://attack.mitre.org/tactics/TA0006/"
@@ -2,7 +2,7 @@
creation_date = "2024/09/06"
integration = ["azure"]
maturity = "production"
updated_date = "2025/05/20"
updated_date = "2025/06/05"
min_stack_version = "8.17.0"
min_stack_comments = "Elastic ES|QL values aggregation is more performant in 8.16.5 and above."
@@ -22,13 +22,13 @@ false_positives = [
""",
]
from = "now-60m"
interval = "10m"
interval = "15m"
language = "esql"
license = "Elastic License v2"
name = "Potential Microsoft 365 Brute Force via Entra ID Sign-Ins"
name = "Microsoft 365 Brute Force via Entra ID Sign-Ins"
note = """## Triage and analysis
### Investigating Potential Microsoft 365 Brute Force via Entra ID Sign-Ins
### Investigating Microsoft 365 Brute Force via Entra ID Sign-Ins
Identifies brute-force authentication activity against Microsoft 365 services using Entra ID sign-in logs. This detection groups and classifies failed sign-in attempts based on behavior indicative of password spraying, credential stuffing, or password guessing. The classification (`bf_type`) is included for immediate triage.
@@ -77,9 +77,10 @@ severity = "medium"
tags = [
"Domain: Cloud",
"Domain: SaaS",
"Domain: Identity",
"Data Source: Azure",
"Data Source: Entra ID",
"Data Source: Entra ID Sign-in",
"Data Source: Entra ID Sign-in Logs",
"Use Case: Identity and Access Audit",
"Use Case: Threat Detection",
"Tactic: Credential Access",
@@ -92,11 +93,11 @@ query = '''
FROM logs-azure.signinlogs*
| EVAL
time_window = DATE_TRUNC(5 minutes, @timestamp),
time_window = DATE_TRUNC(15 minutes, @timestamp),
user_id = TO_LOWER(azure.signinlogs.properties.user_principal_name),
ip = source.ip,
login_error = azure.signinlogs.result_description,
error_code = azure.signinlogs.result_type,
error_code = azure.signinlogs.properties.status.error_code,
request_type = TO_LOWER(azure.signinlogs.properties.incoming_token_type),
app_name = TO_LOWER(azure.signinlogs.properties.app_display_name),
asn_org = source.`as`.organization.name,
@@ -105,28 +106,35 @@ FROM logs-azure.signinlogs*
event_time = @timestamp
| WHERE event.dataset == "azure.signinlogs"
AND event.category == "authentication"
AND azure.signinlogs.category IN ("NonInteractiveUserSignInLogs", "SignInLogs")
AND azure.signinlogs.properties.resource_display_name RLIKE "(.*)365|SharePoint|Exchange|Teams|Office(.*)"
AND event.outcome == "failure"
AND NOT STARTS_WITH("Account is locked", login_error)
AND azure.signinlogs.result_type IN (
"50034", // UserAccountNotFound
"50126", // InvalidUserNameOrPassword
"50053", // IdsLocked or too many sign-in failures
"70000", // InvalidGrant
"70008", // Expired or revoked refresh token
"70043", // Bad token due to sign-in frequency
"50057", // UserDisabled
"50055", // Password expired
"50056", // Invalid or null password
"50064", // Credential validation failure
"50076", // MFA required but not passed
"50079", // MFA registration required
"50105" // EntitlementGrantsNotFound (no access to app)
)
AND user_id IS NOT NULL AND user_id != ""
AND user_agent != "Mozilla/5.0 (compatible; MSAL 1.0) PKeyAuth/1.0"
AND event.category == "authentication"
AND azure.signinlogs.category IN ("NonInteractiveUserSignInLogs", "SignInLogs")
AND azure.signinlogs.properties.resource_display_name RLIKE "(.*)365|SharePoint|Exchange|Teams|Office(.*)"
AND event.outcome == "failure"
AND error_code != 50053
AND azure.signinlogs.properties.status.error_code IN (
50034, // UserAccountNotFound
50126, // InvalidUsernameOrPassword
50055, // PasswordExpired
50056, // InvalidPassword
50057, // UserDisabled
50064, // CredentialValidationFailure
50076, // MFARequiredButNotPassed
50079, // MFARegistrationRequired
50105, // EntitlementGrantsNotFound
70000, // InvalidGrant
70008, // ExpiredOrRevokedRefreshToken
70043, // BadTokenDueToSignInFrequency
80002, // OnPremisePasswordValidatorRequestTimedOut
80005, // OnPremisePasswordValidatorUnpredictableWebException
50144, // InvalidPasswordExpiredOnPremPassword
50135, // PasswordChangeCompromisedPassword
50142, // PasswordChangeRequiredConditionalAccess
120000, // PasswordChangeIncorrectCurrentPassword
120002, // PasswordChangeInvalidNewPasswordWeak
120020 // PasswordChangeFailure
)
AND user_id IS NOT NULL AND user_id != ""
AND user_agent != "Mozilla/5.0 (compatible; MSAL 1.0) PKeyAuth/1.0"
| STATS
authentication_requirement = VALUES(azure.signinlogs.properties.authentication_requirement),
@@ -137,6 +145,7 @@ FROM logs-azure.signinlogs*
conditional_access_status = VALUES(azure.signinlogs.properties.conditional_access_status),
device_detail_browser = VALUES(azure.signinlogs.properties.device_detail.browser),
device_detail_device_id = VALUES(azure.signinlogs.properties.device_detail.device_id),
device_detail_operating_system = VALUES(azure.signinlogs.properties.device_detail.operating_system),
incoming_token_type = VALUES(azure.signinlogs.properties.incoming_token_type),
risk_state = VALUES(azure.signinlogs.properties.risk_state),
session_id = VALUES(azure.signinlogs.properties.session_id),
@@ -150,6 +159,8 @@ FROM logs-azure.signinlogs*
user_id_list = VALUES(user_id),
login_errors = VALUES(login_error),
unique_login_errors = COUNT_DISTINCT(login_error),
error_codes = VALUES(error_code),
unique_error_codes = COUNT_DISTINCT(error_code),
request_types = VALUES(request_type),
app_names = VALUES(app_name),
ip_list = VALUES(ip),
@@ -161,28 +172,43 @@ FROM logs-azure.signinlogs*
first_seen = MIN(event_time),
last_seen = MAX(event_time),
total_attempts = COUNT()
BY time_window
BY time_window
| EVAL
duration_seconds = DATE_DIFF("seconds", first_seen, last_seen),
bf_type = CASE(
unique_users >= 15 AND unique_login_errors == 1 AND total_attempts >= 10 AND duration_seconds <= 1800, "password_spraying",
unique_users >= 8 AND total_attempts >= 15 AND unique_login_errors <= 3 AND unique_ips <= 5 AND duration_seconds <= 600, "credential_stuffing",
unique_users == 1 AND unique_login_errors == 1 AND total_attempts >= 30 AND duration_seconds <= 300, "password_guessing",
// Many users, relatively few distinct login errors, distributed over multiple IPs (but not too many),
// and happens quickly. Often bots using leaked credentials.
unique_users >= 10 AND total_attempts >= 30 AND unique_login_errors <= 3
AND unique_ips >= 5
AND duration_seconds <= 600
AND unique_users > unique_ips,
"credential_stuffing",
// One password against many users. Single error (e.g., "InvalidPassword"), not necessarily fast.
unique_users >= 15 AND unique_login_errors == 1 AND total_attempts >= 15 AND duration_seconds <= 1800,
"password_spraying",
// One user targeted repeatedly (same error), OR extremely noisy pattern from many IPs.
(unique_users == 1 AND unique_login_errors == 1 AND total_attempts >= 30 AND duration_seconds <= 300)
OR (unique_users <= 3 AND unique_ips > 30 AND total_attempts >= 100),
"password_guessing",
// everything else
"other"
)
| KEEP
time_window, bf_type, duration_seconds, total_attempts, first_seen, last_seen,
unique_users, user_id_list, login_errors, unique_login_errors, request_types,
app_names, ip_list, unique_ips, source_orgs, countries,
unique_users, user_id_list, login_errors, unique_login_errors,
unique_error_codes, error_codes, request_types, app_names,
ip_list, unique_ips, source_orgs, countries,
unique_country_count, unique_asn_orgs,
authentication_requirement, client_app_id, client_app_display_name,
target_resource_id, target_resource_display_name, conditional_access_status,
device_detail_browser, device_detail_device_id, incoming_token_type,
risk_state, session_id, user_id, user_principal_name,
result_description, result_signature, result_type
device_detail_browser, device_detail_device_id, device_detail_operating_system,
incoming_token_type, risk_state, session_id, user_id,
user_principal_name, result_description, result_signature, result_type
| WHERE bf_type != "other"
'''
@@ -215,4 +241,3 @@ reference = "https://attack.mitre.org/techniques/T1110/004/"
id = "TA0006"
name = "Credential Access"
reference = "https://attack.mitre.org/tactics/TA0006/"