LoFP LoFP / large enterprises with many users experiencing simultaneous password issues during credential rotation events.

Techniques

Sample rules

Potential Okta Password Spray (Multi-Source)

Description

Detects potential password spray attacks where multiple source IPs target multiple Okta user accounts within a time window, indicating coordinated attacks using IP rotation to evade single-source detection.

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

// Bucket into 15-minute windows and create user-source mapping for context
| EVAL
    Esql.time_bucket = DATE_TRUNC(15 minutes, @timestamp),
    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"), "\"}"
    )

// Aggregate across entire tenant per time bucket to detect distributed spray
| STATS
    Esql.unique_users = COUNT_DISTINCT(okta.actor.alternate_id),
    Esql.unique_source_ips = COUNT_DISTINCT(okta.client.ip),
    Esql.total_attempts = COUNT(*),
    Esql.unique_user_agents = COUNT_DISTINCT(okta.client.user_agent.raw_user_agent),
    Esql.unique_asns = COUNT_DISTINCT(source.as.number),
    Esql.unique_countries = COUNT_DISTINCT(client.geo.country_name),
    Esql.first_seen = MIN(@timestamp),
    Esql.last_seen = MAX(@timestamp),
    Esql.target_users = VALUES(okta.actor.alternate_id),
    Esql.source_ip_values = VALUES(okta.client.ip),
    Esql.user_source_mapping = VALUES(Esql.user_source_info),
    Esql.event_action_values = VALUES(event.action),
    Esql.user_agent_values = VALUES(okta.client.user_agent.raw_user_agent),
    Esql.device_values = VALUES(okta.client.device),
    Esql.is_proxy_values = VALUES(okta.security_context.is_proxy),
    Esql.geo_country_values = VALUES(client.geo.country_name),
    Esql.geo_city_values = VALUES(client.geo.city_name),
    Esql.source_asn_values = VALUES(source.as.number),
    Esql.source_asn_org_values = VALUES(source.as.organization.name),
    Esql.threat_suspected_values = VALUES(okta.debug_context.debug_data.threat_suspected),
    Esql.risk_level_values = VALUES(okta.debug_context.debug_data.risk_level),
    Esql.risk_reasons_values = VALUES(okta.debug_context.debug_data.risk_reasons)
  BY Esql.time_bucket

// Calculate spray metrics
| EVAL
    Esql.attempts_per_user = Esql.total_attempts * 1.0 / Esql.unique_users,
    Esql.attempts_per_ip = Esql.total_attempts * 1.0 / Esql.unique_source_ips,
    Esql.users_per_ip = Esql.unique_users * 1.0 / Esql.unique_source_ips

// Distributed spray: many IPs, many users, moderate spread across both
// Key differentiator: attacks come from multiple IPs (evading per-IP rules)
| WHERE
    Esql.unique_source_ips >= 5
    AND Esql.unique_users >= 8
    AND Esql.total_attempts >= 25
    AND Esql.attempts_per_user <= 5.0
    AND Esql.users_per_ip >= 1.0

| SORT Esql.total_attempts DESC
| KEEP Esql.*