Credential Access (TA0006)(external, opens in a new tab or window)
text code block:FROM logs-okta.system-* METADATA _id, _version, _index | WHERE event.dataset == "okta.system" AND (event.action LIKE "user.authentication.*" OR event.action == "user.session.start") AND okta.outcome.reason IN ("INVALID_CREDENTIALS", "LOCKED_OUT") AND okta.actor.alternate_id IS NOT NULL // Build user-source context as JSON for enrichment | EVAL Esql.user_source_info = CONCAT( "{\"user\":\"", okta.actor.alternate_id, "\",\"ip\":\"", COALESCE(okta.client.ip::STRING, "unknown"), "\",\"user_agent\":\"", COALESCE(okta.client.user_agent.raw_user_agent, "unknown"), "\"}" ) // FIRST STATS: Aggregate by (IP, user) to get per-user attempt counts // This prevents skew from outlier users with many attempts | STATS Esql.user_attempts = COUNT(*), Esql.user_source_info = VALUES(Esql.user_source_info), Esql.user_agents_per_user = VALUES(okta.client.user_agent.raw_user_agent), Esql.devices_per_user = VALUES(okta.client.device), Esql.is_proxy = VALUES(okta.security_context.is_proxy), Esql.geo_country = VALUES(client.geo.country_name), Esql.geo_city = VALUES(client.geo.city_name), Esql.asn_number = VALUES(source.as.number), Esql.asn_org = VALUES(source.as.organization.name), Esql.threat_suspected = VALUES(okta.debug_context.debug_data.threat_suspected), Esql.risk_level = VALUES(okta.debug_context.debug_data.risk_level), Esql.event_actions = VALUES(event.action), Esql.first_seen_user = MIN(@timestamp), Esql.last_seen_user = MAX(@timestamp) BY okta.client.ip, okta.actor.alternate_id // SECOND STATS: Aggregate by IP to detect password spray pattern // Now we can accurately measure the distribution of attempts across users | STATS Esql.unique_users = COUNT(*), Esql.total_attempts = SUM(Esql.user_attempts), Esql.max_attempts_per_user = MAX(Esql.user_attempts), Esql.min_attempts_per_user = MIN(Esql.user_attempts), Esql.avg_attempts_per_user = AVG(Esql.user_attempts), // Spray band: 2-6 attempts per user (deliberate slow spray below lockout) Esql.users_in_spray_band = SUM(CASE(Esql.user_attempts >= 2 AND Esql.user_attempts <= 6, 1, 0)), // Also track users with only 1 attempt (stuffing-like) for differentiation Esql.users_with_single_attempt = SUM(CASE(Esql.user_attempts == 1, 1, 0)), Esql.first_seen = MIN(Esql.first_seen_user), Esql.last_seen = MAX(Esql.last_seen_user), Esql.target_users = VALUES(okta.actor.alternate_id), Esql.user_source_mapping = VALUES(Esql.user_source_info), Esql.event_action_values = VALUES(Esql.event_actions), Esql.user_agent_values = VALUES(Esql.user_agents_per_user), Esql.device_values = VALUES(Esql.devices_per_user), Esql.is_proxy_values = VALUES(Esql.is_proxy), Esql.geo_country_values = VALUES(Esql.geo_country), Esql.geo_city_values = VALUES(Esql.geo_city), Esql.source_asn_values = VALUES(Esql.asn_number), Esql.source_asn_org_values = VALUES(Esql.asn_org), Esql.threat_suspected_values = VALUES(Esql.threat_suspected), Esql.risk_level_values = VALUES(Esql.risk_level) BY okta.client.ip // Calculate spray signature metrics | EVAL // Percentage of users in the spray band (2-6 attempts) Esql.pct_users_in_spray_band = Esql.users_in_spray_band * 100.0 / Esql.unique_users, // Attack duration in minutes (spray is paced, not bursty) Esql.attack_duration_minutes = DATE_DIFF("minute", Esql.first_seen, Esql.last_seen) // Password spraying detection logic: // - Many users targeted (>= 5) // - Hard cap below Okta lockout threshold (max <= 8 attempts per user) // - Majority of users in spray band (2-6 attempts) (at least 60%) // - Attack is paced over time (>= 5 minutes) (not a 10-second burst like stuffing) // - Minimum total attempts to reduce noise // Note: For IP rotation attacks, see "Distributed Password Spray Attack in Okta" rule | WHERE Esql.unique_users >= 5 AND Esql.total_attempts >= 15 AND Esql.max_attempts_per_user <= 8 AND Esql.max_attempts_per_user >= 2 AND Esql.pct_users_in_spray_band >= 60.0 AND Esql.attack_duration_minutes >= 5 | SORT Esql.total_attempts DESC | KEEP Esql.*, okta.client.ip
Install detection rules in Elastic Security
Detect Potential Okta Password Spray (Single Source) 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(external, opens in a new tab or window).