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"
Install detection rules in Elastic Security
Detect Microsoft Entra ID Sign-In Brute Force Activity in the Elastic Security detection engine by installing this rule into your Elastic Stack.
To setup this rule, check out the installation guide for Prebuilt Security Detection Rules(opens in a new tab or window).