LoFP LoFP / automated processes or misconfigured applications retrying authentication may trigger this rule.

Techniques

Sample rules

Potential Okta Password Spray (Single Source)

Description

Detects potential password spray attacks where a single source IP attempts authentication against multiple Okta user accounts with repeated attempts per user, indicating common password guessing paced to avoid lockouts.

Detection logic

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