Detection rules › Panther

Panther rules: okta

RuleSeverity
Okta AD Agent Authentication Anomaly - Z-Score Detectionmedium
Okta AD Agent Token Abuse - Behavioralhigh
Okta Admin Access Granted
Okta Admin Role Assignedinformational
Okta AiTM Phishing Attempt Blocked by FastPasshigh
Okta API Key Createdinformational
Okta API Key Revokedinformational
Okta App Refresh Access Token Reusemedium
Okta App Unauthorized Access Attemptlow
Okta Authentication Bypass via Skeleton Key Injection - Behavioralhigh
Okta Cleartext Passwords Extracted via SCIM Applicationhigh
Okta Group Admin Role Assignedhigh
Okta HAR File IOCs
Okta Identity Provider Created or Modifiedhigh
Okta Identity Provider Sign-inhigh
Okta Investigate MFA and Password resets
Okta Investigate Session ID Activity
Okta Investigate User Activity
Okta Login From CrowdStrike Unmanaged Devicemedium
Okta Login From CrowdStrike Unmanaged Device
Okta Login From CrowdStrike Unmanaged Device (crowdstrike_fdrevent table)
Okta Login Signalinformational
Okta Login Without Pushcritical
Okta Login Without Push Markermedium
Okta MFA Globally Disabledhigh
Okta New Behaviors Acessing Admin Consolehigh
Okta Org2Org application created of modifiedhigh
Okta Password Accessedmedium
Okta Potentially Stolen Sessionhigh
Okta Rate Limitslow
Okta Sign-In from VPN Anonymizermedium
Okta Support Access
Okta Support Access Grantedmedium
Okta Support Reset Credentialhigh
Okta SWA Bulk Access, New Source, and Credential Extraction - Behavioralhigh
Okta SWA Off-Hours Credential Access - Behavioralhigh
Okta ThreatInsight Security Threat Detectedhigh
Okta User Account Lockedlow
Okta User MFA Factor Suspendhigh
Okta User MFA Own Resetinformational
Okta User MFA Reset Alllow
Okta User Reported Suspicious Activityhigh
Okta Username Above 52 Characters Security Advisory
Query.Okta.ADAgentAuthZScoreAnomaly
Query.Okta.ADAgentTokenAbuseBehavioral
Query.Okta.SkeletonKeyBypassBehavioral
Query.Okta.SWABulkAccessBehavioral
Query.Okta.SWAOffHoursAccessBehavioral
SIGNAL - Okta SSO to AWSinformational

Okta AD Agent Authentication Anomaly - Z-Score Detection

#
Status
Experimental
Severity
medium
Tags
Identity & Access Management, Okta, Active Directory, Credential Access:Steal Application Access Token, Credential Access:Brute Force, Initial Access:Valid Accounts, Anomaly Detection, Statistical Analysis
Reference
www.varonis.com
Source
github.com/panther-labs/panther-analysis

Detects potential Okta AD Agent token theft and credential abuse using statistical z-score analysis. This detection uses a lookup table containing 90-day behavioral baselines for each user's AD Agent authentication patterns, then calculates z-scores to identify suspicious activity in the last 7 days. PREREQUISITES: 1. Baseline builder query must run first: Query.Okta.ADAgentBaselineBuilder 2. Lookup table must be configured: okta_ad_pantherflow_baseline_90d 3. Allow 24 hours for initial baseline to populate Detection Logic: - Calculates mean and standard deviation for hourly authentication volume, IP diversity, country diversity, and device diversity - Alerts when recent activity shows BOTH: 1. Volume spike (z-score > 3 standard deviations) 2. Geographic/IP diversity spike (z-score > 2 standard deviations) Why This Matters: Token theft attacks have a distinct signature: stolen credentials are used from multiple locations/IPs simultaneously or in rapid succession. This creates both a volume spike and a diversity spike that this detection identifies. Complementary Detection: This rule complements Okta.ADAgent.TokenAbuse.Behavioral which detects admin actions (token creation, agent configuration) from new sources. This rule detects the actual USE of stolen tokens through authentication patterns.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

def rule(event):
    # Query already filtered for anomalies (is_anomalous = TRUE).
    # Guard against malformed rows missing the primary key field.
    return bool(event.get("user_email"))


def title(event):
    user_email = event.get("user_email", "<UNKNOWN_USER>")
    severity_score = event.get("anomaly_severity_score", 0)

    return (
        f"Okta AD Agent Authentication Anomaly Detected: {user_email} "
        f"(Severity Score: {severity_score})"
    )


def severity(event):
    # Dynamic severity based on anomaly severity score and z-score magnitudes.
    # Higher z-scores = more standard deviations from baseline = more suspicious.
    # Cold-start events have null z-scores (no baseline), so default to 0.
    severity_score = event.get("anomaly_severity_score") or 0
    z_volume = event.get("z_score_volume") or 0
    z_ip = event.get("z_score_ip_diversity") or 0
    z_country = event.get("z_score_country_diversity") or 0

    # Critical: Extreme anomaly (severity score > 15 or any z-score > 5)
    if severity_score > 15 or max(z_volume, z_ip, z_country) > 5:
        return "CRITICAL"

    # High: Strong anomaly (severity score > 10 or any z-score > 4)
    if severity_score > 10 or max(z_volume, z_ip, z_country) > 4:
        return "HIGH"

    # Medium: Moderate anomaly (default for detections that passed threshold)
    return "MEDIUM"


def alert_context(event):
    return {
        # User information
        "user_email": event.get("user_email", "<UNKNOWN_USER>"),
        # Baseline behavior
        "baseline_total_events": event.get("baseline_total_events", 0),
        "baseline_active_days": event.get("baseline_active_days", 0),
        "baseline_mean_events_per_hour": event.get("baseline_mean_events_per_hour", 0),
        "baseline_mean_ip_diversity": event.get("baseline_mean_ip_diversity_per_hour", 0),
        "baseline_mean_country_diversity": event.get("baseline_mean_country_diversity_per_hour", 0),
        # Recent anomalous activity
        "recent_total_events": event.get("recent_total_events", 0),
        "recent_max_events_per_hour": event.get("recent_max_events_per_hour", 0),
        "recent_max_ip_diversity": event.get("recent_max_ip_diversity_per_hour", 0),
        "recent_max_country_diversity": event.get("recent_max_country_diversity_per_hour", 0),
        "recent_max_device_diversity": event.get("recent_max_device_diversity_per_hour", 0),
        # Z-scores (standard deviations from baseline)
        "z_score_volume": event.get("z_score_volume", 0),
        "z_score_ip_diversity": event.get("z_score_ip_diversity", 0),
        "z_score_country_diversity": event.get("z_score_country_diversity", 0),
        "z_score_device_diversity": event.get("z_score_device_diversity", 0),
        "anomaly_severity_score": event.get("anomaly_severity_score", 0),
        # Geographic and network context
        "recent_ip_addresses": event.get("all_recent_ips", []),
        "recent_countries": event.get("all_recent_countries", []),
        # Temporal context
        "first_anomaly_hour": event.get("first_anomaly_hour", "<UNKNOWN>"),
        "last_anomaly_hour": event.get("last_anomaly_hour", "<UNKNOWN>"),
        "detection_timestamp": event.get("detection_timestamp", "<UNKNOWN>"),
    }


def dedup_key(event):
    # Deduplicate by user and hour to avoid alert spam during active attacks.
    user = event.get("user_email", "unknown")
    first_hour = str(event.get("first_anomaly_hour", "unknown"))

    return f"okta_ad_agent_zscore_anomaly_{user}_{first_hour}"

Rule specification

AnalysisType: scheduled_rule
Filename: okta_ad_agent_auth_zscore_anomaly.py
RuleID: "Okta.ADAgent.AuthenticationAnomaly.ZScore"
DisplayName: "Okta AD Agent Authentication Anomaly - Z-Score Detection"
Enabled: false  # Start disabled for tuning
ScheduledQueries:
  - Query.Okta.ADAgentAuthZScoreAnomaly
Severity: Medium  # Default, dynamic severity in rule function
Status: Experimental
Tags:
  - Identity & Access Management
  - Okta
  - Active Directory
  - Credential Access:Steal Application Access Token
  - Credential Access:Brute Force
  - Initial Access:Valid Accounts
  - Anomaly Detection
  - Statistical Analysis
Reports:
  MITRE ATT&CK:
    - TA0006:T1528  # Steal Application Access Token
    - TA0006:T1110  # Brute Force
    - TA0001:T1078  # Valid Accounts
Description: |
  Detects potential Okta AD Agent token theft and credential abuse using statistical z-score analysis.

  This detection uses a lookup table containing 90-day behavioral baselines for each user's AD Agent
  authentication patterns, then calculates z-scores to identify suspicious activity in the last 7 days.

  **PREREQUISITES:**
  1. Baseline builder query must run first: `Query.Okta.ADAgentBaselineBuilder`
  2. Lookup table must be configured: `okta_ad_pantherflow_baseline_90d`
  3. Allow 24 hours for initial baseline to populate

  **Detection Logic:**
  - Calculates mean and standard deviation for hourly authentication volume, IP diversity,
    country diversity, and device diversity
  - Alerts when recent activity shows BOTH:
    1. Volume spike (z-score > 3 standard deviations)
    2. Geographic/IP diversity spike (z-score > 2 standard deviations)

  **Why This Matters:**
  Token theft attacks have a distinct signature: stolen credentials are used from multiple
  locations/IPs simultaneously or in rapid succession. This creates both a volume spike and
  a diversity spike that this detection identifies.

  **Complementary Detection:**
  This rule complements `Okta.ADAgent.TokenAbuse.Behavioral` which detects admin actions
  (token creation, agent configuration) from new sources. This rule detects the actual USE
  of stolen tokens through authentication patterns.

Reference: https://www.varonis.com/blog/okta-attack-vectors
Runbook: |
  1. Compare recent_max_events_per_hour against baseline_mean_events_per_hour for user_email and review z_score_volume and z_score_ip_diversity to quantify the anomaly - confirm baseline_updated_at is within the past 7 days to ensure the baseline reflects current normal behavior
  2. Check all_recent_ips and all_recent_countries for geographic anomalies in the 7 days around first_anomaly_hour - look for simultaneous access from multiple countries or rapid location changes inconsistent with the user's primary_country and primary_ip
  3. Search Okta SystemLog for system.api_token.create and system.agent.ad.agent_instance_added events by user_email in the 48 hours before first_anomaly_hour, and check for Okta.ADAgent.TokenAbuse.Behavioral alerts from this user in the past 7 days

DedupPeriodMinutes: 360  # 6 hours, match query frequency
SummaryAttributes:
  - user_email
  - anomaly_severity_score
  - z_score_volume

Stages and Predicates

Fires when the condition below holds.

Condition

  • user_email is present
Alert deduplication
repeat matches within 6h group into one alert

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
user_emailis_not_null
  • (no value, null check)
field:"user_email" kind:is_not_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
user_email
baseline_total_events
baseline_active_days
baseline_mean_events_per_hour
baseline_mean_ip_diversitybaseline_mean_ip_diversity_per_hour
baseline_mean_country_diversitybaseline_mean_country_diversity_per_hour
recent_total_events
recent_max_events_per_hour
recent_max_ip_diversityrecent_max_ip_diversity_per_hour
recent_max_country_diversityrecent_max_country_diversity_per_hour
recent_max_device_diversityrecent_max_device_diversity_per_hour
z_score_volume
z_score_ip_diversity
z_score_country_diversity
z_score_device_diversity
anomaly_severity_score
recent_ip_addressesall_recent_ips
recent_countriesall_recent_countries
first_anomaly_hour
last_anomaly_hour
detection_timestamp

Response runbook

1. Compare recent_max_events_per_hour against baseline_mean_events_per_hour for user_email and review z_score_volume and z_score_ip_diversity to quantify the anomaly - confirm baseline_updated_at is within the past 7 days to ensure the baseline reflects current normal behavior

2. Check all_recent_ips and all_recent_countries for geographic anomalies in the 7 days around first_anomaly_hour - look for simultaneous access from multiple countries or rapid location changes inconsistent with the user's primary_country and primary_ip

3. Search Okta SystemLog for system.api_token.create and system.agent.ad.agent_instance_added events by user_email in the 48 hours before first_anomaly_hour, and check for Okta.ADAgent.TokenAbuse.Behavioral alerts from this user in the past 7 days

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "all_recent_countries": [
    "United States",
    "Russia",
    "China"
  ],
  "all_recent_ips": [
    "203.0.113.1",
    "198.51.100.50",
    "192.0.2.100",
    "203.0.113.200",
    "198.51.100.75"
  ],
  "anomaly_severity_score": 57.0,
  "baseline_active_days": 30,
  "baseline_mean_country_diversity_per_hour": 1.0,
  "baseline_mean_events_per_hour": 10.5,
  "baseline_mean_ip_diversity_per_hour": 1.2,
  "baseline_stddev_country_diversity_per_hour": 0.1,
  "baseline_stddev_events_per_hour": 2.5,
  "baseline_stddev_ip_diversity_per_hour": 0.3,
  "baseline_total_events": 1000,
  "detection_timestamp": "2024-01-15 19:00:00",
  "first_anomaly_hour": "2024-01-15 14:00:00",
  "last_anomaly_hour": "2024-01-15 18:00:00",
  "recent_max_country_diversity_per_hour": 3,
  "recent_max_device_diversity_per_hour": 4,
  "recent_max_events_per_hour": 50,
  "recent_max_ip_diversity_per_hour": 5,
  "recent_total_events": 500,
  "user_email": "compromised.user@company.com",
  "z_score_country_diversity": 20.0,
  "z_score_device_diversity": 8.5,
  "z_score_ip_diversity": 12.7,
  "z_score_volume": 15.8
}

Okta AD Agent Token Abuse - Behavioral

#
Status
Experimental
Severity
high
Tags
Identity & Access Management, Okta, Active Directory, Credential Access:Steal Application Access Token, Persistence:Account Manipulation, Anomaly Detection
Reference
www.varonis.com
Source
github.com/panther-labs/panther-analysis

Detects potential Okta AD Agent token theft and abuse using behavioral analysis. Instead of relying on hardcoded service account patterns, this detection identifies when AD agent-related activities (API token creation, agent registration, config changes) occur from previously unseen IP addresses or user agents. This behavioral approach adapts to your environment and catches anomalous access patterns that may indicate compromised credentials or unauthorized token generation. What This Detection Catches: - API token creation from new IPs or user agents - New AD agent registrations from unexpected sources - AD agent configuration changes from new locations Complementary Detection: Use alongside Okta.ADAgent.AuthenticationAnomaly.ZScore which detects the actual USE of stolen tokens through authentication pattern anomalies.

MITRE ATT&CK coverage

TacticTechniques
Persistence
Credential Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

def rule(event):
    # Query already filtered for anomalies.
    # Guard against malformed rows missing the primary key field.
    return bool(event.get("actorId"))


def title(event):
    actor = event.get("actorId", "<UNKNOWN_ACTOR>")
    event_type = event.get("eventType", "<UNKNOWN_EVENT>")
    anomaly_type = event.get("anomaly_type", "Unknown Anomaly")

    return f"Okta AD Agent Activity from {anomaly_type}: {actor} - {event_type}"


def severity(event):
    event_type = event.get("eventType", "")

    # New agent registration is critical (potential rogue agent)
    if "agent_instance_added" in event_type:
        return "CRITICAL"

    # Token creation from new source is high severity
    if "api_token.create" in event_type:
        return "HIGH"

    # Config changes are medium severity
    if "config_change" in event_type:
        return "MEDIUM"

    return "MEDIUM"


def dedup_key(event):
    actor = event.get("actorId", "unknown")
    event_date = str(event.get("p_event_time", "unknown"))[:10]
    return f"okta_ad_agent_token_abuse_{actor}_{event_date}"


def alert_context(event):
    return {
        "actor_id": event.get("actorId", "<UNKNOWN_ACTORID>"),
        "actor_name": event.get("actorName", "<UNKNOWN_ACTORNAME>"),
        "event_type": event.get("eventType", "<UNKNOWN_EVENTTYPE>"),
        "source_ip": event.get("sourceIP", "<UNKNOWN_SOURCEIP>"),
        "user_agent": event.get("userAgent", "<UNKNOWN_USERAGENT>"),
        "anomaly_type": event.get("anomaly_type", "<UNKNOWN_ANOMALYTYPE>"),
        "target": event.get("target", []),
        "event_time": event.get("p_event_time", "<UNKNOWN_EVENTTIME>"),
    }

Rule specification

AnalysisType: scheduled_rule
Filename: okta_ad_agent_token_abuse_behavioral.py
RuleID: "Okta.ADAgent.TokenAbuse.Behavioral"
DisplayName: "Okta AD Agent Token Abuse - Behavioral"
Enabled: true
ScheduledQueries:
  - Query.Okta.ADAgentTokenAbuseBehavioral
Severity: High
Status: Experimental
Tags:
  - Identity & Access Management
  - Okta
  - Active Directory
  - Credential Access:Steal Application Access Token
  - Persistence:Account Manipulation
  - Anomaly Detection
Reports:
  MITRE ATT&CK:
    - TA0006:T1528
    - TA0003:T1098
Description: |
  Detects potential Okta AD Agent token theft and abuse using behavioral analysis.
  Instead of relying on hardcoded service account patterns, this detection identifies
  when AD agent-related activities (API token creation, agent registration, config changes)
  occur from previously unseen IP addresses or user agents. This behavioral approach
  adapts to your environment and catches anomalous access patterns that may indicate
  compromised credentials or unauthorized token generation.

  **What This Detection Catches:**
  - API token creation from new IPs or user agents
  - New AD agent registrations from unexpected sources
  - AD agent configuration changes from new locations

  **Complementary Detection:**
  Use alongside `Okta.ADAgent.AuthenticationAnomaly.ZScore` which detects the actual
  USE of stolen tokens through authentication pattern anomalies.

Reference: https://www.varonis.com/blog/okta-attack-vectors
Runbook: |
  1. Query Okta SystemLog for all events by actorId in the 24 hours before and after the alert, focusing on system.api_token.create, system.agent.ad.agent_instance_added, and system.agent.ad.config_change_detected events to establish the full scope of activity
  2. Verify whether sourceIP and userAgent have been seen for actorId in the past 30 days, and check if sourceIP is associated with known corporate or administrative network ranges
  3. Search for other alerts from actorId or sourceIP in the past 7 days, including Okta.ADAgent.AuthenticationAnomaly.ZScore detections that may indicate a stolen token is actively being used

DedupPeriodMinutes: 1440  # 24 hours — matches dedup_key anchor (actor + event_date)
SummaryAttributes:
  - eventType
  - anomaly_type
  - actorId

Stages and Predicates

Fires when the condition below holds.

Condition

  • actorId is present
Alert deduplication
repeat matches within 1d group into one alert

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actorIdis_not_null
  • (no value, null check)
field:"actorId" kind:is_not_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
actor_idactorId
actor_nameactorName
event_typeeventType
source_ipsourceIP
user_agentuserAgent
anomaly_type
target
event_timep_event_time

Response runbook

1. Query Okta SystemLog for all events by actorId in the 24 hours before and after the alert, focusing on system.api_token.create, system.agent.ad.agent_instance_added, and system.agent.ad.config_change_detected events to establish the full scope of activity

2. Verify whether sourceIP and userAgent have been seen for actorId in the past 30 days, and check if sourceIP is associated with known corporate or administrative network ranges

3. Search for other alerts from actorId or sourceIP in the past 7 days, including Okta.ADAgent.AuthenticationAnomaly.ZScore detections that may indicate a stolen token is actively being used

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actorId": "admin@company.com",
  "actorName": "Admin User",
  "anomaly_type": "New IP Address",
  "eventType": "system.api_token.create",
  "p_event_time": "2024-01-15 14:23:45.123",
  "result": "SUCCESS",
  "sourceIP": "203.0.113.50",
  "target": [
    {
      "displayName": "ad-agent-token"
    }
  ],
  "userAgent": "Mozilla/5.0"
}

Okta Admin Access Granted

#

This is a threat-hunting query, not an automated detection. It surfaces activity for an analyst to review rather than firing on a match. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

Source
github.com/panther-labs/panther-analysis

Audit instances of admin access granted in your okta tenant

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule specification

AnalysisType: scheduled_query
QueryName: "Okta Admin Access Granted"
Enabled: false
Description: >
  Audit instances of admin access granted in your okta tenant
SnowflakeQuery: |
  SELECT
  p_event_time as event_time,
  actor:alternateId as actor_email,
  actor:displayName as actor_name,
  displayMessage,
  eventType,
  debugContext:debugData:privilegeGranted as priv_granted,
  target as target_name,
  client:ipAddress as src_ip,
  client:geographicalContext:city as city,
  client:geographicalContext:country as country,
  client:userAgent:rawUserAgent as user_agent
  FROM
    panther_logs.public.okta_systemlog
  WHERE
  ( eventType = 'user.account.privilege.grant'
   OR
    eventType = 'group.privilege.grant'
   AND
     debugContext:debugData:privilegeGranted like '%Admin%'
  )
    AND
    p_occurs_between('2022-01-14','2022-03-22')
  ORDER BY
  event_time desc

DatabricksQuery: |
  SELECT
  p_event_time as event_time,
  actor:alternateId as actor_email,
  actor:displayName as actor_name,
  displayMessage,
  eventType,
  debugContext:debugData:privilegeGranted as priv_granted,
  target as target_name,
  client:ipAddress as src_ip,
  client:geographicalContext:city as city,
  client:geographicalContext:country as country,
  client:userAgent:rawUserAgent as user_agent
  FROM
    panther_logs.okta_systemlog
  WHERE
  ( eventType = 'user.account.privilege.grant'
   OR
    eventType = 'group.privilege.grant'
   AND
     debugContext:debugData:privilegeGranted like '%Admin%'
  )
    AND
    p_occurs_between('2022-01-14','2022-03-22')
  ORDER BY
  event_time desc
Schedule:
  RateMinutes: 43200
  TimeoutMinutes: 1

Stages and Predicates

Stage 1: source

Table
panther_logs.public.okta_systemlog

Stage 2: filter

  • any of:
    • eventType is user.account.privilege.grant
    • all of:
      • eventType is group.privilege.grant
      • debugContext:debugData:privilegeGranted matches the pattern *Admin*

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_timep_event_time
actor_emailactor:alternateId
actor_nameactor:displayName
displayMessage
eventType
priv_granteddebugContext:debugData:privilegeGranted
target_nametarget
src_ipclient:ipAddress
cityclient:geographicalContext:city
countryclient:geographicalContext:country
user_agentclient:userAgent:rawUserAgent

Okta Admin Role Assigned

#
Severity
informational
Group by
debugContext.debugData.requestId
Log types
Okta.SystemLog
Tags
Identity & Access Management, Okta, Privilege Escalation:Valid Accounts
Reference
help.okta.com
Source
github.com/panther-labs/panther-analysis

A user has been granted administrative privileges in Okta

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

import re

from panther_okta_helpers import okta_alert_context

ADMIN_PATTERN = re.compile(r"[aA]dministrator")


def rule(event):
    return (
        event.get("eventType", None) == "user.account.privilege.grant"
        and event.deep_get("outcome", "result") == "SUCCESS"
        and bool(
            ADMIN_PATTERN.search(
                event.deep_get("debugContext", "debugData", "privilegeGranted", default="")
            )
        )
    )


def dedup(event):
    return event.deep_get("debugContext", "debugData", "requestId", default="<UNKNOWN_REQUEST_ID>")


def title(event):
    target = event.get("target", [{}])
    display_name = target[0].get("displayName", "MISSING DISPLAY NAME") if target else ""
    alternate_id = target[0].get("alternateId", "MISSING ALTERNATE ID") if target else ""
    privilege = event.deep_get(
        "debugContext", "debugData", "privilegeGranted", default="<UNKNOWN_PRIVILEGE>"
    )

    return (
        f"{event.deep_get('actor', 'displayName')} "
        f"<{event.deep_get('actor', 'alternateId')}> granted "
        f"[{privilege}] privileges to {display_name} <{alternate_id}>"
    )


def alert_context(event):
    return okta_alert_context(event)


def severity(event):
    if "Super administrator" in event.deep_get(
        "debugContext", "debugData", "privilegeGranted", default=""
    ):
        return "HIGH"
    return "INFO"

Rule specification

AnalysisType: rule
Filename: okta_admin_role_assigned.py
RuleID: "Okta.AdminRoleAssigned"
DisplayName: "Okta Admin Role Assigned"
Enabled: true
LogTypes:
  - Okta.SystemLog
Tags:
  - Identity & Access Management
  - Okta
  - Privilege Escalation:Valid Accounts
Reports:
  MITRE ATT&CK:
    - TA0004:T1078
Severity: Info
Description: A user has been granted administrative privileges in Okta
Reference: https://help.okta.com/en/prod/Content/Topics/Security/administrators-admin-comparison.htm
Runbook: Reach out to the user if needed to validate the activity
DedupPeriodMinutes: 15
SummaryAttributes:
  - eventType
  - severity
  - displayMessage
  - p_any_ip_addresses

Stages and Predicates

Fires on Okta.SystemLog events when all of the conditions below hold.

Condition

  • eventType is user.account.privilege.grant
  • outcome.result is SUCCESS

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Alert deduplication
repeat matches within 15m group into one alert

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses
displayNameactor.displayName
alternateIdactor.alternateId
privilegeGranteddebugContext.debugData.privilegeGranted

Response runbook

Reach out to the user if needed to validate the activity

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "jack@acme.io",
    "displayName": "Jack Naglieri",
    "id": "00uu1uuuuIlllaaaa356",
    "type": "User"
  },
  "authenticationContext": {},
  "client": {
    "device": "Computer",
    "geographicalContext": {
      "city": "San Francisco",
      "country": "United States",
      "geolocation": {
        "lat": 37.7852,
        "lon": -122.3874
      },
      "postalCode": "94105",
      "state": "California"
    },
    "ipAddress": "136.24.229.58",
    "userAgent": {
      "browser": "CHROME",
      "os": "Mac OS X",
      "rawUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36"
    },
    "zone": "null"
  },
  "debugContext": {
    "debugData": {
      "privilegeGranted": "Organization administrator, Application administrator (all)",
      "requestId": "X777JJ9sssQQHHrrrQTyYQAABBE",
      "requestUri": "/api/internal/administrators/00u6eu8c68bb72a21b57",
      "threatSuspected": "false",
      "url": "/api/internal/administrators/00u6eu8c68bb72a21b57"
    }
  },
  "displayMessage": "Grant user privilege",
  "eventType": "user.account.privilege.grant",
  "legacyEventType": "core.user.admin_privilege.granted",
  "outcome": {
    "result": "SUCCESS"
  },
  "published": "2020-11-25 21:27:03.496000000",
  "request": {},
  "securityContext": {},
  "severity": "INFO",
  "target": [
    {
      "alternateId": "alice@acme.io",
      "displayName": "Alice Green",
      "id": "00u6eup97mAJZWYmP357",
      "type": "User"
    }
  ],
  "transaction": {},
  "uuid": "2a992f80-d1ad-4f62-900e-8c68bb72a21b",
  "version": "0"
}

Okta AiTM Phishing Attempt Blocked by FastPass

#
Severity
high
Log types
Okta.SystemLog
Reference
sec.okta.com
Source
github.com/panther-labs/panther-analysis

Okta FastPass detected a user targeted by attackers wielding real-time (AiTM) proxies.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_okta_helpers import okta_alert_context


def rule(event):
    return (
        event.get("eventType") == "user.authentication.auth_via_mfa"
        and event.deep_get("outcome", "result") == "FAILURE"
        and event.deep_get("outcome", "reason") == "FastPass declined phishing attempt"
    )


def title(event):
    return (
        f"{event.deep_get('actor', 'displayName', default='<displayName-not-found>')} "
        f"<{event.deep_get('actor', 'alternateId', default='alternateId-not-found')}> "
        f"FastPass declined phishing attempt"
    )


def alert_context(event):
    return okta_alert_context(event)

Rule specification

AnalysisType: rule
Filename: okta_phishing_attempt_blocked_by_fastpass.py
RuleID: "Okta.Phishing.Attempt.Blocked.FastPass"
DisplayName: "Okta AiTM Phishing Attempt Blocked by FastPass"
Enabled: true
LogTypes:
  - Okta.SystemLog
Reports:
  MITRE ATT&CK:
    - TA0001:T1566 # Phishing
    - TA0006:T1556 # Modify Authentication Process
    - TA0003:T1078.004 # Valid Accounts: Cloud Accounts
Severity: High
Description: >
  Okta FastPass detected a user targeted by attackers wielding real-time (AiTM) proxies.
Runbook: >
  Protect sign-in flows by enforcing phishing-resistant authentication with Okta FastPass and FIDO2 WebAuthn.
Reference: >
  https://sec.okta.com/fastpassphishingdetection
DedupPeriodMinutes: 30
Threshold: 1

Stages and Predicates

Fires on Okta.SystemLog events when all of the conditions below hold.

Condition

  • eventType is user.authentication.auth_via_mfa
  • outcome.result is FAILURE
  • outcome.reason is FastPass declined phishing attempt
Alert deduplication
repeat matches within 30m group into one alert

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses
displayNameactor.displayName
alternateIdactor.alternateId

Response runbook

Protect sign-in flows by enforcing phishing-resistant authentication with Okta FastPass and FIDO2 WebAuthn.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "homer.simpson@duff.com",
    "displayName": "Homer Simpson",
    "id": "00abc123",
    "type": "User"
  },
  "authenticationcontext": {
    "authenticationStep": 0,
    "externalSessionId": "100-abc-9999"
  },
  "client": {
    "device": "Computer",
    "geographicalContext": {
      "city": "Springfield",
      "country": "United States",
      "geolocation": {
        "lat": 20,
        "lon": -25
      },
      "postalCode": "12345",
      "state": "Ohio"
    },
    "ipAddress": "1.3.2.4",
    "userAgent": {
      "browser": "CHROME",
      "os": "Mac OS X",
      "rawUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36"
    },
    "zone": "null"
  },
  "debugcontext": {
    "debugData": {
      "requestId": "AbCdEf12G",
      "requestUri": "/api/v1/users/AbCdEfG/lifecycle/reset_factors",
      "url": "/api/v1/users/AbCdEfG/lifecycle/reset_factors?"
    }
  },
  "displaymessage": "Authentication of user via MFA",
  "eventtype": "user.authentication.auth_via_mfa",
  "legacyeventtype": "core.user.factor.attempt_fail",
  "outcome": {
    "reason": "FastPass declined phishing attempt",
    "result": "FAILURE"
  },
  "published": "2022-06-22 18:18:29.015",
  "request": {
    "ipChain": [
      {
        "geographicalContext": {
          "city": "Springfield",
          "country": "United States",
          "geolocation": {
            "lat": 20,
            "lon": -25
          },
          "postalCode": "12345",
          "state": "Ohio"
        },
        "ip": "1.3.2.4",
        "version": "V4"
      }
    ]
  },
  "securitycontext": {
    "asNumber": 701,
    "asOrg": "verizon",
    "domain": "verizon.net",
    "isProxy": false,
    "isp": "verizon"
  },
  "severity": "INFO",
  "target": [
    {
      "alternateId": "peter.griffin@company.com",
      "displayName": "Peter Griffin",
      "id": "0002222AAAA",
      "type": "User"
    }
  ],
  "transaction": {
    "detail": {},
    "id": "ABcDeFgG",
    "type": "WEB"
  },
  "uuid": "AbC-123-XyZ",
  "version": "0"
}

Okta API Key Created

#
Severity
informational
Log types
Okta.SystemLog
Tags
Identity & Access Management, Okta, Credential Access:Steal Application Access Token
Reference
help.okta.com
Source
github.com/panther-labs/panther-analysis

A user created an API Key in Okta

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_okta_helpers import okta_alert_context


def rule(event):
    return (
        event.get("eventType", None) == "system.api_token.create"
        and event.deep_get("outcome", "result") == "SUCCESS"
    )


def title(event):
    target = event.get("target", [{}])
    key_name = target[0].get("displayName", "MISSING DISPLAY NAME") if target else "MISSING TARGET"

    return (
        f"{event.deep_get('actor', 'displayName')} <{event.deep_get('actor', 'alternateId')}>"
        f"created a new API key - <{key_name}>"
    )


def alert_context(event):
    return okta_alert_context(event)

Rule specification

AnalysisType: rule
Filename: okta_api_key_created.py
RuleID: "Okta.APIKeyCreated"
DisplayName: "Okta API Key Created"
Enabled: true
LogTypes:
  - Okta.SystemLog
Tags:
  - Identity & Access Management
  - Okta
  - Credential Access:Steal Application Access Token
Reports:
  MITRE ATT&CK:
    - TA0006:T1528
Severity: Info
Description: A user created an API Key in Okta
Reference: https://help.okta.com/en/prod/Content/Topics/Security/API.htm
Runbook: Reach out to the user if needed to validate the activity.
SummaryAttributes:
  - eventType
  - severity
  - displayMessage
  - p_any_ip_addresses

Stages and Predicates

Fires on Okta.SystemLog events when all of the conditions below hold.

Condition

  • eventType is system.api_token.create
  • outcome.result is SUCCESS

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses
displayNameactor.displayName
alternateIdactor.alternateId

Response runbook

Reach out to the user if needed to validate the activity.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "user@example.com",
    "displayName": "Test User",
    "id": "00u3q14ei6KUOm4Xi2p4",
    "type": "User"
  },
  "debugContext": {},
  "displayMessage": "Create API token",
  "eventType": "system.api_token.create",
  "legacyEventType": "api.token.create",
  "outcome": {
    "result": "SUCCESS"
  },
  "published": "2021-01-08 21:28:34.875",
  "request": {},
  "severity": "INFO",
  "target": [
    {
      "alternateId": "unknown",
      "details": null,
      "displayName": "test_key",
      "id": "00Tpki36zlWjhjQ1u2p4",
      "type": "Token"
    }
  ],
  "uuid": "2a992f80-d1ad-4f62-900e-8c68bb72a21b",
  "version": "0"
}

Okta API Key Revoked

#
Severity
informational
Log types
Okta.SystemLog
Tags
Identity & Access Management, Okta
Reference
help.okta.com
Source
github.com/panther-labs/panther-analysis

A user has revoked an API Key in Okta

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_okta_helpers import okta_alert_context


def rule(event):
    return (
        event.get("eventType", None) == "system.api_token.revoke"
        and event.deep_get("outcome", "result") == "SUCCESS"
    )


def title(event):
    target = event.get("target", [{}])
    key_name = target[0].get("displayName", "MISSING DISPLAY NAME") if target else "MISSING TARGET"

    return (
        f"{event.deep_get('actor', 'displayName')} <{event.deep_get('actor', 'alternateId')}>"
        f"revoked API key - <{key_name}>"
    )


def alert_context(event):
    return okta_alert_context(event)

Rule specification

AnalysisType: rule
Filename: okta_api_key_revoked.py
RuleID: "Okta.APIKeyRevoked"
DisplayName: "Okta API Key Revoked"
Enabled: true
LogTypes:
  - Okta.SystemLog
Tags:
  - Identity & Access Management
  - Okta
Severity: Info
Description: A user has revoked an API Key in Okta
Reference: https://help.okta.com/en/prod/Content/Topics/Security/API.htm
Runbook: Validate this action was authorized.
SummaryAttributes:
  - eventType
  - severity
  - displayMessage
  - p_any_ip_addresses

Stages and Predicates

Fires on Okta.SystemLog events when all of the conditions below hold.

Condition

  • eventType is system.api_token.revoke
  • outcome.result is SUCCESS

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses
displayNameactor.displayName
alternateIdactor.alternateId

Response runbook

Validate this action was authorized.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "user@example.com",
    "displayName": "Test User",
    "id": "00u3q14ei6KUOm4Xi2p4",
    "type": "User"
  },
  "debugContext": {},
  "displayMessage": "Revoke API token",
  "eventType": "system.api_token.revoke",
  "legacyEventType": "api.token.revoke",
  "outcome": {
    "result": "SUCCESS"
  },
  "published": "2021-01-08 21:28:34.875",
  "request": {},
  "severity": "INFO",
  "target": [
    {
      "alternateId": "unknown",
      "details": null,
      "displayName": "test_key",
      "id": "00Tpki36zlWjhjQ1u2p4",
      "type": "Token"
    }
  ],
  "uuid": "2a992f80-d1ad-4f62-900e-8c68bb72a21b",
  "version": "0"
}

Okta App Refresh Access Token Reuse

#
Severity
medium
Log types
Okta.SystemLog
Reference
developer.okta.com
Source
github.com/panther-labs/panther-analysis

When a client wants to renew an access token, it sends the refresh token with the access token request to the /token Okta endpoint. Okta validates the incoming refresh token, issues a new set of tokens and invalidates the refresh token that was passed with the initial request. This detection alerts when a previously used refresh token is used again with the token request

Telemetry coverage

Detection logic

from panther_okta_helpers import okta_alert_context


def rule(event):
    return event.get("eventtype") in (
        "app.oauth2.as.token.detect_reuse",
        "app.oauth2.token.detect_reuse",
    )


def title(event):
    return (
        "Okta Access Token Reuse Attempted by "
        f"[{event.get('client', {}).get('ipAddress')}] "
        f"[{event.get('actor', {}).get('displayName', '<no-displayname-found>')}]"
    )


def alert_context(event):
    return okta_alert_context(event)

Rule specification

AnalysisType: rule
Description: |-
  When a client wants to renew an access token, it sends the refresh token with the access token request to the /token Okta endpoint.
  Okta validates the incoming refresh token, issues a new set of tokens and invalidates the refresh token that was passed with the initial request.
  This detection alerts when a previously used refresh token is used again with the token request
Reference: https://developer.okta.com/docs/guides/refresh-tokens/main/#refresh-token-reuse-detection
DisplayName: "Okta App Refresh Access Token Reuse"
Enabled: true
Filename: okta_app_refresh_access_token_reuse.py
Runbook: Determine if the clientip is anomalous. Revoke tokens if deemed suspicious.
Severity: Medium
DedupPeriodMinutes: 60
LogTypes:
  - Okta.SystemLog
RuleID: "Okta.Refresh.Access.Token.Reuse"
Threshold: 1

Stages and Predicates

Fires on Okta.SystemLog events when the condition below holds.

Condition

  • eventtype is one of app.oauth2.as.token.detect_reuse, app.oauth2.token.detect_reuse

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses
ipAddressclient.ipAddress
displayNameactor.displayName

Response runbook

Determine if the clientip is anomalous. Revoke tokens if deemed suspicious.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "123456",
    "displayName": "Okta User",
    "id": "okta.1234.",
    "type": "PublicClientApp"
  },
  "authenticationcontext": {
    "authenticationStep": 0,
    "externalSessionId": "123456789"
  },
  "client": {
    "device": "Computer",
    "geographicalContext": {
      "city": "Queens",
      "country": "United States",
      "geolocation": {
        "lat": 40,
        "lon": -70
      },
      "postalCode": "11375",
      "state": "New York"
    },
    "id": "okta.1234",
    "ipAddress": "1.2.3.4",
    "userAgent": {
      "browser": "SAFARI",
      "os": "Mac OS X",
      "rawUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15"
    },
    "zone": "null"
  },
  "debugcontext": {
    "debugData": {
      "authnRequestId": "yyyy-abc-1111",
      "behaviors": "{New Geo-Location=NEGATIVE, New Device=NEGATIVE, New IP=NEGATIVE, New State=NEGATIVE, New Country=NEGATIVE, Velocity=NEGATIVE, New City=NEGATIVE}",
      "dtHash": "11aabbccc",
      "grantType": "authorization_code",
      "grantedScopes": "openid, profile, email, okta.users.read.self",
      "redirectUri": "https://org.okta.com/enduser/callback",
      "requestId": "ABCDEFG",
      "requestUri": "/login/token/redirect",
      "requestedScopes": "openid, profile, email, okta.users.read.self",
      "responseMode": "query",
      "responseType": "code",
      "risk": "{level=LOW}",
      "state": "SDFJDSLFS1234",
      "threatSuspected": "false",
      "url": "/login/token/redirect?stateToken=02.id.ASDDFJLKF",
      "userId": "00abc124"
    }
  },
  "displaymessage": "Token Reuse",
  "eventtype": "app.oauth2.token.detect_reuse",
  "legacyeventtype": "app.oauth2.token.detect_reuse",
  "outcome": {
    "result": "SUCCESS"
  },
  "published": "2022-12-13 15:22:58.759",
  "request": {
    "ipChain": [
      {
        "geographicalContext": {
          "city": "Queens",
          "country": "United States",
          "geolocation": {
            "lat": 40,
            "lon": -70
          },
          "postalCode": "11375",
          "state": "New York"
        },
        "ip": "1.2.3.4",
        "version": "V4"
      }
    ]
  },
  "securitycontext": {
    "asNumber": 701,
    "asOrg": "verizon",
    "domain": "verizon.net",
    "isProxy": false,
    "isp": "verizon"
  },
  "severity": "INFO",
  "target": [
    {
      "id": "abcd123",
      "type": "User"
    },
    {
      "displayName": "Authorization Code",
      "id": "SDFSFJL",
      "type": "code"
    }
  ],
  "transaction": {
    "detail": {},
    "id": "SDKFLSKLFJSLF",
    "type": "WEB"
  },
  "uuid": "abc-1234-aaa",
  "version": "0"
}

Okta App Unauthorized Access Attempt

#
Severity
low
Log types
Okta.SystemLog
Reference
support.okta.com
Source
github.com/panther-labs/panther-analysis

Detects when a user is denied access to an Okta application

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_okta_helpers import okta_alert_context


def rule(event):
    return event.get("eventtype", "") == "app.generic.unauth_app_access_attempt"


def title(event):
    return (
        f"[{event.deep_get('actor', 'alternateId', default = '<id-not-found>')}] "
        f"attempted unauthorized access to "
        f"[{event.get('target', [{}])[0].get('alternateId','<id-not-found>')}]"
    )


def alert_context(event):
    return okta_alert_context(event)

Rule specification

AnalysisType: rule
Description: Detects when a user is denied access to an Okta application
DisplayName: "Okta App Unauthorized Access Attempt"
Enabled: true
Filename: okta_app_unauthorized_access_attempt.py
Severity: Low
Reference: https://support.okta.com/help/s/article/App-Sign-on-Error-403-User-attempted-unauthorized-access-to-app?language=en_US
DedupPeriodMinutes: 60
LogTypes:
  - Okta.SystemLog
RuleID: "Okta.App.Unauthorized.Access.Attempt"
Threshold: 5

Stages and Predicates

Fires on Okta.SystemLog events when the condition below holds.

Condition

  • eventtype is app.generic.unauth_app_access_attempt
Alert cadence
alerts after 5 matches within 1h

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses
alternateIdactor.alternateId

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "homer.simpson@duff.com",
    "displayName": "Homer Simpsons",
    "id": "00ABC123",
    "type": "User"
  },
  "authenticationcontext": {
    "authenticationStep": 0,
    "externalSessionId": "xyz1234"
  },
  "client": {
    "device": "Computer",
    "geographicalContext": {
      "city": "Springfield",
      "country": "United States",
      "geolocation": {
        "lat": 11.111,
        "lon": -70
      },
      "postalCode": "1234",
      "state": "California"
    },
    "ipAddress": "1.2.3.4",
    "userAgent": {
      "browser": "CHROME",
      "os": "Mac OS X",
      "rawUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
    },
    "zone": "null"
  },
  "debugcontext": {
    "debugData": {
      "authnRequestId": "ABC123",
      "deviceFingerprint": "009988771ABC",
      "dtHash": "123abc1234",
      "requestId": "abc-111-adf",
      "requestUri": "/idp/idx/identify",
      "threatSuspected": "false",
      "url": "/idp/idx/identify?"
    }
  },
  "displaymessage": "User attempted unauthorized access to app",
  "eventtype": "app.generic.unauth_app_access_attempt",
  "legacyeventtype": "app.generic.unauth_app_access_attempt",
  "outcome": {
    "result": "FAILURE"
  },
  "published": "2022-12-13 00:58:19.811",
  "request": {
    "ipChain": [
      {
        "geographicalContext": {
          "city": "Springfield",
          "country": "United States",
          "geolocation": {
            "lat": 11.111,
            "lon": -70
          },
          "postalCode": "1234",
          "state": "California"
        },
        "ip": "1.2.3.4",
        "version": "V4"
      }
    ]
  },
  "securitycontext": {
    "asNumber": 11351,
    "asOrg": "charter communications inc",
    "domain": "rr.com",
    "isProxy": false,
    "isp": "charter communications inc"
  },
  "severity": "WARN",
  "target": [
    {
      "alternateId": "App (123)",
      "displayName": "App (123)",
      "id": "12345",
      "type": "AppInstance"
    }
  ],
  "transaction": {
    "detail": {},
    "id": "aaa-bbb-123",
    "type": "WEB"
  },
  "uuid": "aa-11-22-33-44-bb",
  "version": "0"
}

Okta Authentication Bypass via Skeleton Key Injection - Behavioral

#
Status
Experimental
Severity
high
Tags
Identity & Access Management, Okta, Active Directory, Defense Evasion:Modify Authentication Process, Persistence:Account Manipulation, Anomaly Detection
Reference
www.varonis.com
Source
github.com/panther-labs/panther-analysis

Detects potential Okta authentication bypass via skeleton key injection using behavioral z-score analysis. Skeleton key attacks in Okta involve manipulating authentication policies to weaken MFA requirements (disabling requireFactor, zeroing maxSessionLifetime) and bulk-enrolling attacker-controlled authenticators on victim accounts. This detection builds a 90-day behavioral baseline for each admin's policy change and factor enrollment patterns, then identifies anomalous spikes in the last 7 days. Detection Logic: - Z-score: Spike in security-weakening policy changes (> 2σ above baseline) - Z-score: Spike in admin-on-behalf-of MFA factor enrollments (> 3σ above baseline) - Cold-start: First-time security weakening (no prior baseline - immediate high-confidence signal) - Cold-start: First-time admin-enrolled factors for other users Why This Matters: Skeleton key attacks require two steps: weaken authentication policies to reduce MFA friction, then enroll attacker-controlled authenticators on victim accounts. This detection catches both steps using behavioral baselines that adapt to legitimate admin workflows. Complementary Detection: Use alongside Okta.ADAgent.TokenAbuse.Behavioral for admin credential theft scenarios.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

def rule(event):
    # Query already filtered for is_anomalous = TRUE.
    # Guard against malformed rows missing the primary key field.
    return bool(event.get("admin_email"))


def title(event):
    admin = event.get("admin_email", "Unknown")
    recent_weakenings = event.get("recent_total_weakenings") or 0
    recent_admin_enrollments = event.get("recent_total_admin_enrollments") or 0
    if recent_weakenings > 0:
        return f"Okta Skeleton Key: Security Policy Weakening by {admin}"
    if recent_admin_enrollments > 0:
        return f"Okta Skeleton Key: Bulk Admin Factor Enrollment by {admin}"
    return f"Okta Skeleton Key Bypass Anomaly Detected for {admin}"


def severity(event):
    score = event.get("anomaly_severity_score") or 0
    is_first_security_weakening = event.get("is_first_time_security_weakening") or False
    recent_weakenings = event.get("recent_total_weakenings") or 0
    if is_first_security_weakening or (recent_weakenings > 0 and score > 20):
        return "CRITICAL"
    if recent_weakenings > 0 or score > 15:
        return "HIGH"
    return "MEDIUM"


def dedup_key(event):
    admin = event.get("admin_email", "unknown")
    first_event = str(
        event.get("recent_policy_first_event")
        or event.get("recent_enrollment_first_event", "unknown")
    )[:10]
    return f"okta_skeleton_key_{admin}_{first_event}"


def alert_context(event):
    return {
        "admin_email": event.get("admin_email"),
        "recent_total_weakenings": event.get("recent_total_weakenings"),
        "recent_total_admin_enrollments": event.get("recent_total_admin_enrollments"),
        "z_score_security_weakenings": event.get("z_score_security_weakenings"),
        "z_score_admin_enrollments": event.get("z_score_admin_enrollments"),
        "anomaly_severity_score": event.get("anomaly_severity_score"),
        "is_first_time_security_weakening": event.get("is_first_time_security_weakening"),
        "is_first_time_admin_enrollment": event.get("is_first_time_admin_enrollment"),
        "baseline_total_weakenings": event.get("baseline_total_weakenings"),
        "recent_policy_first_event": event.get("recent_policy_first_event"),
        "recent_policy_last_event": event.get("recent_policy_last_event"),
    }

Rule specification

AnalysisType: scheduled_rule
Filename: okta_skeleton_key_bypass_behavioral.py
RuleID: "Okta.SkeletonKeyBypass.Behavioral"
DisplayName: "Okta Authentication Bypass via Skeleton Key Injection - Behavioral"
Enabled: true
ScheduledQueries:
  - Query.Okta.SkeletonKeyBypassBehavioral
Severity: High  # Default, dynamic severity in rule function
Status: Experimental
Tags:
  - Identity & Access Management
  - Okta
  - Active Directory
  - Defense Evasion:Modify Authentication Process
  - Persistence:Account Manipulation
  - Anomaly Detection
Reports:
  MITRE ATT&CK:
    - TA0005:T1556  # Modify Authentication Process
    - TA0003:T1098  # Account Manipulation
Description: |
  Detects potential Okta authentication bypass via skeleton key injection using behavioral z-score analysis.

  Skeleton key attacks in Okta involve manipulating authentication policies to weaken MFA requirements
  (disabling requireFactor, zeroing maxSessionLifetime) and bulk-enrolling attacker-controlled
  authenticators on victim accounts. This detection builds a 90-day behavioral baseline for each
  admin's policy change and factor enrollment patterns, then identifies anomalous spikes in the last
  7 days.

  **Detection Logic:**
  - Z-score: Spike in security-weakening policy changes (> 2σ above baseline)
  - Z-score: Spike in admin-on-behalf-of MFA factor enrollments (> 3σ above baseline)
  - Cold-start: First-time security weakening (no prior baseline - immediate high-confidence signal)
  - Cold-start: First-time admin-enrolled factors for other users

  **Why This Matters:**
  Skeleton key attacks require two steps: weaken authentication policies to reduce MFA friction,
  then enroll attacker-controlled authenticators on victim accounts. This detection catches both
  steps using behavioral baselines that adapt to legitimate admin workflows.

  **Complementary Detection:**
  Use alongside `Okta.ADAgent.TokenAbuse.Behavioral` for admin credential theft scenarios.

Reference: https://www.varonis.com/blog/okta-attack-vectors
Runbook: |
  1. Review recent_total_weakenings and recent_max_weakenings_per_hour for admin_email against baseline_total_weakenings - query Okta SystemLog for policy.rule.update and policy.lifecycle.update events by admin_email in the 24 hours around recent_policy_first_event, focusing on changedAttributes containing requireFactor=false or maxSessionLifetimeMinutes=0
  2. Check recent_total_admin_enrollments and z_score_admin_enrollments for user.mfa.factor.activate events by admin_email in the 7 days around recent_enrollment_first_event - identify target accounts enrolled and verify whether enrollment was authorized and factor types are consistent with corporate standards
  3. Search for Okta.ADAgent.TokenAbuse.Behavioral or other privileged account alerts for admin_email in the 48 hours before recent_policy_first_event to determine whether the admin account itself was compromised prior to the policy manipulation

DedupPeriodMinutes: 1440  # 24 hours
SummaryAttributes:
  - admin_email
  - anomaly_severity_score
  - recent_total_weakenings

Stages and Predicates

Fires when the condition below holds.

Condition

  • admin_email is present
Alert deduplication
repeat matches within 1d group into one alert

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
admin_emailis_not_null
  • (no value, null check)
field:"admin_email" kind:is_not_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
admin_email
recent_total_weakenings
recent_total_admin_enrollments
z_score_security_weakenings
z_score_admin_enrollments
anomaly_severity_score
is_first_time_security_weakening
is_first_time_admin_enrollment
baseline_total_weakenings
recent_policy_first_event
recent_policy_last_event

Response runbook

1. Review recent_total_weakenings and recent_max_weakenings_per_hour for admin_email against baseline_total_weakenings - query Okta SystemLog for policy.rule.update and policy.lifecycle.update events by admin_email in the 24 hours around recent_policy_first_event, focusing on changedAttributes containing requireFactor=false or maxSessionLifetimeMinutes=0

2. Check recent_total_admin_enrollments and z_score_admin_enrollments for user.mfa.factor.activate events by admin_email in the 7 days around recent_enrollment_first_event - identify target accounts enrolled and verify whether enrollment was authorized and factor types are consistent with corporate standards

3. Search for Okta.ADAgent.TokenAbuse.Behavioral or other privileged account alerts for admin_email in the 48 hours before recent_policy_first_event to determine whether the admin account itself was compromised prior to the policy manipulation

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "admin_email": "attacker@company.com",
  "anomaly_severity_score": 20.0,
  "baseline_mean_weakenings_per_hour": 0.0,
  "baseline_total_admin_enrollments": 0,
  "baseline_total_enrollments": 0,
  "baseline_total_policy_changes": 0,
  "baseline_total_weakenings": 0,
  "is_admin_enrollment_anomaly": false,
  "is_anomalous": true,
  "is_first_time_admin_enrollment": false,
  "is_first_time_security_weakening": true,
  "is_policy_volume_anomaly": false,
  "is_security_weakening_anomaly": false,
  "recent_max_policy_changes_per_hour": 3,
  "recent_max_weakenings_per_hour": 2,
  "recent_policy_first_event": "2024-01-15 02:00:00",
  "recent_policy_last_event": "2024-01-15 03:00:00",
  "recent_total_admin_enrollments": 0,
  "recent_total_enrollments": 0,
  "recent_total_policy_changes": 3,
  "recent_total_weakenings": 2,
  "z_score_admin_enrollments": null,
  "z_score_enrollments": null,
  "z_score_policy_changes": null,
  "z_score_security_weakenings": null
}

Okta Cleartext Passwords Extracted via SCIM Application

#
Severity
high
Log types
Okta.SystemLog
Reference
www.authomize.com
Source
github.com/panther-labs/panther-analysis

An application admin has extracted cleartext user passwords via SCIM app. Malcious actors can extract plaintext passwords by creating a SCIM application under their control and configuring it to sync passwords from Okta.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_okta_helpers import okta_alert_context


def rule(event):
    return event.get(
        "eventType"
    ) == "application.lifecycle.update" and "Pushing user passwords" in event.deep_get(
        "outcome", "reason", default=""
    )


def title(event):
    target = event.deep_walk(
        "target", "alternateId", default="<alternateId-not-found>", return_val="first"
    )
    return (
        f"{event.deep_get('actor', 'displayName', default='<displayName-not-found>')} "
        f"<{event.deep_get('actor', 'alternateId', default='alternateId-not-found')}> "
        f"extracted cleartext user passwords via SCIM app [{target}]"
    )


def alert_context(event):
    return okta_alert_context(event)

Rule specification

AnalysisType: rule
Filename: okta_password_extraction_via_scim.py
RuleID: "Okta.Password.Extraction.via.SCIM"
DisplayName: "Okta Cleartext Passwords Extracted via SCIM Application"
Enabled: true
LogTypes:
  - Okta.SystemLog
Reports:
  MITRE ATT&CK:
    - TA0006:T1556 # Modify Authentication Process
Severity: High
Description: >
  An application admin has extracted cleartext user passwords via SCIM app.
  Malcious actors can extract plaintext passwords by creating a SCIM application under their control and configuring it to sync passwords from Okta.
Reference: >
  https://www.authomize.com/blog/authomize-discovers-password-stealing-and-impersonation-risks-to-in-okta/
DedupPeriodMinutes: 30
Threshold: 1

Stages and Predicates

Fires on Okta.SystemLog events when all of the conditions below hold.

Condition

  • eventType is application.lifecycle.update
  • outcome.reason contains Pushing user passwords
Alert deduplication
repeat matches within 30m group into one alert

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses
displayNameactor.displayName
alternateIdactor.alternateId
alternateIdtarget.alternateId

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "homer.simpson@duff.com",
    "displayName": "Homer Simpson",
    "id": "00abc123",
    "type": "User"
  },
  "authenticationcontext": {
    "authenticationStep": 0,
    "externalSessionId": "100-abc-9999"
  },
  "client": {
    "device": "Computer",
    "geographicalContext": {
      "city": "Springfield",
      "country": "United States",
      "geolocation": {
        "lat": 20,
        "lon": -25
      },
      "postalCode": "12345",
      "state": "Ohio"
    },
    "ipAddress": "1.3.2.4",
    "userAgent": {
      "browser": "CHROME",
      "os": "Mac OS X",
      "rawUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36"
    },
    "zone": "null"
  },
  "debugcontext": {
    "debugData": {
      "requestId": "AbCdEf12G",
      "requestUri": "/api/v1/users/AbCdEfG/lifecycle/reset_factors",
      "url": "/api/v1/users/AbCdEfG/lifecycle/reset_factors?"
    }
  },
  "displaymessage": "Authentication of user via MFA",
  "eventtype": "application.lifecycle.update",
  "legacyeventtype": "core.user.factor.attempt_fail",
  "outcome": {
    "reason": "Pushing user passwords",
    "result": "SUCCESS"
  },
  "published": "2022-06-22 18:18:29.015",
  "request": {
    "ipChain": [
      {
        "geographicalContext": {
          "city": "Springfield",
          "country": "United States",
          "geolocation": {
            "lat": 20,
            "lon": -25
          },
          "postalCode": "12345",
          "state": "Ohio"
        },
        "ip": "1.3.2.4",
        "version": "V4"
      }
    ]
  },
  "securitycontext": {
    "asNumber": 701,
    "asOrg": "verizon",
    "domain": "verizon.net",
    "isProxy": false,
    "isp": "verizon"
  },
  "severity": "INFO",
  "target": [
    {
      "alternateId": "peter.griffin@company.com",
      "displayName": "Peter Griffin",
      "id": "0002222AAAA",
      "type": "User"
    }
  ],
  "transaction": {
    "detail": {},
    "id": "ABcDeFgG",
    "type": "WEB"
  },
  "uuid": "AbC-123-XyZ",
  "version": "0"
}

Okta Group Admin Role Assigned

#
Severity
high
Log types
Okta.SystemLog
Reference
support.okta.com
Source
github.com/panther-labs/panther-analysis

Detect when an admin role is assigned to a group

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_okta_helpers import okta_alert_context


def rule(event):
    return event.get("eventtype", "") == "group.privilege.grant"


def title(event):
    # pylint: disable=W0613
    return (
        "Okta Admin Privileges Assigned to Group "
        f"[{event.get('target', [{}])[0].get('alternateId', '<id-not-found>')}]"
    )


def alert_context(event):
    return okta_alert_context(event)

Rule specification

AnalysisType: rule
Description: Detect when an admin role is assigned to a group
DisplayName: "Okta Group Admin Role Assigned"
Enabled: true
Filename: okta_group_admin_role_assigned.py
Reference: https://support.okta.com/help/s/article/How-to-assign-Administrator-roles-to-groups?language=en_US#:~:text=Log%20in%20to%20the%20Admin,user%20and%20click%20Save%20changes
Severity: High
DedupPeriodMinutes: 60
LogTypes:
  - Okta.SystemLog
RuleID: "Okta.Group.Admin.Role.Assigned"
Threshold: 1

Stages and Predicates

Fires on Okta.SystemLog events when the condition below holds.

Condition

  • eventtype is group.privilege.grant

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "homer.simpson@duff.com",
    "displayName": "Homer Simpsons",
    "id": "00ABC123",
    "type": "User"
  },
  "authenticationcontext": {
    "authenticationStep": 0,
    "externalSessionId": "xyz1234"
  },
  "client": {
    "device": "Computer",
    "geographicalContext": {
      "city": "Springfield",
      "country": "United States",
      "geolocation": {
        "lat": 11.111,
        "lon": -70
      },
      "postalCode": "1234",
      "state": "California"
    },
    "ipAddress": "1.2.3.4",
    "userAgent": {
      "browser": "CHROME",
      "os": "Mac OS X",
      "rawUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
    },
    "zone": "null"
  },
  "debugcontext": {
    "debugData": {
      "authnRequestId": "ABC123",
      "deviceFingerprint": "009988771ABC",
      "dtHash": "123abc1234",
      "requestId": "abc-111-adf",
      "requestUri": "/idp/idx/identify",
      "threatSuspected": "false",
      "url": "/idp/idx/identify?"
    }
  },
  "displaymessage": "Group Privilege granted",
  "eventtype": "group.privilege.grant",
  "legacyeventtype": "group.privilege.grant",
  "outcome": {
    "result": "FAILURE"
  },
  "published": "2022-12-13 00:58:19.811",
  "request": {
    "ipChain": [
      {
        "geographicalContext": {
          "city": "Springfield",
          "country": "United States",
          "geolocation": {
            "lat": 11.111,
            "lon": -70
          },
          "postalCode": "1234",
          "state": "California"
        },
        "ip": "1.2.3.4",
        "version": "V4"
      }
    ]
  },
  "securitycontext": {
    "asNumber": 11351,
    "asOrg": "charter communications inc",
    "domain": "rr.com",
    "isProxy": false,
    "isp": "charter communications inc"
  },
  "severity": "WARN",
  "target": [
    {
      "alternateId": "App (123)",
      "displayName": "App (123)",
      "id": "12345",
      "type": "AppInstance"
    }
  ],
  "transaction": {
    "detail": {},
    "id": "aaa-bbb-123",
    "type": "WEB"
  },
  "uuid": "aa-11-22-33-44-bb",
  "version": "0"
}

Okta HAR File IOCs

#

Rule specification

AnalysisType: saved_query
QueryName: "Okta HAR File IOCs"
Description: https://sec.okta.com/harfiles
SnowflakeQuery: |-
  SELECT
   *
  FROM
         panther_logs.public.okta_systemlog
  WHERE
         (ARRAYS_OVERLAP(p_any_ip_addresses,ARRAY_CONSTRUCT('23.105.182.19', '104.251.211.122', '202.59.10.100', '162.210.194.35', '198.16.66.124', '198.16.66.156', '198.16.70.28', '198.16.74.203', '198.16.74.204', '198.16.74.205', '198.98.49.203', '2.56.164.52', '207.244.71.82', '207.244.71.84', '207.244.89.161', '207.244.89.162', '23.106.249.52', '23.106.56.11', '23.106.56.21', '23.106.56.36', '23.106.56.37', '23.106.56.38', '23.106.56.54')) OR client:userAgent.rawUserAgent IN ('Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.7113.93 Safari/537.36', ' Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36'))

DatabricksQuery: |-
  SELECT
   *
  FROM
         panther_logs.okta_systemlog
  WHERE
         (SIZE(ARRAY_INTERSECT(p_any_ip_addresses, ARRAY('23.105.182.19', '104.251.211.122', '202.59.10.100', '162.210.194.35', '198.16.66.124', '198.16.66.156', '198.16.70.28', '198.16.74.203', '198.16.74.204', '198.16.74.205', '198.98.49.203', '2.56.164.52', '207.244.71.82', '207.244.71.84', '207.244.89.161', '207.244.89.162', '23.106.249.52', '23.106.56.11', '23.106.56.21', '23.106.56.36', '23.106.56.37', '23.106.56.38', '23.106.56.54'))) > 0 OR client.userAgent.rawUserAgent IN ('Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.7113.93 Safari/537.36', ' Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36'))

Stages and Predicates

Stage 1: source

Table
panther_logs.public.okta_systemlog

Stage 2: filter

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
*

Okta Identity Provider Created or Modified

#
Severity
high
Log types
Okta.SystemLog
Reference
sec.okta.com
Source
github.com/panther-labs/panther-analysis

A new 3rd party Identity Provider has been created or modified. Attackers have been observed configuring a second Identity Provider to act as an "impersonation app" to access applications within the compromised Org on behalf of other users. This second Identity Provider, also controlled by the attacker, would act as a “source” IdP in an inbound federation relationship (sometimes called “Org2Org”) with the target.

MITRE ATT&CK coverage

Telemetry coverage

Detection logic

from panther_okta_helpers import okta_alert_context


def rule(event):
    return "system.idp.lifecycle" in event.get("eventType")


def title(event):
    action = event.get("eventType").split(".")[-1]
    target = event.deep_walk(
        "target", "displayName", default="<displayName-not-found>", return_val="first"
    )
    return (
        f"{event.deep_get('actor', 'displayName', default='<displayName-not-found>')} "
        f"<{event.deep_get('actor', 'alternateId', default='alternateId-not-found')}> "
        f"{action}d Identity Provider [{target}]"
    )


def severity(event):
    if "create" in event.get("eventType"):
        return "HIGH"
    return "MEDIUM"


def alert_context(event):
    return okta_alert_context(event)

Rule specification

AnalysisType: rule
Filename: okta_idp_create_modify.py
RuleID: "Okta.Identity.Provider.Created.Modified"
DisplayName: "Okta Identity Provider Created or Modified"
Enabled: true
LogTypes:
  - Okta.SystemLog
Reports:
  MITRE ATT&CK:
    - TA0006:T1556 # Modify Authentication Process
    - TA0001:T1199 # Trusted Relationship
    - TA0003:T1098 # Account Manipulation
Severity: High
Description: >
  A new 3rd party Identity Provider has been created or modified.
  Attackers have been observed configuring a second Identity Provider to act as an "impersonation app"
  to access applications within the compromised Org on behalf of other users. This second Identity Provider,
  also controlled by the attacker, would act as a “source” IdP in an inbound federation relationship
  (sometimes called “Org2Org”) with the target.
Runbook: |
  Delegate access to this feature to a Custom Admin Role with the minimum required permissions.
  Constrain these roles to groups that exclude highly privileged administrators.
Reference: >
  https://sec.okta.com/articles/2023/08/cross-tenant-impersonation-prevention-and-detection
DedupPeriodMinutes: 30
Threshold: 1

Stages and Predicates

Fires on Okta.SystemLog events when the condition below holds.

Condition

  • eventType contains system.idp.lifecycle
Alert deduplication
repeat matches within 30m group into one alert

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventTypecontains
  • system.idp.lifecycle
field:"eventType" kind:contains value:"system.idp.lifecycle"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses
displayNameactor.displayName
alternateIdactor.alternateId
displayNametarget.displayName

Response runbook

Delegate access to this feature to a Custom Admin Role with the minimum required permissions.

Constrain these roles to groups that exclude highly privileged administrators.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "homer.simpson@duff.com",
    "displayName": "Homer Simpson",
    "id": "00abc123",
    "type": "User"
  },
  "authenticationcontext": {
    "authenticationStep": 0,
    "externalSessionId": "100-abc-9999"
  },
  "client": {
    "device": "Computer",
    "geographicalContext": {
      "city": "Springfield",
      "country": "United States",
      "geolocation": {
        "lat": 20,
        "lon": -25
      },
      "postalCode": "12345",
      "state": "Ohio"
    },
    "ipAddress": "1.3.2.4",
    "userAgent": {
      "browser": "CHROME",
      "os": "Mac OS X",
      "rawUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36"
    },
    "zone": "null"
  },
  "debugcontext": {
    "debugData": {
      "requestId": "AbCdEf12G",
      "requestUri": "/api/v1/users/AbCdEfG/lifecycle/reset_factors",
      "url": "/api/v1/users/AbCdEfG/lifecycle/reset_factors?"
    }
  },
  "displaymessage": "Authentication of user via MFA",
  "eventtype": "system.idp.lifecycle.create",
  "legacyeventtype": "core.user.factor.attempt_fail",
  "outcome": {
    "result": "SUCCESS"
  },
  "published": "2022-06-22 18:18:29.015",
  "request": {
    "ipChain": [
      {
        "geographicalContext": {
          "city": "Springfield",
          "country": "United States",
          "geolocation": {
            "lat": 20,
            "lon": -25
          },
          "postalCode": "12345",
          "state": "Ohio"
        },
        "ip": "1.3.2.4",
        "version": "V4"
      }
    ]
  },
  "securitycontext": {
    "asNumber": 701,
    "asOrg": "verizon",
    "domain": "verizon.net",
    "isProxy": false,
    "isp": "verizon"
  },
  "severity": "INFO",
  "target": [
    {
      "alternateId": "peter.griffin@company.com",
      "displayName": "Peter Griffin",
      "id": "0002222AAAA",
      "type": "User"
    }
  ],
  "transaction": {
    "detail": {},
    "id": "ABcDeFgG",
    "type": "WEB"
  },
  "uuid": "AbC-123-XyZ",
  "version": "0"
}

Okta Identity Provider Sign-in

#
Severity
high
Log types
Okta.SystemLog
Tags
Configuration Required
Reference
sec.okta.com
Source
github.com/panther-labs/panther-analysis

A user has signed in using a 3rd party Identity Provider. Attackers have been observed configuring a second Identity Provider to act as an "impersonation app" to access applications within the compromised Org on behalf of other users. This second Identity Provider, also controlled by the attacker, would act as a “source” IdP in an inbound federation relationship (sometimes called “Org2Org”) with the target. From this “source” IdP, the threat actor manipulated the username parameter for targeted users in the second “source” Identity Provider to match a real user in the compromised “target” Identity Provider. This provided the ability to Single sign-on (SSO) into applications in the target IdP as the targeted user. Do not use this rule if your organization uses legitimate 3rd-party Identity Providers.

MITRE ATT&CK coverage

TacticTechniques
Initial Access
Persistence

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_okta_helpers import okta_alert_context


def rule(event):
    return event.get("eventType") == "user.authentication.auth_via_IDP"


def title(event):
    target = event.deep_walk(
        "target", "displayName", default="displayName-not-found", return_val="first"
    )
    return (
        f"{event.deep_get('actor', 'displayName', default='<displayName-not-found>')} "
        f"<{event.deep_get('actor', 'alternateId', default='alternateId-not-found')}> "
        f"signed in via 3rd party Identity Provider to {target}"
    )


def alert_context(event):
    return okta_alert_context(event)

Rule specification

AnalysisType: rule
Filename: okta_idp_signin.py
RuleID: "Okta.Identity.Provider.SignIn"
DisplayName: "Okta Identity Provider Sign-in"
Enabled: false
LogTypes:
  - Okta.SystemLog
Tags:
  - Configuration Required
Reports:
  MITRE ATT&CK:
    - TA0001:T1199 # Trusted Relationship
    - TA0003:T1098 # Account Manipulation
Severity: High
Description: >
  A user has signed in using a 3rd party Identity Provider.
  Attackers have been observed configuring a second Identity Provider to act as an "impersonation app"
  to access applications within the compromised Org on behalf of other users. This second Identity Provider,
  also controlled by the attacker, would act as a “source” IdP in an inbound federation relationship
  (sometimes called “Org2Org”) with the target.
  From this “source” IdP, the threat actor manipulated the username parameter for targeted users in the second
  “source” Identity Provider to match a real user in the compromised “target” Identity Provider.
  This provided the ability to Single sign-on (SSO) into applications in the target IdP as the targeted user.
  Do not use this rule if your organization uses legitimate 3rd-party Identity Providers.
Reference: >
  https://sec.okta.com/articles/2023/08/cross-tenant-impersonation-prevention-and-detection
DedupPeriodMinutes: 30
Threshold: 1

Stages and Predicates

Fires on Okta.SystemLog events when the condition below holds.

Condition

  • eventType is user.authentication.auth_via_IDP
Alert deduplication
repeat matches within 30m group into one alert

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventTypeeq
  • user.authentication.auth_via_IDP
field:"eventType" kind:eq value:"user.authentication.auth_via_IDP"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses
displayNameactor.displayName
alternateIdactor.alternateId
displayNametarget.displayName

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "homer.simpson@duff.com",
    "displayName": "Homer Simpson",
    "id": "00abc123",
    "type": "User"
  },
  "authenticationcontext": {
    "authenticationStep": 0,
    "externalSessionId": "100-abc-9999"
  },
  "client": {
    "device": "Computer",
    "geographicalContext": {
      "city": "Springfield",
      "country": "United States",
      "geolocation": {
        "lat": 20,
        "lon": -25
      },
      "postalCode": "12345",
      "state": "Ohio"
    },
    "ipAddress": "1.3.2.4",
    "userAgent": {
      "browser": "CHROME",
      "os": "Mac OS X",
      "rawUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36"
    },
    "zone": "null"
  },
  "debugcontext": {
    "debugData": {
      "requestId": "AbCdEf12G",
      "requestUri": "/api/v1/users/AbCdEfG/lifecycle/reset_factors",
      "url": "/api/v1/users/AbCdEfG/lifecycle/reset_factors?"
    }
  },
  "displaymessage": "Authentication of user via MFA",
  "eventtype": "user.authentication.auth_via_IDP",
  "legacyeventtype": "core.user.factor.attempt_fail",
  "outcome": {
    "result": "SUCCESS"
  },
  "published": "2022-06-22 18:18:29.015",
  "request": {
    "ipChain": [
      {
        "geographicalContext": {
          "city": "Springfield",
          "country": "United States",
          "geolocation": {
            "lat": 20,
            "lon": -25
          },
          "postalCode": "12345",
          "state": "Ohio"
        },
        "ip": "1.3.2.4",
        "version": "V4"
      }
    ]
  },
  "securitycontext": {
    "asNumber": 701,
    "asOrg": "verizon",
    "domain": "verizon.net",
    "isProxy": false,
    "isp": "verizon"
  },
  "severity": "INFO",
  "target": [
    {
      "alternateId": "peter.griffin@company.com",
      "displayName": "Peter Griffin",
      "id": "0002222AAAA",
      "type": "User"
    }
  ],
  "transaction": {
    "detail": {},
    "id": "ABcDeFgG",
    "type": "WEB"
  },
  "uuid": "AbC-123-XyZ",
  "version": "0"
}

Okta Investigate MFA and Password resets

#

This is a threat-hunting query, not an automated detection. It surfaces activity for an analyst to review rather than firing on a match. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

Source
github.com/panther-labs/panther-analysis

Investigate Password and MFA resets for the last 7 days

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule specification

AnalysisType: scheduled_query
QueryName: "Okta Investigate MFA and Password resets"
Enabled: false
Description: >
  Investigate Password and MFA resets for the last 7 days
SnowflakeQuery: |
  SELECT p_event_time,actor:alternateId as actor_user,target[0]:alternateId as target_user, eventType,client:ipAddress as ip_address
  FROM panther_logs.public.okta_systemlog
  WHERE eventType IN ('user.mfa.factor.reset_all', 'user.mfa.factor.deactivate', 'user.mfa.factor.suspend', 'user.account.reset_password', 'user.account.update_password','user.mfa.factor.update')
  and p_occurs_since('7 days')
  -- If you wish to investigate an individual user , uncomment this line and add their email here
  -- and actor:alternateId = '<EMAIL_GOES_HERE>'
  ORDER by p_event_time DESC

DatabricksQuery: |
  SELECT p_event_time,actor:alternateId as actor_user,target[0]:alternateId as target_user, eventType,client:ipAddress as ip_address
  FROM panther_logs.okta_systemlog
  WHERE eventType IN ('user.mfa.factor.reset_all', 'user.mfa.factor.deactivate', 'user.mfa.factor.suspend', 'user.account.reset_password', 'user.account.update_password','user.mfa.factor.update')
  and p_occurs_since('7 days')
  -- If you wish to investigate an individual user , uncomment this line and add their email here
  -- and actor:alternateId = '<EMAIL_GOES_HERE>'
  ORDER by p_event_time DESC
Schedule:
  RateMinutes: 43200
  TimeoutMinutes: 1

Stages and Predicates

Stage 1: source

Table
panther_logs.public.okta_systemlog

Stage 2: filter

  • eventType is one of user.mfa.factor.reset_all, user.mfa.factor.deactivate, user.mfa.factor.suspend, user.account.reset_password, user.account.update_password (+1 more values, see Indicators below)
Window
7d

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventTypein
  • user.account.reset_password
  • user.account.update_password
  • user.mfa.factor.deactivate
  • user.mfa.factor.reset_all
  • user.mfa.factor.suspend
  • user.mfa.factor.update
field:"eventType" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
p_event_time
actor_useractor:alternateId
target_usertarget [ 0 ] : alternateId
eventType
ip_addressclient:ipAddress

Okta Investigate Session ID Activity

#

This is a threat-hunting query, not an automated detection. It surfaces activity for an analyst to review rather than firing on a match. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

Source
github.com/panther-labs/panther-analysis

Search for activity related to a specific SessionID in Okta panther_logs.okta_systemlog

Rule specification

AnalysisType: scheduled_query
QueryName: "Okta Investigate Session ID Activity"
Enabled: false
Description: >
  Search for activity related to a specific SessionID in Okta panther_logs.okta_systemlog
SnowflakeQuery: |
  SELECT
    p_event_time as event_time,
    actor:alternateId as actor_email,
    actor:displayName as actor_name,
    authenticationContext:externalSessionId as sessionId,
    displayMessage,
    eventType,
    client:ipAddress as src_ip,
    client:geographicalContext:city as city,
    client:geographicalContext:country as country,
    client:userAgent:rawUserAgent as user_agent
  FROM panther_logs.public.okta_systemlog
  WHERE p_occurs_since('7 days')
  -- Uncomment the line below and replace 'sessionId' with the sessionId you are investigating
  -- and authenticationContext:externalSessionId = '<SESSIONID_GOES_HERE>'
  ORDER BY event_time DESC

DatabricksQuery: |
  SELECT
    p_event_time as event_time,
    actor:alternateId as actor_email,
    actor:displayName as actor_name,
    authenticationContext:externalSessionId as sessionId,
    displayMessage,
    eventType,
    client:ipAddress as src_ip,
    client:geographicalContext:city as city,
    client:geographicalContext:country as country,
    client:userAgent:rawUserAgent as user_agent
  FROM panther_logs.okta_systemlog
  WHERE p_occurs_since('7 days')
  -- Uncomment the line below and replace 'sessionId' with the sessionId you are investigating
  -- and authenticationContext:externalSessionId = '<SESSIONID_GOES_HERE>'
  ORDER BY event_time DESC
Schedule:
  RateMinutes: 43200
  TimeoutMinutes: 1

Stages and Predicates

Stage 1: source

Table
panther_logs.public.okta_systemlog

Stage 2: filter

Window
7d

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_timep_event_time
actor_emailactor:alternateId
actor_nameactor:displayName
sessionIdauthenticationContext:externalSessionId
displayMessage
eventType
src_ipclient:ipAddress
cityclient:geographicalContext:city
countryclient:geographicalContext:country
user_agentclient:userAgent:rawUserAgent

Okta Investigate User Activity

#

This is an enrichment or summary query that produces aggregate or lookup data for other rules to consume, not a standalone detection. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

Source
github.com/panther-labs/panther-analysis

Audit user activity across your environment. Customize to filter on specific users, time ranges, etc

Rule specification

AnalysisType: scheduled_query
QueryName: "Okta Investigate User Activity"
Enabled: false
Description: >
  Audit user activity across your environment. Customize to filter on specific users, time ranges, etc
SnowflakeQuery: |
  SELECT actor:displayName AS actor_name, actor:alternateId AS actor_email, eventType, COUNT(*) AS activity_count
  FROM panther_logs.public.okta_systemlog
  WHERE p_occurs_since('7 days')
  AND actor:type = 'User'
  -- Uncomment lines below to filter by user email and/or eventType
  -- and actor_email = 'email'
  -- and eventType = 'eventType'
  GROUP BY actor:displayName, actor:alternateId, eventType
  ORDER BY  actor_name, activity_count DESC

DatabricksQuery: |
  SELECT actor:displayName AS actor_name, actor:alternateId AS actor_email, eventType, COUNT(*) AS activity_count
  FROM panther_logs.okta_systemlog
  WHERE p_occurs_since('7 days')
  AND actor:type = 'User'
  -- Uncomment lines below to filter by user email and/or eventType
  -- and actor_email = 'email'
  -- and eventType = 'eventType'
  GROUP BY actor:displayName, actor:alternateId, eventType
  ORDER BY  actor_name, activity_count DESC
Schedule:
  RateMinutes: 43200
  TimeoutMinutes: 1

Stages and Predicates

Stage 1: source

Table
panther_logs.public.okta_systemlog

Stage 2: filter

  • actor:type is User
Grouped by
actor:displayName, actor:alternateId, eventType
Window
7d

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
actor_nameactor:displayName
actor_emailactor:alternateId
eventType
activity_countCOUNT ( * )

Okta Login From CrowdStrike Unmanaged Device

#
Severity
medium
Tags
Multi-Table Query
Reference
www.crowdstrike.com
Source
github.com/panther-labs/panther-analysis

Detects Okta Logins from IP addresses not found in CrowdStrike''s AIP list. May indicate unmanaged device being used, or faulty CrowdStrike Sensor.

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

def rule(_):
    return True


def title(event):
    return (
        "Okta Login for "
        f"[{event.deep_get('actor', 'alternateId', default = '<email_not_found>')}]"
        " from unmanaged IP Address."
    )

Rule specification

AnalysisType: scheduled_rule
Description: Detects Okta Logins from IP addresses not found in CrowdStrike''s AIP list. May indicate unmanaged device being used, or faulty CrowdStrike Sensor.
DisplayName: "Okta Login From CrowdStrike Unmanaged Device"
Enabled: false
Filename: okta_login_from_crowdstrike_unmanaged_device.py
Reference: https://www.crowdstrike.com/wp-content/uploads/2023/05/crowdstrike-falcon-device-control-data-sheet.pdf
Severity: Medium
DedupPeriodMinutes: 60
RuleID: "Okta.Login.From.CrowdStrike.Unmanaged.Device"
Threshold: 1
ScheduledQueries:
  - Okta Login From CrowdStrike Unmanaged Device
Tags:
  - Multi-Table Query

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query Okta Login From CrowdStrike Unmanaged Device; its Python module (Detection logic above) shapes the alert rather than filtering.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
alternateIdactor.alternateId

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "homer.simpson@springfield.com",
    "displayName": "Homer Simpson",
    "id": "AbcdEfghIjklmno",
    "type": "User"
  },
  "authenticationcontext": {
    "authenticationStep": 0,
    "externalSessionId": "AbcDefgiH"
  },
  "client": {
    "device": "Computer",
    "geographicalContext": {
      "city": "San Francisco",
      "country": "United States",
      "geolocation": {
        "lat": 30,
        "lon": -100
      },
      "postalCode": "9000",
      "state": "California"
    },
    "ipAddress": "1.2.3.4",
    "userAgent": {
      "browser": "CHROME",
      "os": "Mac OS X",
      "rawUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
    },
    "zone": "null"
  },
  "debugcontext": {
    "debugData": {
      "authnRequestId": "abcdefg",
      "deviceFingerprint": "abcdefg",
      "dtHash": "abcdefgc",
      "logOnlySecurityData": "{\"risk\":{\"level\":\"LOW\"},\"behaviors\":{\"New Geo-Location\":\"NEGATIVE\",\"New Device\":\"NEGATIVE\",\"New IP\":\"NEGATIVE\",\"New State\":\"NEGATIVE\",\"New Country\":\"NEGATIVE\",\"Velocity\":\"NEGATIVE\",\"New City\":\"NEGATIVE\"}}",
      "origin": "https://springfield.okta.com",
      "requestId": "abcdefg",
      "requestUri": "/idp/idx/identify",
      "threatSuspected": "false",
      "url": "/idp/idx/identify?"
    }
  },
  "displaymessage": "User login to Okta",
  "eventtype": "user.session.start",
  "legacyeventtype": "core.user_auth.login_success",
  "outcome": {
    "result": "SUCCESS"
  },
  "published": "2023-01-10 17:39:40.526",
  "request": {
    "ipChain": [
      {
        "geographicalContext": {
          "city": "San Francisco",
          "country": "United States",
          "geolocation": {
            "lat": 30,
            "lon": -100
          },
          "postalCode": "90000",
          "state": "California"
        },
        "ip": "1.2.3.4",
        "version": "V4"
      }
    ]
  },
  "securitycontext": {
    "asNumber": 1337,
    "asOrg": "springfield",
    "domain": ".",
    "isProxy": false,
    "isp": "duff inc"
  },
  "severity": "INFO",
  "target": [
    {
      "alternateId": "unknown",
      "displayName": "Password",
      "id": "abcdefg",
      "type": "AuthenticatorEnrollment"
    },
    {
      "alternateId": "Okta Dashboard",
      "displayName": "Okta Dashboard",
      "id": "abcdefg",
      "type": "AppInstance"
    }
  ],
  "transaction": {
    "detail": {},
    "id": "abcdefg",
    "type": "WEB"
  },
  "uuid": "abcdefg",
  "version": "0"
}

Okta Login From CrowdStrike Unmanaged Device

#
Tags
Multi-Table Query
Source
github.com/panther-labs/panther-analysis

Okta Logins from an IP Address not found in CrowdStrike's AIP List

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule specification

AnalysisType: scheduled_query
Description: Okta Logins from an IP Address not found in CrowdStrike's AIP List
Enabled: false
SnowflakeQuery: |
  SELECT *
  FROM panther_logs.public.okta_systemlog
  WHERE p_occurs_since('1 hour')
    AND eventtype = 'user.session.start'
    AND outcome:result = 'SUCCESS'
    AND client:device = 'Computer'
    AND client:ipAddress LIKE '%.%.%.%'
    AND client:ipAddress NOT IN
      (
        SELECT DISTINCT aip
        FROM panther_logs.public.crowdstrike_aidmaster
        WHERE p_occurs_since('3 days')
      )

DatabricksQuery: |
  SELECT *
  FROM panther_logs.okta_systemlog
  WHERE p_occurs_since('1 hour')
    AND eventtype = 'user.session.start'
    AND outcome:result = 'SUCCESS'
    AND client:device = 'Computer'
    AND client:ipAddress LIKE '%.%.%.%'
    AND client:ipAddress NOT IN
      (
        SELECT DISTINCT aip
        FROM panther_logs.crowdstrike_aidmaster
        WHERE p_occurs_since('3 days')
      )
QueryName: "Okta Login From CrowdStrike Unmanaged Device"
Schedule:
  RateMinutes: 60
  TimeoutMinutes: 1
Tags:
  - Multi-Table Query

Stages and Predicates

Stage 1: source

Table
panther_logs.public.okta_systemlog

Stage 2: filter

  • eventtype is user.session.start
  • outcome:result is SUCCESS
  • client:device is Computer
  • client:ipAddress matches the pattern *.*.*.*
  • client:ipAddress is not in the results of a subquery on panther_logs.public.crowdstrike_aidmaster
Window
1h

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
*

Okta Login From CrowdStrike Unmanaged Device (crowdstrike_fdrevent table)

#
Tags
Multi-Table Query
Source
github.com/panther-labs/panther-analysis

Okta Logins from an IP Address not found in CrowdStrike's AIP List (crowdstrike_fdrevent table)

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule specification

# This file is the part of the Crowdstrike FDREvent migration, and it's the equivalent of
# https://github.com/panther-labs/panther-analysis/blob/b61db1ecf3967c5f6a44c1782f8891fd5f54384d/queries/okta_queries/Okta_Login_From_CrowdStrike_Unmanaged_Device.yml
#
AnalysisType: scheduled_query
Description: Okta Logins from an IP Address not found in CrowdStrike's AIP List (crowdstrike_fdrevent table)
Enabled: false
SnowflakeQuery: |
  SELECT *
  FROM panther_logs.public.okta_systemlog
  WHERE p_occurs_since('1 days')
    AND eventtype = 'user.session.start'
    AND outcome:result = 'SUCCESS'
    AND client:device = 'Computer'
    AND client:ipAddress LIKE '%.%.%.%'
    AND client:ipAddress NOT IN
      (
        SELECT DISTINCT aip
        FROM panther_logs.public.crowdstrike_fdrevent
        WHERE p_occurs_since('3 days') AND panther_logs.public.crowdstrike_fdrevent.fdr_event_type = 'aid_master'
      )

DatabricksQuery: |
  SELECT *
  FROM panther_logs.okta_systemlog
  WHERE p_occurs_since('1 days')
    AND eventtype = 'user.session.start'
    AND outcome:result = 'SUCCESS'
    AND client:device = 'Computer'
    AND client:ipAddress LIKE '%.%.%.%'
    AND client:ipAddress NOT IN
      (
        SELECT DISTINCT aip
        FROM panther_logs.crowdstrike_fdrevent
        WHERE p_occurs_since('3 days') AND panther_logs.crowdstrike_fdrevent.fdr_event_type = 'aid_master'
      )
QueryName: "Okta Login From CrowdStrike Unmanaged Device (crowdstrike_fdrevent table)"
Schedule:
  RateMinutes: 1440
  TimeoutMinutes: 1
Tags:
  - Multi-Table Query

Stages and Predicates

Stage 1: source

Table
panther_logs.public.okta_systemlog

Stage 2: filter

  • eventtype is user.session.start
  • outcome:result is SUCCESS
  • client:device is Computer
  • client:ipAddress matches the pattern *.*.*.*
  • client:ipAddress is not in the results of a subquery on panther_logs.public.crowdstrike_fdrevent
Window
1d

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
*

Okta Login Signal

#
Severity
informational
Log types
Okta.SystemLog
Source
github.com/panther-labs/panther-analysis

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

def rule(event):
    return (
        event.get("eventType") == "user.session.start"
        and event.deep_get("outcome", "result") == "SUCCESS"
    )


def title(event):
    return f'{event.deep_get("actor", "displayName")} logged in to Okta'

Rule specification

AnalysisType: rule
Filename: okta_login_signal.py
RuleID: "Okta.Login.Success"
DisplayName: "Okta Login Signal"
Enabled: false
CreateAlert: false
LogTypes:
  - Okta.SystemLog
Severity: Info
DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on Okta.SystemLog events when all of the conditions below hold.

Condition

  • eventType is user.session.start
  • outcome.result is SUCCESS

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
displayNameactor.displayName

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "casey.hill@hey.com",
    "displayName": "Casey Hill",
    "id": "00ubewfku1EX0WCFk697",
    "type": "User"
  },
  "authenticationContext": {
    "authenticationStep": 0,
    "externalSessionId": "idxvF50v_5sT2-GOA7_K0Amyw"
  },
  "client": {
    "device": "Computer",
    "geographicalContext": {
      "city": "Atlanta",
      "country": "United States",
      "geolocation": {
        "lat": 33.9794,
        "lon": -84.3459
      },
      "postalCode": "30350",
      "state": "Georgia"
    },
    "ipAddress": "99.108.5.25",
    "userAgent": {
      "browser": "CHROME",
      "os": "Mac OS 14.4.1 (Sonoma)",
      "rawUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"
    },
    "zone": "null"
  },
  "debugContext": {
    "debugData": {
      "authnRequestId": "5167029d2c8308348d651c0be650230f",
      "dtHash": "f23be3b6d8bfd69c14e0d1b33e790b84fa5358eab0a09a1058816ad65d633da4",
      "oktaUserAgentExtended": "okta-auth-js/7.0.1 okta-signin-widget-7.16.1",
      "origin": "https://trial-2340039.okta.com",
      "requestId": "601b158a3b3e23be5bbf74d0fe63cd78",
      "requestUri": "/idp/idx/challenge/answer",
      "threatSuspected": "false",
      "url": "/idp/idx/challenge/answer?"
    }
  },
  "displayMessage": "User login to Okta",
  "eventType": "user.session.start",
  "legacyEventType": "core.user_auth.login_success",
  "outcome": {
    "result": "SUCCESS"
  },
  "published": "2024-04-02 19:17:37.621000000",
  "request": {
    "ipChain": [
      {
        "geographicalContext": {
          "city": "Atlanta",
          "country": "United States",
          "geolocation": {
            "lat": 33.9794,
            "lon": -84.3459
          },
          "postalCode": "30350",
          "state": "Georgia"
        },
        "ip": "99.108.5.25",
        "version": "V4"
      }
    ]
  },
  "securityContext": {
    "asNumber": 7018,
    "asOrg": "at&t corp.",
    "domain": "sbcglobal.net",
    "isProxy": false,
    "isp": "att services inc"
  },
  "severity": "INFO",
  "target": [
    {
      "alternateId": "unknown",
      "displayName": "Password",
      "id": "lae1at5k3ir9bV1gr697",
      "type": "AuthenticatorEnrollment"
    },
    {
      "alternateId": "Okta Dashboard",
      "displayName": "Okta Dashboard",
      "id": "0oabewfkt83T8ve1o697",
      "type": "AppInstance"
    }
  ],
  "transaction": {
    "detail": {},
    "id": "601b158a3b3e23be5bbf74d0fe63cd78",
    "type": "WEB"
  },
  "uuid": "aac560bd-f125-11ee-9caa-cd5d09945def",
  "version": "0"
}

Okta Login Without Push

#
Severity
critical
Time window
30h
Match by
actor.alternateId, new.email
Tags
Okta, Push Security, Identity Verification, Configuration Required, Credential Access
Reference
www.okta.com
Source
github.com/panther-labs/panther-analysis

Identifies successful Okta logins not followed by Push Security authorization within 60 minutes. Push Security provides additional identity verification beyond Okta MFA as a defense-in-depth strategy. Missing Push Security verification suggests compromised credentials, session hijacking, or MFA bypass where attackers satisfied Okta authentication but cannot complete additional verification.

MITRE ATT&CK coverage

Rule specification

AnalysisType: correlation_rule
RuleID: "Okta.Login.Without.Push.Group"
DisplayName: "Okta Login Without Push"
Enabled: false
Tags:
  - Okta
  - Push Security
  - Identity Verification
  - Configuration Required
  - Credential Access
Reports:
  MITRE ATT&CK:
    - TA0001:T1078 # Initial Access: Valid Accounts
    - TA0006:T1539 # Credential Access: Steal Web Session Cookie
    - TA0006:T1621 # Credential Access: Multi-Factor Authentication Request Generation
Severity: Critical
Description: >
  Identifies successful Okta logins not followed by Push Security authorization within 60 minutes. Push Security provides additional identity verification beyond Okta MFA as a defense-in-depth strategy. Missing Push Security verification suggests compromised credentials, session hijacking, or MFA bypass where attackers satisfied Okta authentication but cannot complete additional verification.
Runbook: |
  1. Query Okta System Log for all authentication events by actor.alternateId in the 90 minutes around the login to check if Push Security authentication occurred outside the 60-minute detection window, and review the source IP, geolocation, device, and MFA method used
  2. Query Push Security logs for any authentication attempts or failures by the same user in the 2 hours around the Okta login to determine if the user attempted but failed to complete Push Security verification
  3. Check Okta audit logs for all application access, permission changes, and administrative actions during the Okta session to identify suspicious activity that may indicate compromised credentials or session hijacking
Reference: https://www.okta.com/resources/datasheet/okta-adaptive-multi-factor-authentication-product-datasheet/
Detection:
  - Group:
      - ID: Push
        RuleID: Push.Security.Authorized.IdP.Login
        Absence: true
      - ID: Okta
        RuleID: Okta.Login.Success
    MatchCriteria:
      field_name:
        - GroupID: Push
          Match: new.email
        - GroupID: Okta
          Match: actor.alternateId
    Schedule:
      RateMinutes: 1440
      TimeoutMinutes: 10
    LookbackWindowMinutes: 1800

Stages and Predicates

Fires when the steps below all occur within 30h, correlated by actor.alternateId, new.email. Each step needs one match unless a higher minimum is shown.

Stage 1: step Push (negated)

References detection Push Security Authorized IdP Login.

Stage 2: step Okta

References detection Okta Login Signal.

Response runbook

1. Query Okta System Log for all authentication events by actor.alternateId in the 90 minutes around the login to check if Push Security authentication occurred outside the 60-minute detection window, and review the source IP, geolocation, device, and MFA method used

2. Query Push Security logs for any authentication attempts or failures by the same user in the 2 hours around the Okta login to determine if the user attempted but failed to complete Push Security verification

3. Check Okta audit logs for all application access, permission changes, and administrative actions during the Okta session to identify suspicious activity that may indicate compromised credentials or session hijacking

Okta Login Without Push Marker

#
Severity
medium
Log types
Okta.SystemLog
Tags
Push Security, Configuration Required
Source
github.com/panther-labs/panther-analysis

Detection logic

# configure this Push marker based on your environment
PUSH_MARKER = "PS_mxzqarw"


def rule(event):
    return not event.deep_get("client", "userAgent", "rawUserAgent", default="").endswith(
        PUSH_MARKER
    )


def title(event):
    actor = event.deep_get("actor", "displayName")
    return f"{actor} logged in from device without expected Push marker"

Rule specification

AnalysisType: rule
Filename: okta_login_without_push_marker.py
RuleID: "Okta.Login.Without.Push.Marker"
DisplayName: "Okta Login Without Push Marker"
Enabled: false
Tags:
  - Push Security
  - Configuration Required
LogTypes:
  - Okta.SystemLog
Severity: Medium
DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on Okta.SystemLog events when the condition below holds.

Condition

  • client.userAgent.rawUserAgent does not end with PS_mxzqarw

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
client.userAgent.rawUserAgentends_withPS_mxzqarwexcludes:client.userAgent.rawUserAgent field:"client.userAgent.rawUserAgent" value:"PS_mxzqarw"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
displayNameactor.displayName

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "alice.beaver@company.com",
    "displayName": "Alice Beaver",
    "id": "00u99ped55av2JpGs5d7",
    "type": "User"
  },
  "authenticationContext": {
    "authenticationStep": 0,
    "externalSessionId": "trsxcsf59kYRG-GwAbWjw-PZA"
  },
  "client": {
    "device": "Unknown",
    "ipAddress": "11.22.33.44",
    "userAgent": {
      "browser": "UNKNOWN",
      "os": "Unknown",
      "rawUserAgent": "Go-http-client/2.0"
    },
    "zone": "null"
  },
  "debugContext": {
    "debugData": {
      "dtHash": "53dd1a7513e0256eb13b9a47bb07ed61e8ca3d35fbdc36c909567a21a65a2b19",
      "rateLimitBucketUuid": "b192d91c-b242-36da-9332-d97a5579f865",
      "rateLimitScopeType": "ORG",
      "rateLimitSecondsToReset": "6",
      "requestId": "234cf34e0081e025e1fe14224464bbd6",
      "requestUri": "/api/v1/logs",
      "threshold": "20",
      "timeSpan": "1",
      "timeUnit": "MINUTES",
      "url": "/api/v1/logs?since=2023-09-21T17%3A04%3A22Z&limit=1000&after=1714675441520_1",
      "userId": "00u99ped55av2JpGs5d7",
      "warningPercent": "60"
    }
  },
  "displayMessage": "Rate limit warning",
  "eventType": "system.org.rate_limit.warning",
  "legacyEventType": "core.framework.ratelimit.warning",
  "outcome": {
    "result": "SUCCESS"
  },
  "published": "2024-05-02 18:46:21.121000000",
  "request": {
    "ipChain": [
      {
        "ip": "11.22.33.44",
        "version": "V4"
      }
    ]
  },
  "securityContext": {},
  "severity": "WARN",
  "target": [
    {
      "id": "/api/v1/logs",
      "type": "URL Pattern"
    },
    {
      "id": "b192d91c-b242-36da-9332-d97a5579f865",
      "type": "Bucket Uuid"
    }
  ],
  "transaction": {
    "detail": {
      "requestApiTokenId": "00T1bjatrp6Nl1dOc5d7"
    },
    "id": "234cf34e0081e025e1fe14224464bbd6",
    "type": "WEB"
  },
  "uuid": "44aeb388-08b4-11ef-9cec-73ffcb6f9fdd",
  "version": "0"
}

Okta MFA Globally Disabled

#
Severity
high
Log types
Okta.SystemLog
Tags
Identity & Access Management, DataModel, Okta, Defense Evasion:Modify Authentication Process
Reference
help.okta.com
Source
github.com/panther-labs/panther-analysis

An admin user has disabled the MFA requirement for your Okta account

MITRE ATT&CK coverage

Detection logic

import panther_event_type_helpers as event_type


def rule(event):
    return event.udm("event_type") == event_type.ADMIN_MFA_DISABLED


def title(event):
    return f"Okta System-wide MFA Disabled by Admin User {event.udm('actor_user')}"


def alert_context(event):
    context = {
        "user": event.udm("actor_user"),
        "ip": event.udm("source_ip"),
        "event": event.get("eventType"),
    }
    return context

Rule specification

AnalysisType: rule
Filename: okta_admin_disabled_mfa.py
RuleID: "Okta.Global.MFA.Disabled"
DisplayName: "Okta MFA Globally Disabled"
Enabled: true
LogTypes:
  - Okta.SystemLog
Tags:
  - Identity & Access Management
  - DataModel
  - Okta
  - Defense Evasion:Modify Authentication Process
Reports:
  MITRE ATT&CK:
    - TA0005:T1556
Severity: High
Description: An admin user has disabled the MFA requirement for your Okta account
Reference: https://help.okta.com/oie/en-us/content/topics/identity-engine/authenticators/about-authenticators.htm
Runbook: Contact Admin to ensure this was sanctioned activity
DedupPeriodMinutes: 15
SummaryAttributes:
  - eventType
  - severity
  - displayMessage
  - p_any_ip_addresses

Stages and Predicates

Fires on Okta.SystemLog events when the condition below holds.

Condition

  • event_type is admin_mfa_disabled
Alert deduplication
repeat matches within 15m group into one alert

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
useractor_user
ipsource_ip
eventeventType

Response runbook

Contact Admin to ensure this was sanctioned activity

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "homer@springfield.gov",
    "displayName": "Homer Simpson",
    "id": "111111",
    "type": "User"
  },
  "client": {
    "device": "Computer",
    "ipAddress": "1.1.1.1",
    "userAgent": {
      "browser": "CHROME",
      "os": "Mac OS X",
      "rawUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36"
    },
    "zone": "null"
  },
  "eventType": "system.mfa.factor.deactivate",
  "p_log_type": "Okta.SystemLog",
  "published": "2022-03-22 14:21:53.225",
  "severity": "HIGH",
  "version": "0"
}

Okta New Behaviors Acessing Admin Console

#
Severity
high
Log types
Okta.SystemLog
Reference
sec.okta.com
Source
github.com/panther-labs/panther-analysis

New Behaviors Observed while Accessing Okta Admin Console. A user attempted to access the Okta Admin Console from a new device with a new IP.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

import json

from panther_base_helpers import deep_get
from panther_okta_helpers import okta_alert_context


def rule(event):
    if event.get("eventtype") != "policy.evaluate_sign_on":
        return False

    if "Okta Admin Console" not in event.deep_walk("target", "displayName", default=""):
        return False

    behaviors = event.deep_get("debugContext", "debugData", "behaviors")
    if behaviors:
        return "New Device=POSITIVE" in behaviors and "New IP=POSITIVE" in behaviors

    log_only_security_data = event.deep_get("debugContext", "debugData", "logOnlySecurityData")
    if isinstance(log_only_security_data, str):
        log_only_security_data = json.loads(log_only_security_data)
    return (
        deep_get(log_only_security_data, "behaviors", "New Device") == "POSITIVE"
        and deep_get(log_only_security_data, "behaviors", "New IP") == "POSITIVE"
    )


def title(event):
    return (
        f"{event.deep_get('actor', 'displayName', default='<displayName-not-found>')} "
        f"<{event.deep_get('actor', 'alternateId', default='alternateId-not-found')}> "
        f"accessed Okta Admin Console using new behaviors: "
        f"New IP: {event.deep_get('client', 'ipAddress', default='<ipAddress-not-found>')} "
        f"New Device: {event.deep_get('device', 'name', default='<deviceName-not-found>')}"
    )


def alert_context(event):
    return okta_alert_context(event)

Rule specification

AnalysisType: rule
Filename: okta_new_behavior_accessing_admin_console.py
RuleID: "Okta.New.Behavior.Accessing.Admin.Console"
DisplayName: "Okta New Behaviors Acessing Admin Console"
Enabled: true
LogTypes:
  - Okta.SystemLog
Reports:
  MITRE ATT&CK:
    - TA0001:T1078.004 # Valid Accounts: Cloud Accounts
Severity: High
Description: >
  New Behaviors Observed while Accessing Okta Admin Console.
  A user attempted to access the Okta Admin Console from a new device with a new IP.
Runbook: >
  Configure Authentication Policies (Application Sign-on Policies) for access to privileged applications, including the Admin Console, to require re-authentication “at every sign-in”.
  Turn on and test New Device and Suspicious Activity end-user notifications.
Reference: >
  https://sec.okta.com/articles/2023/08/cross-tenant-impersonation-prevention-and-detection
DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on Okta.SystemLog events when all of the conditions below hold.

Condition

  • eventtype is policy.evaluate_sign_on
  • target.displayName contains Okta Admin Console
  • any of:
    • all of:
      • debugContext.debugData.behaviors is present
      • debugContext.debugData.behaviors contains New Device=POSITIVE
      • debugContext.debugData.behaviors contains New IP=POSITIVE
    • all of:
      • debugContext.debugData.behaviors is empty
      • debugContext.debugData.logOnlySecurityData.behaviors.New Device is POSITIVE
      • debugContext.debugData.logOnlySecurityData.behaviors.New IP is POSITIVE

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses
displayNameactor.displayName
alternateIdactor.alternateId
ipAddressclient.ipAddress
namedevice.name

Response runbook

Configure Authentication Policies (Application Sign-on Policies) for access to privileged applications, including the Admin Console, to require re-authentication “at every sign-in”. Turn on and test New Device and Suspicious Activity end-user notifications.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "homer.simpson@duff.com",
    "displayName": "Homer Simpson",
    "id": "00abc123",
    "type": "User"
  },
  "authenticationcontext": {
    "authenticationStep": 0,
    "externalSessionId": "100-abc-9999"
  },
  "client": {
    "device": "Computer",
    "geographicalContext": {
      "city": "Springfield",
      "country": "United States",
      "geolocation": {
        "lat": 20,
        "lon": -25
      },
      "postalCode": "12345",
      "state": "Ohio"
    },
    "ipAddress": "1.3.2.4",
    "userAgent": {
      "browser": "CHROME",
      "like Gecko) Chrome/102.0.0.0 Safari/537.36": null,
      "os": "Mac OS X",
      "rawUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML"
    },
    "zone": "null"
  },
  "debugcontext": {
    "debugData": {
      "behaviors": [
        "New Geo-Location=NEGATIVE",
        "New Device=POSITIVE",
        "New IP=POSITIVE",
        "New State=NEGATIVE",
        "New Country=NEGATIVE",
        "Velocity=NEGATIVE",
        "New City=NEGATIVE"
      ],
      "requestId": "AbCdEf12G",
      "requestUri": "/api/v1/users/AbCdEfG/lifecycle/reset_factors",
      "url": "/api/v1/users/AbCdEfG/lifecycle/reset_factors?"
    }
  },
  "device": {
    "name": "Evil Computer"
  },
  "displaymessage": "Evaluation of sign-on policy",
  "eventtype": "policy.evaluate_sign_on",
  "outcome": {
    "reason": "Sign-on policy evaluation resulted in CHALLENGE",
    "result": "CHALLENGE"
  },
  "published": "2022-06-22 18:18:29.015",
  "request": {
    "ipChain": [
      {
        "geographicalContext": {
          "city": "Springfield",
          "country": "United States",
          "geolocation": {
            "lat": 20,
            "lon": -25
          },
          "ip": "1.3.2.4",
          "postalCode": "12345",
          "state": "Ohio",
          "version": "V4"
        }
      }
    ]
  },
  "securitycontext": {
    "asNumber": 701,
    "asOrg": "verizon",
    "domain": "verizon.net",
    "isProxy": false,
    "isp": "verizon"
  },
  "severity": "INFO",
  "target": [
    {
      "alternateId": "Okta Admin Console",
      "displayName": "Okta Admin Console",
      "type": "AppInstance"
    },
    {
      "alternateId": "peter.griffin@company.com",
      "displayName": "Peter Griffin",
      "id": "0002222AAAA",
      "type": "User"
    }
  ],
  "transaction": {
    "detail": {},
    "id": "ABcDeFgG",
    "type": "WEB"
  },
  "uuid": "AbC-123-XyZ",
  "version": "0"
}

Okta Org2Org application created of modified

#
Severity
high
Log types
Okta.SystemLog
Reference
www.authomize.com
Source
github.com/panther-labs/panther-analysis

An Okta Org2Org application has been created or modified. Okta's Org2Org applications instances are used to push and match users from one Okta organization to another. A malicious actor can add an Org2Org application instance and create a user in the source organization (controlled by the attacker) with the same identifier as a Super Administrator in the target organization.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation
Credential Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_okta_helpers import okta_alert_context

APP_LIFECYCLE_EVENTS = (
    "application.lifecycle.update",
    "application.lifecycle.create",
    "application.lifecycle.activate",
)


def rule(event):
    if event.get("eventType") not in APP_LIFECYCLE_EVENTS:
        return False

    return "Org2Org" in event.deep_walk("target", "displayName", default="", return_val="first")


def title(event):
    action = event.get("eventType").split(".")[-1]
    target = event.deep_walk(
        "target", "alternateId", default="<alternateId-not-found>", return_val="first"
    )
    return (
        f"{event.deep_get('actor', 'displayName', default='<displayName-not-found>')} "
        f"<{event.deep_get('actor', 'alternateId', default='alternateId-not-found')}> "
        f"{action}d Org2Org app [{target}]"
    )


def severity(event):
    if "create" in event.get("eventType"):
        return "HIGH"
    return "MEDIUM"


def alert_context(event):
    return okta_alert_context(event)

Rule specification

AnalysisType: rule
Filename: okta_org2org_creation_modification.py
RuleID: "Okta.Org2org.Creation.Modification"
DisplayName: "Okta Org2Org application created of modified"
Enabled: true
LogTypes:
  - Okta.SystemLog
Reports:
  MITRE ATT&CK:
    - TA0006:T1556 # Modify Authentication Process
    - TA0004:T1078.004 # Valid Accounts: Cloud Accounts
Severity: High
Description: >
  An Okta Org2Org application has been created or modified.
  Okta's Org2Org applications instances are used to push and match users from one Okta organization to another.
  A malicious actor can add an Org2Org application instance and create a user in the source organization (controlled by the attacker)
  with the same identifier as a Super Administrator in the target organization.
Reference: >
  https://www.authomize.com/blog/authomize-discovers-password-stealing-and-impersonation-risks-to-in-okta/
DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on Okta.SystemLog events when all of the conditions below hold.

Condition

  • eventType is one of application.lifecycle.update, application.lifecycle.create, application.lifecycle.activate
  • target.displayName contains Org2Org

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventTypein
  • application.lifecycle.activate
  • application.lifecycle.create
  • application.lifecycle.update
field:"eventType" kind:in
target.displayNamecontains
  • Org2Org
field:"target.displayName" kind:contains value:"Org2Org"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses
displayNameactor.displayName
alternateIdactor.alternateId
alternateIdtarget.alternateId

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "homer.simpson@duff.com",
    "displayName": "Homer Simpson",
    "id": "00abc123",
    "type": "User"
  },
  "authenticationcontext": {
    "authenticationStep": 0,
    "externalSessionId": "100-abc-9999"
  },
  "client": {
    "device": "Computer",
    "geographicalContext": {
      "city": "Springfield",
      "country": "United States",
      "geolocation": {
        "lat": 20,
        "lon": -25
      },
      "postalCode": "12345",
      "state": "Ohio"
    },
    "ipAddress": "1.3.2.4",
    "userAgent": {
      "browser": "CHROME",
      "os": "Mac OS X",
      "rawUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36"
    },
    "zone": "null"
  },
  "debugcontext": {
    "debugData": {
      "behaviors": {
        "New City=NEGATIVE": null,
        "New Country=NEGATIVE": null,
        "New Device=POSITIVE": null,
        "New Geo-Location=NEGATIVE": null,
        "New IP=POSITIVE": null,
        "New State=NEGATIVE": null,
        "Velocity=NEGATIVE": null
      },
      "requestId": "AbCdEf12G",
      "requestUri": "/api/v1/users/AbCdEfG/lifecycle/reset_factors",
      "url": "/api/v1/users/AbCdEfG/lifecycle/reset_factors?"
    }
  },
  "device": {
    "name": "Evil Computer"
  },
  "displaymessage": "Evaluation of sign-on policy",
  "eventtype": "application.lifecycle.update",
  "outcome": {
    "reason": "Sign-on policy evaluation resulted in CHALLENGE",
    "result": "CHALLENGE"
  },
  "published": "2022-06-22 18:18:29.015",
  "request": {
    "ipChain": [
      {
        "geographicalContext": {
          "city": "Springfield",
          "country": "United States",
          "geolocation": {
            "lat": 20,
            "lon": -25
          },
          "ip": "1.3.2.4",
          "postalCode": "12345",
          "state": "Ohio",
          "version": "V4"
        }
      }
    ]
  },
  "securitycontext": {
    "asNumber": 701,
    "asOrg": "verizon",
    "domain": "verizon.net",
    "isProxy": false,
    "isp": "verizon"
  },
  "severity": "INFO",
  "target": [
    {
      "alternateId": "Okta Org2Org",
      "displayName": "Okta Org2Org",
      "type": "AppInstance"
    },
    {
      "alternateId": "peter.griffin@company.com",
      "displayName": "Peter Griffin",
      "id": "0002222AAAA",
      "type": "User"
    }
  ],
  "transaction": {
    "detail": {},
    "id": "ABcDeFgG",
    "type": "WEB"
  },
  "uuid": "AbC-123-XyZ",
  "version": "0"
}

Okta Password Accessed

#
Severity
medium
Group by
actor.alternateId
Entities
domain_names, emails, ip_addresses
Log types
Okta.SystemLog
Tags
Okta, Credential Access:Unsecured Credentials
Reference
help.okta.com
Source
github.com/panther-labs/panther-analysis

User accessed another user's application password

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Telemetry coverage

Detection logic

from panther_base_helpers import get_val_from_list

# pylint: disable=global-variable-undefined


def rule(event):
    global TARGET_USERS
    global TARGET_APP_NAMES

    if event.get("eventType") != "application.user_membership.show_password":
        return False

    # event['target'] = [{...}, {...}, {...}]
    TARGET_USERS = get_val_from_list(event.get("target", [{}]), "alternateId", "type", "User")
    TARGET_APP_NAMES = get_val_from_list(
        event.get("target", [{}]), "alternateId", "type", "AppInstance"
    )

    if event.deep_get("actor", "alternateId") not in TARGET_USERS:
        return True
    return False


def dedup(event):
    dedup_str = event.deep_get("actor", "alternateId")

    if TARGET_USERS:
        dedup_str += ":" + str(TARGET_USERS)
    if TARGET_APP_NAMES:
        dedup_str += ":" + str(TARGET_APP_NAMES)
    return dedup_str or ""


def title(event):
    return (
        f"A user {event.deep_get('actor', 'alternateId')} accessed another user's "
        f"{TARGET_USERS} "
        f"{TARGET_APP_NAMES} password"
    )

Rule specification

AnalysisType: rule
Filename: okta_password_accessed.py
RuleID: "Okta.PasswordAccess"
DisplayName: "Okta Password Accessed"
Enabled: true
LogTypes:
  - Okta.SystemLog
Tags:
  - Okta
  - Credential Access:Unsecured Credentials
Reports:
  MITRE ATT&CK:
    - TA0006:T1552
Severity: Medium
Description: >
  User accessed another user's application password
Reference: https://help.okta.com/en-us/content/topics/apps/apps_revealing_the_password.htm
Runbook: >
  Investigate whether this was authorized access.

Stages and Predicates

Fires on Okta.SystemLog events when the condition below holds.

Condition

  • eventType is application.user_membership.show_password

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventTypeeq
  • application.user_membership.show_password
field:"eventType" kind:eq value:"application.user_membership.show_password"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
alternateIdactor.alternateId

Response runbook

Investigate whether this was authorized access.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "eric.montgomery@email.com",
    "displayName": "Eric Montgomery",
    "id": "XXXXXXXXXXXXXXXX",
    "type": "User"
  },
  "authenticationContext": {
    "authenticationStep": 0,
    "externalSessionId": "XXXXXXXXXXXXXXXXX"
  },
  "client": {
    "device": "Mobile",
    "geographicalContext": {
      "country": "Iceland",
      "geolocation": {
        "lat": 81.0959,
        "lon": -10.30578
      },
      "state": "Colorado"
    },
    "ipAddress": "218.56.201.220",
    "userAgent": {
      "browser": "CHROME",
      "os": "Android 1.x",
      "rawUserAgent": "Mozilla/5.0 (Linux; Android 11; ONEPLUS A6013) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Mobile Safari/537.36"
    },
    "zone": "null"
  },
  "debugContext": {
    "debugData": ""
  },
  "eventType": "application.user_membership.show_password",
  "legacyEventType": "app.generic.show.password",
  "outcome": {
    "result": "SUCCESS"
  },
  "p_any_domain_names": [
    "."
  ],
  "p_any_emails": [
    "eric.montgomery@email.com"
  ],
  "p_any_ip_addresses": [
    "218.56.201.220"
  ],
  "p_log_type": "Okta.SystemLog",
  "published": "2022-09-09 04:26:09.792",
  "request": {
    "ipChain": [
      {
        "geographicalContext": {
          "country": "Iceland",
          "geolocation": {
            "lat": 81.09596,
            "lon": -10.30578
          }
        },
        "ip": "218.56.201.220",
        "version": "V4"
      }
    ]
  },
  "securityContext": {
    "asNumber": 124526,
    "asOrg": "t-mobile",
    "domain": ".",
    "isProxy": false,
    "isp": "t-mobile usa  inc."
  },
  "severity": "INFO",
  "target": [
    {
      "alternateId": "vanessajohns@email.com",
      "displayName": "Vanessa Johns",
      "id": "0uat6tr9otyvdJbBM696",
      "type": "AppUser"
    },
    {
      "alternateId": "Application3",
      "displayName": "Application3",
      "id": "0oas6wl204Dn3gG5D696",
      "type": "AppInstance"
    },
    {
      "alternateId": "vanessajohns@email.com",
      "displayName": "Vanessa Johns",
      "id": "XXXXXXXXXXXXXXXX",
      "type": "User"
    }
  ],
  "transaction": {
    "detail": {},
    "id": "XXXXXXXXXXXXXXXX",
    "type": "WEB"
  },
  "uuid": "XXXXXXXXXXXXXXXX",
  "version": "0"
}

Okta Potentially Stolen Session

#
Severity
high
Entities
domain_names, ip_addresses
Log types
Okta.SystemLog
Tags
Identity & Access Management, Okta
Reference
sec.okta.com
Source
github.com/panther-labs/panther-analysis

This rule looks for the same session being used from two devices, indicating a compromised session token.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

import json
from datetime import timedelta
from difflib import SequenceMatcher

from panther_detection_helpers.caching import get_string_set, put_string_set
from panther_okta_helpers import okta_alert_context

FUZZ_RATIO_MIN = 0.95
PREVIOUS_SESSION = {}
# the number of days an Okta session is valid for (configured in Okta)
SESSION_TIMEOUT = timedelta(days=1).total_seconds()
EVENT_TYPES = ("user.authentication.sso", "user.session.start")


def rule(event):
    # pylint: disable=global-statement
    # ensure previous session info is avaialable in the alert_context for investigation
    global PREVIOUS_SESSION

    session_id = event.deep_get("authenticationContext", "externalSessionId", default="unknown")
    dt_hash = event.deep_get("debugContext", "debugData", "dtHash", default="unknown")

    # Some events by Okta admins may appear to have changed IPs
    # and user agents due to internal Okta behavior:
    # https://support.okta.com/help/s/article/okta-integrations-showing-as-rawuseragent-with-okta-ips
    # As such, we ignore certain client ids known to originate from Okta:
    # https://developer.okta.com/docs/api/openapi/okta-myaccount/myaccount/tag/OktaApplications/
    if event.deep_get("client", "id") in [
        "okta.b58d5b75-07d4-5f25-bf59-368a1261a405"  # Admin Console
    ]:
        return False

    # Filter only on app access and session start events
    if event.get("eventType") not in EVENT_TYPES or (
        session_id == "unknown" or dt_hash == "unknown"
    ):
        return False

    key = session_id + "-" + dt_hash

    # lookup if we've previously stored the session cookie
    PREVIOUS_SESSION = get_string_set(key)

    # For unit test mocks we need to eval the string to a set
    if isinstance(PREVIOUS_SESSION, str):
        PREVIOUS_SESSION = set(json.loads(PREVIOUS_SESSION))

    # If the sessionID has not been seen before, store information about it
    if not PREVIOUS_SESSION:
        put_string_set(
            key,
            [
                str(event.deep_get("securityContext", "asNumber")),
                event.deep_get("client", "ipAddress"),
                # clearly label the user agent string so we can find it during the comparison
                "user_agent:" + event.deep_get("client", "userAgent", "rawUserAgent"),
                event.deep_get("client", "userAgent", "browser"),
                event.deep_get("client", "userAgent", "os"),
                event.get("p_event_time"),
                "sign_on_mode:"
                + event.deep_get("debugContext", "debugData", "signOnMode", default="unknown"),
                "threat_suspected:"
                + event.deep_get(
                    "debugContext", "debugData", "threat_suspected", default="unknown"
                ),
            ],
            epoch_seconds=event.event_time_epoch() + SESSION_TIMEOUT,
        )

    # if the session cookie was seen before
    else:
        # we use a fuzz match to compare the current and prev user agent.
        # We cannot do a direct match since Okta can occasionally maintain
        # a session across browser upgrades.

        # the user-agent was tagged during storage so we can find it, remove that tag
        [prev_ua] = [x for x in PREVIOUS_SESSION if "user_agent:" in x] or ["prev_ua_not_found"]
        prev_ua = prev_ua.split("_agent:")[1]

        diff_ratio = SequenceMatcher(
            None,
            event.deep_get("client", "userAgent", "rawUserAgent", default="ua_not_found"),
            prev_ua,
        ).ratio()

        # is this session being used from a new IP and a different browser
        if (
            str(event.deep_get("client", "ipAddress", default="ip_not_found"))
            not in PREVIOUS_SESSION
            and diff_ratio < FUZZ_RATIO_MIN
        ):
            # make the fuzz ratio available in the alert context
            PREVIOUS_SESSION.add("Fuzz Ratio: " + str(diff_ratio))
            return True

    return False


def title(event):
    return (
        f"Potentially Stolen Okta Session - "
        f"{event.deep_get('actor', 'displayName', default='Unknown_user')}"
    )


def alert_context(event):
    context = okta_alert_context(event)
    context["previous_session"] = str(PREVIOUS_SESSION)
    return context

Rule specification

AnalysisType: rule
Filename: okta_potentially_stolen_session.py
RuleID: Okta.PotentiallyStolenSession
DisplayName: Okta Potentially Stolen Session
Enabled: true
LogTypes:
  - Okta.SystemLog
Tags:
  - Identity & Access Management
  - Okta
Reports:
  MITRE ATT&CK:
    - TA0006:T1539
Severity: High
Description: This rule looks for the same session being used from two devices, indicating a compromised session token.
Runbook: Confirm the session is used on two devices, one of which is unknown. Lock the users Okta account and clear the users sessions in down stream apps.
Reference: https://sec.okta.com/sessioncookietheft
SummaryAttributes:
  - eventType
  - severity
  - p_any_ip_addresses
  - p_any_domain_names

Stages and Predicates

Fires on Okta.SystemLog events when all of the conditions below hold.

Condition

  • client.id is not one of okta.b58d5b75-07d4-5f25-bf59-368a1261a405
  • eventType is one of user.authentication.sso, user.session.start
  • authenticationContext.externalSessionId is not unknown
  • debugContext.debugData.dtHash is not unknown

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventTypein
  • user.authentication.sso
  • user.session.start
field:"eventType" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses
displayNameactor.displayName

Response runbook

Confirm the session is used on two devices, one of which is unknown. Lock the users Okta account and clear the users sessions in down stream apps.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "admin",
    "displayName": "Bobert",
    "id": "unknown",
    "type": "User"
  },
  "authenticationContext": {
    "authenticationStep": 0,
    "externalSessionId": "123456789"
  },
  "client": {
    "device": "Computer",
    "geographicalContext": {
      "city": "Dois Irmaos",
      "country": "Brazil",
      "geolocation": {
        "lat": -29.6116,
        "lon": -51.0933
      },
      "postalCode": "93950",
      "state": "Rio Grande do Sul"
    },
    "ipAddress": "1.2.3.4",
    "userAgent": {
      "browser": "CHROME",
      "os": "Linux",
      "rawUserAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36"
    },
    "zone": "null"
  },
  "debugContext": {
    "debugData": {
      "dtHash": "kzpx58a99d2oam082rlu588wgy1mb0zfi1e1l63f9cjx4uxc455k4t6xdiwbxian",
      "loginResult": "VERIFICATION_ERROR",
      "requestId": "redacted",
      "requestUri": "redacted",
      "threatSuspected": "false",
      "url": "redacted"
    }
  },
  "displayMessage": "User login to Okta",
  "eventType": "user.session.start",
  "legacyEventType": "core.user_auth.login_failed",
  "outcome": {
    "reason": "VERIFICATION_ERROR",
    "result": "FAILURE"
  },
  "p_any_domain_names": [
    "rnvtelecom.com.br"
  ],
  "p_any_ip_addresses": [
    "redacted"
  ],
  "p_event_time": "redacted",
  "p_log_type": "Okta.SystemLog",
  "p_parse_time": "redacted",
  "p_row_id": "redacted",
  "p_source_id": "redacted",
  "p_source_label": "Okta",
  "published": "redacted",
  "request": {
    "ipChain": [
      {
        "geographicalContext": {
          "city": "Dois Irmaos",
          "country": "Brazil",
          "geolocation": {
            "lat": -29.6116,
            "lon": -51.0933
          },
          "postalCode": "93950",
          "state": "Rio Grande do Sul"
        },
        "ip": "redacted",
        "version": "V4"
      }
    ]
  },
  "securityContext": {
    "asNumber": 263297,
    "asOrg": "renovare telecom",
    "domain": "rnvtelecom.com.br",
    "isProxy": false,
    "isp": "renovare telecom"
  },
  "severity": "INFO",
  "transaction": {
    "detail": {},
    "id": "redacted",
    "type": "WEB"
  },
  "uuid": "redacted",
  "version": "0"
}

Okta Rate Limits

#
Severity
low
Group by
actor.id
Log types
Okta.SystemLog
Tags
Credential Access, Brute Force, Impact, Network Denial of Service
Reference
developer.okta.com
Source
github.com/panther-labs/panther-analysis

Potential DoS/Bruteforce attack or hitting limits (system degradation)

MITRE ATT&CK coverage

TacticTechniques
Credential Access
Impact

Telemetry coverage

Detection logic

from fnmatch import fnmatch

from panther_okta_helpers import okta_alert_context

DETECTION_EVENTS = [
    "app.oauth2.client_id_rate_limit_warning",
    "application.integration.rate_limit_exceeded",
    "system.client.rate_limit.*",
    "system.client.concurrency_rate_limit.*",
    "system.operation.rate_limit.*",
    "system.org.rate_limit.*",
    "core.concurrency.org.limit.violation",
]


def rule(event):
    eventtype = event.get("eventtype", "")
    for detection_event in DETECTION_EVENTS:
        if fnmatch(eventtype, detection_event) and "violation" in eventtype:
            return True
    return False


def title(event):
    actor = event.deep_get("actor", "alternateId")
    if actor == "unknown":
        actor = event.deep_get("actor", "displayName", default="<id-not-found>")
    return (
        f"Okta Rate Limit Event: [{event.get('eventtype','')}] "
        f"by [{actor}/{event.deep_get('actor', 'type', default='<type-not-found>')}] "
    )


def dedup(event):
    return event.deep_get("actor", "id")


def alert_context(event):
    return okta_alert_context(event)

Rule specification

AnalysisType: rule
Description: Potential DoS/Bruteforce attack or hitting limits (system degradation)
DisplayName: "Okta Rate Limits"
Enabled: true
Filename: okta_rate_limits.py
Severity: Low
Tags:
  - Credential Access
  - Brute Force
  - Impact
  - Network Denial of Service
Reports:
  MITRE ATT&CK:
    - TA0006:T1110
    - TA0040:T1498
Reference: https://developer.okta.com/docs/reference/rl-system-log-events/
DedupPeriodMinutes: 1440 # 24 hours
LogTypes:
  - Okta.SystemLog
RuleID: "Okta.Rate.Limits"
Threshold: 1

Stages and Predicates

Fires on Okta.SystemLog events when any of the conditions below holds.

Condition

  • any of:
    • all of:
      • eventtype matches the pattern app.oauth2.client_id_rate_limit_warning
      • eventtype contains violation
    • all of:
      • eventtype matches the pattern application.integration.rate_limit_exceeded
      • eventtype contains violation
    • all of:
      • eventtype matches the pattern system.client.rate_limit.*
      • eventtype contains violation
    • all of:
      • eventtype matches the pattern system.client.concurrency_rate_limit.*
      • eventtype contains violation
    • all of:
      • eventtype matches the pattern system.operation.rate_limit.*
      • eventtype contains violation
    • all of:
      • eventtype matches the pattern system.org.rate_limit.*
      • eventtype contains violation
    • all of:
      • eventtype matches the pattern core.concurrency.org.limit.violation
      • eventtype contains violation
Alert deduplication
repeat matches within 1d group into one alert

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses
displayNameactor.displayName
typeactor.type

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "homer.simpson@duff.com",
    "displayName": "Homer Simpson",
    "id": "00abc456",
    "type": "User"
  },
  "authenticationcontext": {
    "authenticationStep": 0,
    "externalSessionId": "abc12345"
  },
  "client": {
    "device": "Unknown",
    "ipAddress": "1.2.3.4",
    "userAgent": {
      "browser": "UNKNOWN",
      "os": "Unknown",
      "rawUserAgent": "Chrome"
    },
    "zone": "null"
  },
  "debugcontext": {
    "debugData": {
      "authnRequestId": "ABCDFDE",
      "dtHash": "adfalsjflasfjsdfd",
      "operationRateLimitScopeType": "User",
      "operationRateLimitSecondsToReset": "10",
      "operationRateLimitSubtype": "Authenticated user",
      "operationRateLimitThreshold": "40",
      "operationRateLimitTimeSpan": "10",
      "operationRateLimitTimeUnit": "SECONDS",
      "operationRateLimitType": "Web request",
      "requestId": "asfsagadffdaf",
      "requestUri": "/app/google/",
      "url": "/app/google/"
    }
  },
  "displaymessage": "Operation rate limit violation",
  "eventtype": "system.operation.rate_limit.violation",
  "outcome": {
    "reason": "Too many requests attempted by an individual user",
    "result": "DENY"
  },
  "published": "2022-08-29 16:07:26.592",
  "request": {
    "ipChain": [
      {
        "ip": "1.2.3.4",
        "version": "V4"
      }
    ]
  },
  "securitycontext": {},
  "severity": "WARN",
  "target": [
    {
      "id": "/app/{app}/{key}/",
      "type": "URL Pattern"
    }
  ],
  "transaction": {
    "detail": {},
    "id": "YABCDE",
    "type": "WEB"
  },
  "uuid": "asdfdashh",
  "version": "0"
}

Okta Sign-In from VPN Anonymizer

#
Severity
medium
Log types
Okta.SystemLog
Reference
sec.okta.com
Source
github.com/panther-labs/panther-analysis

A user is attempting to sign-in to Okta from a known VPN anonymizer. The threat actor would access the compromised account using anonymizing proxy services.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_okta_helpers import okta_alert_context


def rule(event):
    return event.get("eventType") == "user.session.start" and event.deep_get(
        "securityContext", "isProxy", default=False
    )


def title(event):
    ip_context = {}
    client = event.get("client", default={})
    security_context = event.get("securityContext", default={})
    if client.get("ipAddress"):
        ip_context["IP"] = client.get("ipAddress")
    for key, source_value in [
        {"ASO", security_context.get("asOrg")},
        {"ISP", security_context.get("isp")},
        {"Domain", security_context.get("domain")},
    ]:
        if source_value:
            ip_context[key] = source_value

    if service := event.deep_get("p_enrichment", "ipinfo_privacy", "client.ipAddress", "service"):
        ip_context["Service"] = service

    return (
        f"{event.deep_get('actor', 'displayName', default='<displayName-not-found>')} "
        f"<{event.deep_get('actor', 'alternateId', default='alternateId-not-found')}> "
        f"attempted to sign-in from anonymizing VPN - {ip_context}"
    )


def alert_context(event):
    return okta_alert_context(event)


def severity(event):
    # If the user is using Apple Private Relay, demote the severity to INFO
    if (
        event.deep_get("p_enrichment", "ipinfo_privacy", "client.ipAddress", "service")
        == "Apple Private Relay"
    ):
        return "INFO"
    # Return Medium by default
    return "MEDIUM"

Rule specification

AnalysisType: rule
Filename: okta_anonymizing_vpn_login.py
RuleID: "Okta.Anonymizing.VPN.Login"
DisplayName: "Okta Sign-In from VPN Anonymizer"
Enabled: true
LogTypes:
  - Okta.SystemLog
Reports:
  MITRE ATT&CK:
    - TA0006:T1556 # Modify Authentication Process
Severity: Medium
Description: >
  A user is attempting to sign-in to Okta from a known VPN anonymizer.  The threat actor would access the compromised account using anonymizing proxy services.
Runbook: >
  Restrict this access to trusted Network Zones and deny access from anonymizing proxies in policy using a Dynamic Network Zone.
Reference: >
  https://sec.okta.com/articles/2023/08/cross-tenant-impersonation-prevention-and-detection
DedupPeriodMinutes: 360 # 6 hours
Threshold: 1

Stages and Predicates

Fires on Okta.SystemLog events when all of the conditions below hold.

Condition

  • eventType is user.session.start
  • securityContext.isProxy is present
Alert deduplication
repeat matches within 6h group into one alert

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses
displayNameactor.displayName
alternateIdactor.alternateId

Response runbook

Restrict this access to trusted Network Zones and deny access from anonymizing proxies in policy using a Dynamic Network Zone.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "homer.simpson@duff.com",
    "displayName": "Homer Simpson",
    "id": "00abc123",
    "type": "User"
  },
  "authenticationcontext": {
    "authenticationStep": 0,
    "externalSessionId": "100-abc-9999"
  },
  "client": {
    "device": "Computer",
    "geographicalContext": {
      "city": "Springfield",
      "country": "United States",
      "geolocation": {
        "lat": 20,
        "lon": -25
      },
      "postalCode": "12345",
      "state": "Ohio"
    },
    "ipAddress": "1.3.2.4",
    "userAgent": {
      "browser": "CHROME",
      "os": "Mac OS X",
      "rawUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36"
    },
    "zone": "null"
  },
  "debugcontext": {
    "debugData": {
      "requestId": "AbCdEf12G",
      "requestUri": "/api/v1/users/AbCdEfG/lifecycle/reset_factors",
      "url": "/api/v1/users/AbCdEfG/lifecycle/reset_factors?"
    }
  },
  "displaymessage": "Authentication of user via MFA",
  "eventtype": "user.session.start",
  "legacyeventtype": "core.user.factor.attempt_fail",
  "outcome": {
    "reason": "FastPass declined phishing attempt",
    "result": "FAILURE"
  },
  "published": "2022-06-22 18:18:29.015",
  "request": {
    "ipChain": [
      {
        "geographicalContext": {
          "city": "Springfield",
          "country": "United States",
          "geolocation": {
            "lat": 20,
            "lon": -25
          },
          "postalCode": "12345",
          "state": "Ohio"
        },
        "ip": "1.3.2.4",
        "version": "V4"
      }
    ]
  },
  "securitycontext": {
    "asNumber": 701,
    "asOrg": "verizon",
    "domain": "anonymous.org",
    "isProxy": true,
    "isp": "verizon"
  },
  "severity": "INFO",
  "target": [
    {
      "alternateId": "peter.griffin@company.com",
      "displayName": "Peter Griffin",
      "id": "0002222AAAA",
      "type": "User"
    }
  ],
  "transaction": {
    "detail": {},
    "id": "ABcDeFgG",
    "type": "WEB"
  },
  "uuid": "AbC-123-XyZ",
  "version": "0"
}

Okta Support Access

#

This is a threat-hunting query, not an automated detection. It surfaces activity for an analyst to review rather than firing on a match. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

Source
github.com/panther-labs/panther-analysis

Show instances that Okta support was granted to your account

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule specification

AnalysisType: scheduled_query
QueryName: "Okta Support Access"
Enabled: false
Description: >
  Show instances that Okta support was granted to your account
SnowflakeQuery: |
  SELECT
  p_event_time as event_time,
  actor:alternateId as actor_email,
  actor:displayName as actor_name,
  client:ipAddress as src_ip,
  client:geographicalContext:city as city,
  client:geographicalContext:country as country,
  client:userAgent:rawUserAgent as user_agent,
  displayMessage,
  eventType
  FROM
  panther_logs.public.okta_systemlog
  WHERE
    eventType = 'user.session.impersonation.grant'
    OR
    eventType = 'user.session.impersonation.initiate'
   AND
      p_occurs_between('2022-01-14','2022-03-22')
  ORDER BY
    event_time desc

DatabricksQuery: |
  SELECT
  p_event_time as event_time,
  actor:alternateId as actor_email,
  actor:displayName as actor_name,
  client:ipAddress as src_ip,
  client:geographicalContext:city as city,
  client:geographicalContext:country as country,
  client:userAgent:rawUserAgent as user_agent,
  displayMessage,
  eventType
  FROM
  panther_logs.okta_systemlog
  WHERE
    eventType = 'user.session.impersonation.grant'
    OR
    eventType = 'user.session.impersonation.initiate'
   AND
      p_occurs_between('2022-01-14','2022-03-22')
  ORDER BY
    event_time desc
Schedule:
  RateMinutes: 43200
  TimeoutMinutes: 1

Stages and Predicates

Stage 1: source

Table
panther_logs.public.okta_systemlog

Stage 2: filter

  • any of:
    • eventType is user.session.impersonation.grant
    • eventType is user.session.impersonation.initiate

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventTypeeq
  • user.session.impersonation.grant
  • user.session.impersonation.initiate
field:"eventType" kind:eq

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_timep_event_time
actor_emailactor:alternateId
actor_nameactor:displayName
src_ipclient:ipAddress
cityclient:geographicalContext:city
countryclient:geographicalContext:country
user_agentclient:userAgent:rawUserAgent
displayMessage
eventType

Okta Support Access Granted

#
Severity
medium
Log types
Okta.SystemLog
Tags
Identity & Access Management, DataModel, Okta, Initial Access:Trusted Relationship
Reference
help.okta.com
Source
github.com/panther-labs/panther-analysis

An admin user has granted access to Okta Support to your account

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

OKTA_SUPPORT_ACCESS_EVENTS = [
    "user.session.impersonation.grant",
    "user.session.impersonation.initiate",
]


def rule(event):
    return event.get("eventType") in OKTA_SUPPORT_ACCESS_EVENTS


def title(event):
    return f"Okta Support Access Granted by {event.udm('actor_user')}"


def alert_context(event):
    context = {
        "user": event.udm("actor_user"),
        "ip": event.udm("source_ip"),
        "event": event.get("eventType"),
    }
    return context

Rule specification

AnalysisType: rule
Filename: okta_account_support_access.py
RuleID: "Okta.Support.Access"
DisplayName: "Okta Support Access Granted"
Enabled: true
LogTypes:
  - Okta.SystemLog
Tags:
  - Identity & Access Management
  - DataModel
  - Okta
  - Initial Access:Trusted Relationship
Reports:
  MITRE ATT&CK:
    - TA0001:T1199
Severity: Medium
Description: An admin user has granted access to Okta Support to your account
Reference: https://help.okta.com/en/prod/Content/Topics/Settings/settings-support-access.htm
Runbook: Contact Admin to ensure this was sanctioned activity
DedupPeriodMinutes: 15
SummaryAttributes:
  - eventType
  - severity
  - displayMessage
  - p_any_ip_addresses

Stages and Predicates

Fires on Okta.SystemLog events when the condition below holds.

Condition

  • eventType is one of user.session.impersonation.grant, user.session.impersonation.initiate
Alert deduplication
repeat matches within 15m group into one alert

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventTypein
  • user.session.impersonation.grant
  • user.session.impersonation.initiate
field:"eventType" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
useractor_user
ipsource_ip
eventeventType

Response runbook

Contact Admin to ensure this was sanctioned activity

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "homer@springfield.gov",
    "displayName": "Homer Simpson",
    "id": "111111",
    "type": "User"
  },
  "client": {
    "device": "Computer",
    "ipAddress": "1.1.1.1",
    "userAgent": {
      "browser": "CHROME",
      "os": "Mac OS X",
      "rawUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36"
    },
    "zone": "null"
  },
  "displayMessage": "Enable impersonation grant",
  "eventType": "user.session.impersonation.grant",
  "legacyEventType": "core.user.impersonation.grant.enabled",
  "p_log_type": "Okta.SystemLog",
  "published": "2022-03-22 14:21:53.225",
  "severity": "INFO",
  "version": "0"
}

Okta Support Reset Credential

#
Severity
high
Log types
Okta.SystemLog
Tags
Identity & Access Management, DataModel, Okta, Initial Access:Trusted Relationship
Reference
help.okta.com
Source
github.com/panther-labs/panther-analysis

A Password or MFA factor was reset by Okta Support

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_okta_helpers import okta_alert_context

OKTA_SUPPORT_RESET_EVENTS = [
    "user.account.reset_password",
    "user.mfa.factor.update",
    "system.mfa.factor.deactivate",
    "user.mfa.attempt_bypass",
]


def rule(event):
    if event.get("eventType") not in OKTA_SUPPORT_RESET_EVENTS:
        return False
    return (
        event.deep_get("actor", "alternateId") == "system@okta.com"
        and event.deep_get("transaction", "id") == "unknown"
        and event.deep_get("client", "userAgent", "rawUserAgent") is None
        and event.deep_get("client", "geographicalContext", "country") is None
    )


def title(event):
    targets = event.get("target") or []
    impacted = next((t for t in targets if t.get("type") == "User"), None) or {}
    user = impacted.get("alternateId") or impacted.get("displayName") or "<unknown-user>"
    return f"Okta Support Reset Password or MFA for user {user}"


def alert_context(event):
    return okta_alert_context(event)

Rule specification

AnalysisType: rule
Filename: okta_support_reset.py
RuleID: "Okta.Support.Reset"
DisplayName: "Okta Support Reset Credential"
Enabled: true
LogTypes:
  - Okta.SystemLog
Tags:
  - Identity & Access Management
  - DataModel
  - Okta
  - Initial Access:Trusted Relationship
Reports:
  MITRE ATT&CK:
    - TA0001:T1199
Severity: High
Description: A Password or MFA factor was reset by Okta Support
Reference: https://help.okta.com/en/prod/Content/Topics/Directory/get-support.htm#:~:text=Visit%20the%20Okta%20Help%20Center,1%2D800%2D219%2D0964
Runbook: Contact Admin to ensure this was sanctioned activity
DedupPeriodMinutes: 15
SummaryAttributes:
  - eventType
  - severity
  - p_any_ip_addresses

Stages and Predicates

Fires on Okta.SystemLog events when all of the conditions below hold.

Condition

  • eventType is one of user.account.reset_password, user.mfa.factor.update, system.mfa.factor.deactivate, user.mfa.attempt_bypass
  • actor.alternateId is system@okta.com
  • transaction.id is unknown
  • client.userAgent.rawUserAgent is empty
  • client.geographicalContext.country is empty
Alert deduplication
repeat matches within 15m group into one alert

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses

Response runbook

Contact Admin to ensure this was sanctioned activity

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "system@okta.com",
    "displayName": "system@okta.com",
    "id": "1111111",
    "type": "User"
  },
  "client": {
    "device": "Computer",
    "ipAddress": "1.1.1.1",
    "userAgent": {
      "browser": "CHROME",
      "os": "Mac OS X"
    },
    "zone": "null"
  },
  "displayMessage": "Fired when the user's Okta password is reset",
  "eventType": "user.account.reset_password",
  "legacyEventType": "core.user.config.user_status.password_reset",
  "outcome": {
    "result": "SUCCESS"
  },
  "p_log_type": "Okta.SystemLog",
  "published": "2021-11-29 18:56:40.014",
  "severity": "INFO",
  "target": [
    {
      "alternateId": "homer@springfield.gov",
      "displayName": "Homer Simpson",
      "id": "1111111",
      "type": "User"
    }
  ],
  "transaction": {
    "detail": {},
    "id": "unknown",
    "type": "WEB"
  },
  "uuid": "12343",
  "version": "0"
}

Okta SWA Bulk Access, New Source, and Credential Extraction - Behavioral

#
Status
Experimental
Severity
high
Tags
Identity & Access Management, Okta, SWA, Credential Access:Credentials from Password Stores, Collection:Data from Information Repositories, Initial Access:Valid Accounts, Anomaly Detection
Reference
www.varonis.com
Source
github.com/panther-labs/panther-analysis

Detects Okta SWA (Secure Web Authentication) bulk credential extraction, abuse, and access from previously unseen IP addresses or user agents using behavioral z-score and source novelty analysis. SWA apps store credentials in Okta's encrypted vault. Admin accounts with SWA access can view or rotate credentials for users across many apps. This detection builds a 90-day behavioral baseline for each admin's SWA access, credential change patterns, and known source IPs/user agents, then identifies anomalous spikes or new sources in the last 7 days. Detection Logic: - Z-score: SWA authentication volume spike (> 3σ above baseline) - Z-score: Unique SWA app diversity spike (many different apps accessed in one hour) (> 3σ) - Z-score: Credential extraction volume spike (> 3σ) - Z-score: Victim diversity spike (credential changes across many users) (> 2σ) - Cold-start: First-time bulk SWA access (>= 10 events, no prior baseline) - Cold-start: First-time credential extraction (>= 5 extractions, no prior baseline) - New source: SWA access from IP address not seen in 90-day baseline - New source: SWA access from user agent not seen in 90-day baseline - Critical compound: New IP + any credential extraction events Why This Matters: SWA credential extraction is a powerful lateral movement technique. An attacker with admin access can silently retrieve plaintext credentials for hundreds of SWA-protected applications without triggering MFA or generating obvious authentication failures. New source detection catches the initial access phase when a compromised admin account is used from an unfamiliar device or location. Complementary Detection: Use alongside Okta.SWA.OffHoursAccess.Behavioral which detects the same attack vector occurring outside normal business hours.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

def rule(event):
    # Query already filtered for is_anomalous = TRUE.
    # Guard against malformed rows missing the primary key field.
    return bool(event.get("admin_email"))


def title(event):
    admin = event.get("admin_email", "Unknown")
    recent_extractions = event.get("recent_total_extractions") or 0
    recent_swa_events = event.get("recent_total_swa_events") or 0
    has_new_ip = event.get("has_new_ip") or False
    new_ip_extraction_count = event.get("new_ip_extraction_count") or 0
    if recent_extractions > 0 and has_new_ip:
        return (
            f"Okta SWA: Credential Extraction from New IP by {admin}"
            f" ({new_ip_extraction_count} extractions from new source)"
        )
    if recent_extractions > 0:
        return f"Okta SWA: Bulk Credential Extraction by {admin} ({recent_extractions} extractions)"
    if has_new_ip:
        return f"Okta SWA: Access from New IP by {admin} ({recent_swa_events} events)"
    return f"Okta SWA: Bulk App Access Anomaly by {admin} ({recent_swa_events} events)"


def severity(event):
    is_first_extraction = event.get("is_first_time_credential_extraction") or False
    is_extraction_anomaly = event.get("is_extraction_volume_anomaly") or False
    is_victim_anomaly = event.get("is_victim_diversity_anomaly") or False
    recent_extractions = event.get("recent_total_extractions") or 0
    has_new_ip = event.get("has_new_ip") or False
    has_new_user_agent = event.get("has_new_user_agent") or False
    new_ip_extraction_count = event.get("new_ip_extraction_count") or 0
    score = event.get("anomaly_severity_score") or 0
    if has_new_ip and new_ip_extraction_count > 0:
        return "CRITICAL"
    if is_first_extraction or is_extraction_anomaly or is_victim_anomaly:
        return "CRITICAL"
    if has_new_ip or recent_extractions > 0 or score > 20:
        return "HIGH"
    if has_new_user_agent:
        return "MEDIUM"
    return "MEDIUM"  # Default: anomalous SWA volume with no other escalating signals


def dedup_key(event):
    admin = event.get("admin_email", "unknown")
    first_event = str(
        event.get("recent_extraction_first_event") or event.get("recent_swa_first_event", "unknown")
    )[:10]
    return f"okta_swa_bulk_{admin}_{first_event}"


def alert_context(event):
    return {
        "admin_email": event.get("admin_email"),
        "recent_total_extractions": event.get("recent_total_extractions"),
        "recent_max_victim_diversity_per_hour": event.get("recent_max_victim_diversity_per_hour"),
        "recent_total_swa_events": event.get("recent_total_swa_events"),
        "recent_max_app_diversity_per_hour": event.get("recent_max_app_diversity_per_hour"),
        "z_score_extraction_volume": event.get("z_score_extraction_volume"),
        "z_score_victim_diversity": event.get("z_score_victim_diversity"),
        "z_score_swa_volume": event.get("z_score_swa_volume"),
        "has_new_ip": event.get("has_new_ip"),
        "has_new_user_agent": event.get("has_new_user_agent"),
        "new_ip_count": event.get("new_ip_count"),
        "new_ip_extraction_count": event.get("new_ip_extraction_count"),
        "new_ip_victim_count": event.get("new_ip_victim_count"),
        "anomaly_severity_score": event.get("anomaly_severity_score"),
        "is_first_time_credential_extraction": event.get("is_first_time_credential_extraction"),
        "recent_extraction_first_event": event.get("recent_extraction_first_event"),
        "recent_extraction_last_event": event.get("recent_extraction_last_event"),
    }

Rule specification

AnalysisType: scheduled_rule
Filename: okta_swa_bulk_access_behavioral.py
RuleID: "Okta.SWA.BulkAccess.Behavioral"
DisplayName: "Okta SWA Bulk Access, New Source, and Credential Extraction - Behavioral"
Enabled: true
ScheduledQueries:
  - Query.Okta.SWABulkAccessBehavioral
Severity: High  # Default, dynamic severity in rule function
Status: Experimental
Tags:
  - Identity & Access Management
  - Okta
  - SWA
  - Credential Access:Credentials from Password Stores
  - Collection:Data from Information Repositories
  - Initial Access:Valid Accounts
  - Anomaly Detection
Reports:
  MITRE ATT&CK:
    - TA0006:T1555  # Credentials from Password Stores
    - TA0009:T1213  # Data from Information Repositories
    - TA0001:T1078  # Valid Accounts
Description: |
  Detects Okta SWA (Secure Web Authentication) bulk credential extraction, abuse, and access from
  previously unseen IP addresses or user agents using behavioral z-score and source novelty analysis.

  SWA apps store credentials in Okta's encrypted vault. Admin accounts with SWA access can view or
  rotate credentials for users across many apps. This detection builds a 90-day behavioral baseline
  for each admin's SWA access, credential change patterns, and known source IPs/user agents, then
  identifies anomalous spikes or new sources in the last 7 days.

  **Detection Logic:**
  - Z-score: SWA authentication volume spike (> 3σ above baseline)
  - Z-score: Unique SWA app diversity spike (many different apps accessed in one hour) (> 3σ)
  - Z-score: Credential extraction volume spike (> 3σ)
  - Z-score: Victim diversity spike (credential changes across many users) (> 2σ)
  - Cold-start: First-time bulk SWA access (>= 10 events, no prior baseline)
  - Cold-start: First-time credential extraction (>= 5 extractions, no prior baseline)
  - New source: SWA access from IP address not seen in 90-day baseline
  - New source: SWA access from user agent not seen in 90-day baseline
  - Critical compound: New IP + any credential extraction events

  **Why This Matters:**
  SWA credential extraction is a powerful lateral movement technique. An attacker with admin access
  can silently retrieve plaintext credentials for hundreds of SWA-protected applications without
  triggering MFA or generating obvious authentication failures. New source detection catches the
  initial access phase when a compromised admin account is used from an unfamiliar device or location.

  **Complementary Detection:**
  Use alongside `Okta.SWA.OffHoursAccess.Behavioral` which detects the same attack vector
  occurring outside normal business hours.

Reference: https://www.varonis.com/blog/okta-attack-vectors
Runbook: |
  1. Review recent_total_extractions and recent_max_victim_diversity_per_hour for admin_email against baseline_total_extractions - query Okta SystemLog for application.user_membership.change_username events by admin_email in the 24 hours around recent_extraction_first_event, listing all target users and app names affected
  2. If has_new_ip is true, review new_ip_extraction_count and new_ip_victim_count - query Okta SystemLog for all events by admin_email in the 7 days preceding the alert to identify the specific new IPs used; verify whether these IPs belong to known corporate VPN ranges, cloud egress IPs, or the admin's registered home network
  3. Check recent_max_app_diversity_per_hour and z_score_app_diversity for user.authentication.sso events by admin_email in the 7 days around recent_swa_first_event - compare the set of apps accessed against the admin's baseline_mean_app_diversity_per_hour to identify apps outside their normal scope
  4. Search for concurrent alerts from admin_email in the 48 hours before recent_swa_first_event, including Okta.SkeletonKeyBypass.Behavioral and any failed MFA events, to determine whether the admin account was compromised prior to the bulk access activity

DedupPeriodMinutes: 1440  # 24 hours
SummaryAttributes:
  - admin_email
  - anomaly_severity_score
  - recent_total_extractions

Stages and Predicates

Fires when the condition below holds.

Condition

  • admin_email is present
Alert deduplication
repeat matches within 1d group into one alert

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
admin_emailis_not_null
  • (no value, null check)
field:"admin_email" kind:is_not_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
admin_email
recent_total_extractions
recent_max_victim_diversity_per_hour
recent_total_swa_events
recent_max_app_diversity_per_hour
z_score_extraction_volume
z_score_victim_diversity
z_score_swa_volume
has_new_ip
has_new_user_agent
new_ip_count
new_ip_extraction_count
new_ip_victim_count
anomaly_severity_score
is_first_time_credential_extraction
recent_extraction_first_event
recent_extraction_last_event

Response runbook

1. Review recent_total_extractions and recent_max_victim_diversity_per_hour for admin_email against baseline_total_extractions - query Okta SystemLog for application.user_membership.change_username events by admin_email in the 24 hours around recent_extraction_first_event, listing all target users and app names affected

2. If has_new_ip is true, review new_ip_extraction_count and new_ip_victim_count - query Okta SystemLog for all events by admin_email in the 7 days preceding the alert to identify the specific new IPs used; verify whether these IPs belong to known corporate VPN ranges, cloud egress IPs, or the admin's registered home network

3. Check recent_max_app_diversity_per_hour and z_score_app_diversity for user.authentication.sso events by admin_email in the 7 days around recent_swa_first_event - compare the set of apps accessed against the admin's baseline_mean_app_diversity_per_hour to identify apps outside their normal scope

4. Search for concurrent alerts from admin_email in the 48 hours before recent_swa_first_event, including Okta.SkeletonKeyBypass.Behavioral and any failed MFA events, to determine whether the admin account was compromised prior to the bulk access activity

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "admin_email": "attacker@company.com",
  "anomaly_severity_score": 159.0,
  "baseline_total_extractions": 0,
  "baseline_total_swa_events": 0,
  "has_new_ip": false,
  "has_new_user_agent": false,
  "is_anomalous": true,
  "is_extraction_volume_anomaly": false,
  "is_first_time_bulk_swa_access": false,
  "is_first_time_credential_extraction": true,
  "is_swa_volume_anomaly": false,
  "is_victim_diversity_anomaly": false,
  "new_ip_count": 0,
  "new_ip_extraction_count": 0,
  "new_ip_victim_count": 0,
  "recent_extraction_first_event": "2024-01-15 03:00:00",
  "recent_extraction_last_event": "2024-01-15 04:00:00",
  "recent_max_app_diversity_per_hour": 6,
  "recent_max_extractions_per_hour": 8,
  "recent_max_swa_events_per_hour": 8,
  "recent_max_victim_diversity_per_hour": 7,
  "recent_total_extractions": 12,
  "recent_total_swa_events": 8,
  "z_score_extraction_volume": null,
  "z_score_swa_volume": null,
  "z_score_victim_diversity": null
}

Okta SWA Off-Hours Credential Access - Behavioral

#
Status
Experimental
Severity
high
Tags
Identity & Access Management, Okta, SWA, Credential Access:Credentials from Password Stores, Initial Access:Valid Accounts, Anomaly Detection
Reference
www.varonis.com
Source
github.com/panther-labs/panther-analysis

Detects Okta SWA credential access occurring outside normal business hours using behavioral z-score analysis on temporal patterns. Compromised admin accounts often access SWA credentials at unusual times - late at night, during weekends, or from a different geographic location than normal. This detection builds a 90-day baseline for each admin's temporal credential access patterns, then identifies anomalous shifts toward off-hours, late-night, and weekend activity in the last 7 days. Detection Logic: - Z-score: Off-hours ratio spike (> 3σ above normal off-hours proportion) - Z-score: Late-night ratio spike (2 AM - 6 AM accesses) (> 2σ) - Z-score: Weekend ratio spike (> 2σ) - Cold-start: First-time off-hours credential access (>= 3 events, no prior baseline) - Cold-start: First-time late-night access (>= 2 events, no prior baseline) - Cold-start: First-time weekend access (>= 2 events, no prior baseline) - Compound: Geographic shift + off-hours activity (high-confidence indicator) Why This Matters: Attackers using stolen admin credentials typically operate at off-hours to avoid detection and minimize interference with active users. A sudden shift in the time distribution of SWA credential accesses is a strong indicator of account compromise. Complementary Detection: Use alongside Okta.SWA.BulkAccess.Behavioral which detects the same attack vector based on volume rather than temporal patterns.

MITRE ATT&CK coverage

TacticTechniques
Initial Access
Credential Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

def rule(event):
    # Query already filtered for is_anomalous = TRUE.
    # Guard against malformed rows missing the primary key field.
    return bool(event.get("admin_email"))


def title(event):
    admin = event.get("admin_email", "Unknown")
    is_geo_shift = event.get("is_geographic_shift") or False
    is_cold_start = event.get("is_cold_start") or False
    is_inactive_hour = event.get("is_inactive_hour_anomaly") or False
    baseline_country = event.get("baseline_primary_country", "Unknown")
    recent_country = event.get("recent_primary_country", "Unknown")
    if is_geo_shift and is_inactive_hour:
        return (
            f"Okta SWA: Off-Hours Credential Access with Geographic Shift for {admin}"
            f" ({baseline_country} -> {recent_country})"
        )
    if is_cold_start:
        return f"Okta SWA: Credential Access with No Baseline (Cold Start) for {admin}"
    if is_geo_shift:
        return (
            f"Okta SWA: Geographic Shift in Credential Access for {admin}"
            f" ({baseline_country} -> {recent_country})"
        )
    return f"Okta SWA: Off-Hours Credential Access Anomaly for {admin}"


def severity(event):
    is_geo_shift = event.get("is_geographic_shift") or False
    is_inactive_hour = event.get("is_inactive_hour_anomaly") or False
    is_cold_start = event.get("is_cold_start") or False
    z_score = event.get("z_score_inactive_slot_ratio") or 0
    score = event.get("anomaly_severity_score") or 0
    if is_geo_shift and is_inactive_hour:
        return "CRITICAL"
    if is_geo_shift or is_cold_start or z_score > 6 or score > 20:
        return "HIGH"
    if is_inactive_hour:
        return "MEDIUM"
    return "LOW"


def dedup_key(event):
    admin = event.get("admin_email", "unknown")
    first_event = str(event.get("recent_first_event", "unknown"))[:10]
    return f"okta_swa_offhours_{admin}_{first_event}"


def alert_context(event):
    return {
        "admin_email": event.get("admin_email"),
        "recent_total_credential_access": event.get("recent_total_credential_access"),
        "recent_inactive_slot_events": event.get("recent_inactive_slot_events"),
        "recent_inactive_slot_ratio": event.get("recent_inactive_slot_ratio"),
        "recent_avg_inactive_slot_ratio_per_hour": event.get(
            "recent_avg_inactive_slot_ratio_per_hour"
        ),
        "z_score_inactive_slot_ratio": event.get("z_score_inactive_slot_ratio"),
        "baseline_active_slot_count": event.get("baseline_active_slot_count"),
        "is_inactive_hour_anomaly": event.get("is_inactive_hour_anomaly"),
        "is_cold_start": event.get("is_cold_start"),
        "is_geographic_shift": event.get("is_geographic_shift"),
        "baseline_primary_country": event.get("baseline_primary_country"),
        "recent_primary_country": event.get("recent_primary_country"),
        "anomaly_severity_score": event.get("anomaly_severity_score"),
        "recent_first_event": event.get("recent_first_event"),
        "recent_last_event": event.get("recent_last_event"),
    }

Rule specification

AnalysisType: scheduled_rule
Filename: okta_swa_offhours_access_behavioral.py
RuleID: "Okta.SWA.OffHoursAccess.Behavioral"
DisplayName: "Okta SWA Off-Hours Credential Access - Behavioral"
Enabled: true
ScheduledQueries:
  - Query.Okta.SWAOffHoursAccessBehavioral
Severity: High  # Default, dynamic severity in rule function
Status: Experimental
Tags:
  - Identity & Access Management
  - Okta
  - SWA
  - Credential Access:Credentials from Password Stores
  - Initial Access:Valid Accounts
  - Anomaly Detection
Reports:
  MITRE ATT&CK:
    - TA0006:T1555  # Credentials from Password Stores
    - TA0001:T1078  # Valid Accounts
Description: |
  Detects Okta SWA credential access occurring outside normal business hours using behavioral
  z-score analysis on temporal patterns.

  Compromised admin accounts often access SWA credentials at unusual times - late at night,
  during weekends, or from a different geographic location than normal. This detection builds
  a 90-day baseline for each admin's temporal credential access patterns, then identifies
  anomalous shifts toward off-hours, late-night, and weekend activity in the last 7 days.

  **Detection Logic:**
  - Z-score: Off-hours ratio spike (> 3σ above normal off-hours proportion)
  - Z-score: Late-night ratio spike (2 AM - 6 AM accesses) (> 2σ)
  - Z-score: Weekend ratio spike (> 2σ)
  - Cold-start: First-time off-hours credential access (>= 3 events, no prior baseline)
  - Cold-start: First-time late-night access (>= 2 events, no prior baseline)
  - Cold-start: First-time weekend access (>= 2 events, no prior baseline)
  - Compound: Geographic shift + off-hours activity (high-confidence indicator)

  **Why This Matters:**
  Attackers using stolen admin credentials typically operate at off-hours to avoid detection
  and minimize interference with active users. A sudden shift in the time distribution of
  SWA credential accesses is a strong indicator of account compromise.

  **Complementary Detection:**
  Use alongside `Okta.SWA.BulkAccess.Behavioral` which detects the same attack vector
  based on volume rather than temporal patterns.

Reference: https://www.varonis.com/blog/okta-attack-vectors
Runbook: |
  1. Compare recent_late_night_ratio and recent_offhours_ratio for admin_email against baseline_late_night_ratio and baseline_offhours_ratio - query Okta SystemLog for application.user_membership.change_username events by admin_email in the 7 days around recent_first_event, listing UTC timestamps and app names for all off-hours accesses
  2. Check is_geographic_shift by comparing baseline_primary_country against recent_primary_country for admin_email - review client IP addresses and geolocation data for recent accesses in the 24 hours around recent_first_event to confirm whether the location change is legitimate travel or suspicious
  3. Search for Okta.SWA.BulkAccess.Behavioral and any MFA bypass or session anomaly alerts for admin_email in the 48 hours before recent_first_event to determine whether this off-hours access is part of a broader credential theft campaign

DedupPeriodMinutes: 1440  # 24 hours
SummaryAttributes:
  - admin_email
  - anomaly_severity_score
  - is_geographic_shift

Stages and Predicates

Fires when the condition below holds.

Condition

  • admin_email is present
Alert deduplication
repeat matches within 1d group into one alert

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
admin_emailis_not_null
  • (no value, null check)
field:"admin_email" kind:is_not_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
admin_email
recent_total_credential_access
recent_inactive_slot_events
recent_inactive_slot_ratio
recent_avg_inactive_slot_ratio_per_hour
z_score_inactive_slot_ratio
baseline_active_slot_count
is_inactive_hour_anomaly
is_cold_start
is_geographic_shift
baseline_primary_country
recent_primary_country
anomaly_severity_score
recent_first_event
recent_last_event

Response runbook

1. Compare recent_late_night_ratio and recent_offhours_ratio for admin_email against baseline_late_night_ratio and baseline_offhours_ratio - query Okta SystemLog for application.user_membership.change_username events by admin_email in the 7 days around recent_first_event, listing UTC timestamps and app names for all off-hours accesses

2. Check is_geographic_shift by comparing baseline_primary_country against recent_primary_country for admin_email - review client IP addresses and geolocation data for recent accesses in the 24 hours around recent_first_event to confirm whether the location change is legitimate travel or suspicious

3. Search for Okta.SWA.BulkAccess.Behavioral and any MFA bypass or session anomaly alerts for admin_email in the 48 hours before recent_first_event to determine whether this off-hours access is part of a broader credential theft campaign

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "admin_email": "victim-admin@company.com",
  "anomaly_severity_score": 34.0,
  "baseline_active_days": 25,
  "baseline_active_slot_count": 45,
  "baseline_hours_with_activity": 60,
  "baseline_mean_credential_access_per_hour": 1.33,
  "baseline_primary_city": "San Francisco",
  "baseline_primary_country": "United States",
  "baseline_stddev_credential_access_per_hour": 0.5,
  "baseline_total_credential_access": 80,
  "is_anomalous": true,
  "is_cold_start": false,
  "is_geographic_shift": true,
  "is_inactive_hour_anomaly": true,
  "recent_active_days": 3,
  "recent_avg_inactive_slot_ratio_per_hour": 0.75,
  "recent_avg_per_hour": 2.5,
  "recent_city_diversity": 1,
  "recent_country_diversity": 1,
  "recent_first_event": "2024-01-15 02:00:00",
  "recent_inactive_slot_events": 16,
  "recent_inactive_slot_ratio": 0.8,
  "recent_last_event": "2024-01-15 05:00:00",
  "recent_max_per_hour": 5,
  "recent_primary_city": "Moscow",
  "recent_primary_country": "Russia",
  "recent_total_credential_access": 20,
  "z_score_inactive_slot_ratio": 7.0
}

Okta ThreatInsight Security Threat Detected

#
Severity
high
Log types
Okta.SystemLog
Reference
help.okta.com
Source
github.com/panther-labs/panther-analysis

Okta ThreatInsight identified request from potentially malicious IP address

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_okta_helpers import okta_alert_context


def severity_from_threat_string(threat_detection):
    # threat detection is a string but contains json data
    # can contain multiple threats detected with multiple severities
    # return highest found severity
    if "CRITICAL" in threat_detection:
        return "CRITICAL"
    if "HIGH" in threat_detection:
        return "HIGH"
    if "MEDIUM" in threat_detection:
        return "MEDIUM"
    if "LOW" in threat_detection:
        return "LOW"
    if "INFO" in threat_detection:
        return "INFO"
    return "MEDIUM"


def rule(event):
    return event.get("eventtype") == "security.threat.detected"


def title(event):
    return (
        "Okta: ThreatInsight identified potentially malicious behavior"
        f" for [{event.get('actor',{}).get('displayName', '<display-name-not-found>')}]"
    )


def severity(event):
    outcome = event.deep_get("outcome", "result", default="<OUTCOME_NOT_FOUND>")
    if outcome == "DENY":
        return "INFO"
    threat_detection = (
        event.get("debugcontext", {})
        .get("debugData", {})
        .get("threatDetections", "<threat-detection-not-found>")
    )
    return severity_from_threat_string(threat_detection)


def alert_context(event):
    return okta_alert_context(event)

Rule specification

AnalysisType: rule
Description: Okta ThreatInsight identified request from potentially malicious IP address
Reference: https://help.okta.com/en-us/Content/Topics/Security/threat-insight/configure-threatinsight-system-log.htm
DisplayName: "Okta ThreatInsight Security Threat Detected"
Enabled: true
Filename: okta_threatinsight_security_threat_detected.py
Severity: High
DedupPeriodMinutes: 60
LogTypes:
  - Okta.SystemLog
RuleID: "Okta.ThreatInsight.Security.Threat.Detected"
Threshold: 1

Stages and Predicates

Fires on Okta.SystemLog events when the condition below holds.

Condition

  • eventtype is security.threat.detected

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses
displayNameactor.displayName

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "unknown",
    "displayName": "1.2.3.4",
    "id": "1.2.3.4",
    "type": "IP address"
  },
  "authenticationcontext": {
    "authenticationStep": 0
  },
  "client": {
    "device": "Computer",
    "geographicalContext": {
      "city": "Dallas",
      "country": "United States",
      "geolocation": {
        "lat": 32.7908,
        "lon": -96.8336
      },
      "postalCode": "75207",
      "state": "Texas"
    },
    "ipAddress": "1.2.3.4",
    "userAgent": {
      "browser": "CHROME",
      "os": "Windows 10",
      "rawUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
    },
    "zone": "null"
  },
  "debugcontext": {
    "debugData": {
      "dtHash": "abcsadfjsald",
      "requestId": "alsdjflasf",
      "requestUri": "/oauth2/v1/authorize",
      "threatDetections": "{\"Login failures with high unknown users count\":\"HIGH\",\"Password Spray\":\"HIGH\",\"Login Failures\":\"MEDIUM\"}",
      "threatSuspected": "true",
      "url": "/oauth2/v1/authorize"
    }
  },
  "displaymessage": "Request from suspicious actor",
  "eventtype": "security.threat.detected",
  "legacyeventtype": "security.threat.detected",
  "outcome": {
    "reason": "Password Spray, Login failures with high unknown users count, Login Failures",
    "result": "DENY"
  },
  "published": "2022-12-14 19:16:32.015",
  "request": {
    "ipChain": [
      {
        "geographicalContext": {
          "city": "Dallas",
          "country": "United States",
          "geolocation": {
            "lat": 32.7908,
            "lon": -96.8336
          },
          "ip": "1.2.3.4",
          "postalCode": "75207",
          "state": "Texas",
          "version": "V4"
        }
      }
    ]
  },
  "securitycontext": {
    "asNumber": 62240,
    "asOrg": "packethub s.a.",
    "domain": ".",
    "isProxy": false,
    "isp": "clouvider limited"
  },
  "severity": "WARN",
  "transaction": {
    "detail": {},
    "id": "asdfjaslf",
    "type": "WEB"
  },
  "uuid": "asdfa-1234-asdfdas",
  "version": "0"
}

Okta User Account Locked

#
Severity
low
Log types
Okta.SystemLog
Reference
support.okta.com
Source
github.com/panther-labs/panther-analysis

An Okta user has locked their account.

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_okta_helpers import okta_alert_context


def rule(event):
    return event.get("eventtype") in ("user.account.lock", "user.account.lock.limit")


def title(event):
    return (
        f"Okta: [{event.get('actor', {}).get('alternateId', '<id-not-found>')}] "
        f"[{event.get('displaymessage', 'account has been locked.')}]"
    )


def alert_context(event):
    return okta_alert_context(event)

Rule specification

AnalysisType: rule
Description: An Okta user has locked their account.
DisplayName: "Okta User Account Locked"
Enabled: true
Filename: okta_user_account_locked.py
Reference: https://support.okta.com/help/s/article/How-to-Configure-the-Number-of-Failed-Login-Attempts-Before-User-Lockout?language=en_US
Severity: Low
DedupPeriodMinutes: 60
LogTypes:
  - Okta.SystemLog
RuleID: "Okta.User.Account.Locked"
Threshold: 1

Stages and Predicates

Fires on Okta.SystemLog events when the condition below holds.

Condition

  • eventtype is one of user.account.lock, user.account.lock.limit

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses
alternateIdactor.alternateId
displaymessage

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "homer.simpson@duff.com",
    "displayName": "Homer Simpson",
    "id": "00abc123",
    "type": "User"
  },
  "authenticationcontext": {
    "authenticationStep": 0,
    "externalSessionId": "abcd-1234"
  },
  "client": {
    "device": "Computer",
    "geographicalContext": {
      "city": "Atlanta",
      "country": "United States",
      "geolocation": {
        "lat": 33,
        "lon": -80
      },
      "postalCode": "30318",
      "state": "Georgia"
    },
    "ipAddress": "1.2.3.4",
    "userAgent": {
      "browser": "CHROME",
      "os": "Mac OS X",
      "rawUserAgent": "Chrome"
    },
    "zone": "null"
  },
  "debugcontext": {
    "debugData": {
      "deviceFingerprint": "abc1234",
      "dtHash": "000abc",
      "requestId": "abc1111",
      "requestUri": "/idp/idx/identify",
      "threatSuspected": "false",
      "url": "/idp/idx/identify?"
    }
  },
  "displaymessage": "Account Locked from New Devices - Max sign-in attempts exceeded.",
  "eventtype": "user.account.lock",
  "legacyeventtype": "core.user_auth.account_locked",
  "outcome": {
    "reason": "LOCKED_OUT",
    "result": "FAILURE"
  },
  "published": "2022-11-22 18:48:49.177",
  "request": {
    "ipChain": [
      {
        "geographicalContext": {
          "city": "Atlanta",
          "country": "United States",
          "geolocation": {
            "lat": 33,
            "lon": -80
          },
          "postalCode": "30318",
          "state": "Georgia"
        },
        "ip": "1.2.3.4",
        "version": "V4"
      }
    ]
  },
  "securitycontext": {
    "asNumber": 7018,
    "asOrg": "at&t corp.",
    "domain": "sbcglobal.net",
    "isProxy": false,
    "isp": "att services inc"
  },
  "severity": "DEBUG",
  "transaction": {
    "detail": {},
    "id": "12345aaa",
    "type": "WEB"
  },
  "uuid": "aa-bb-cc-11",
  "version": "0"
}

Okta User MFA Factor Suspend

#
Severity
high
Log types
Okta.SystemLog
Reference
help.okta.com
Source
github.com/panther-labs/panther-analysis

Suspend factor or authenticator enrollment method for user.

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_okta_helpers import okta_alert_context


def rule(event):
    return (
        event.get("eventtype") == "user.mfa.factor.suspend"
        and event.deep_get("outcome", "result") == "SUCCESS"
    )


def title(event):
    return (
        "Okta: Authentication Factor for "
        f"[{event.get('target',[{}])[0].get('alternateId', '<id-not-found>')}] "
        f"has been suspended."
    )


def alert_context(event):
    return okta_alert_context(event)

Rule specification

AnalysisType: rule
Description: Suspend factor or authenticator enrollment method for user.
DisplayName: "Okta User MFA Factor Suspend"
Enabled: true
Filename: okta_user_mfa_factor_suspend.py
Reference: https://help.okta.com/en-us/content/topics/security/mfa/mfa-factors.htm
Severity: High
DedupPeriodMinutes: 60
LogTypes:
  - Okta.SystemLog
RuleID: "Okta.User.MFA.Factor.Suspend"
Threshold: 1

Stages and Predicates

Fires on Okta.SystemLog events when all of the conditions below hold.

Condition

  • eventtype is user.mfa.factor.suspend
  • outcome.result is SUCCESS

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "homer.simpson@duff.com",
    "displayName": "Homer Simpson",
    "id": "00abc123",
    "type": "User"
  },
  "authenticationcontext": {
    "authenticationStep": 0,
    "externalSessionId": "100-abc-9999"
  },
  "client": {
    "device": "Computer",
    "geographicalContext": {
      "city": "Springfield",
      "country": "United States",
      "geolocation": {
        "lat": 20,
        "lon": -25
      },
      "postalCode": "12345",
      "state": "Ohio"
    },
    "ipAddress": "1.3.2.4",
    "userAgent": {
      "browser": "CHROME",
      "os": "Mac OS X",
      "rawUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36"
    },
    "zone": "null"
  },
  "debugcontext": {
    "debugData": {
      "requestId": "AbCdEf12G",
      "requestUri": "/api/v1/users/AbCdEfG/lifecycle/reset_factors",
      "url": "/api/v1/users/AbCdEfG/lifecycle/reset_factors?"
    }
  },
  "displaymessage": "Suspend factor for user",
  "eventtype": "user.mfa.factor.suspend",
  "outcome": {
    "reason": "User suspended SIGNED_NONCE factor",
    "result": "SUCCESS"
  },
  "published": "2022-06-22 18:18:29.015",
  "request": {
    "ipChain": [
      {
        "geographicalContext": {
          "city": "Springfield",
          "country": "United States",
          "geolocation": {
            "lat": 20,
            "lon": -25
          },
          "postalCode": "12345",
          "state": "Ohio"
        },
        "ip": "1.3.2.4",
        "version": "V4"
      }
    ]
  },
  "securitycontext": {
    "asNumber": 701,
    "asOrg": "verizon",
    "domain": "verizon.net",
    "isProxy": false,
    "isp": "verizon"
  },
  "severity": "INFO",
  "target": [
    {
      "alternateId": "peter.griffin@company.com",
      "displayName": "Peter Griffin",
      "id": "0002222AAAA",
      "type": "User"
    }
  ],
  "transaction": {
    "detail": {},
    "id": "ABcDeFgG",
    "type": "WEB"
  },
  "uuid": "AbC-123-XyZ",
  "version": "0"
}

Okta User MFA Own Reset

#
Severity
informational
Log types
Okta.SystemLog
Reference
support.okta.com
Source
github.com/panther-labs/panther-analysis

User has reset one of their own MFA factors

Detection logic

import panther_event_type_helpers as event_type
from panther_okta_helpers import okta_alert_context


def rule(event):
    return event.udm("event_type") == event_type.MFA_RESET


def title(event):
    try:
        which_factor = event.get("outcome", {}).get("reason", "").split()[2]
    except IndexError:
        which_factor = "<FACTOR_NOT_FOUND>"
    return (
        f"Okta: User reset their MFA factor [{which_factor}] "
        f"[{event.get('target',[{}])[0].get('alternateId', '<id-not-found>')}] "
        f"by [{event.get('actor',{}).get('alternateId','<id-not-found>')}]"
    )


def alert_context(event):
    return okta_alert_context(event)

Rule specification

AnalysisType: rule
Description: "User has reset one of their own MFA factors"
DisplayName: "Okta User MFA Own Reset"
RuleID: "Okta.User.MFA.Reset.Single"
Enabled: true
Filename: okta_user_mfa_reset.py
Reference: https://support.okta.com/help/s/article/How-to-avoid-lockouts-and-reset-your-Multifactor-Authentication-MFA-for-Okta-Admins?language=en_US
Severity: Info
DedupPeriodMinutes: 60
LogTypes:
  - Okta.SystemLog
Threshold: 1

Stages and Predicates

Fires on Okta.SystemLog events when the condition below holds.

Condition

  • event_type is mfa_reset

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses
alternateIdactor.alternateId

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "homer@springfield.gov",
    "displayName": "Homer Simpson",
    "id": "11111111111",
    "type": "User"
  },
  "authenticationcontext": {
    "authenticationStep": 0,
    "externalSessionId": "1111111"
  },
  "client": {
    "device": "Computer",
    "ipAddress": "1.1.1.1",
    "userAgent": {
      "browser": "CHROME",
      "os": "Mac OS X",
      "rawUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36"
    },
    "zone": "null"
  },
  "displaymessage": "Reset factor for user",
  "eventtype": "user.mfa.factor.deactivate",
  "outcome": {
    "reason": "User reset FIDO_WEBAUTHN factor",
    "result": "SUCCESS"
  },
  "p_log_type": "Okta.SystemLog",
  "severity": "INFO",
  "target": [
    {
      "alternateId": "homer@springfield.gov",
      "displayName": "Homer Simpson",
      "id": "1111111",
      "type": "User"
    }
  ],
  "version": "0"
}

Okta User MFA Reset All

#
Severity
low
Log types
Okta.SystemLog
Reference
help.okta.com
Source
github.com/panther-labs/panther-analysis

All MFA factors have been reset for a user.

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_okta_helpers import okta_alert_context


def rule(event):
    return event.get("eventtype") == "user.mfa.factor.reset_all"


def title(event):
    return (
        "Okta: All MFA factors were reset for "
        f"[{event.get('target',[{}])[0].get('alternateId', '<id-not-found>')}] "
        f"by [{event.get('actor',{}).get('alternateId','<id-not-found>')}]"
    )


def alert_context(event):
    return okta_alert_context(event)

Rule specification

AnalysisType: rule
Description: "All MFA factors have been reset for a user."
DisplayName: "Okta User MFA Reset All"
Enabled: true
Filename: okta_user_mfa_reset_all.py
Reference: https://help.okta.com/en-us/content/topics/security/mfa/mfa-reset-users.htm#:~:text=the%20Admin%20Console%3A-,In%20the%20Admin%20Console%2C%20go%20to%20DirectoryPeople.,Selected%20Factors%20or%20Reset%20All
Severity: Low
DedupPeriodMinutes: 60
LogTypes:
  - Okta.SystemLog
RuleID: "Okta.User.MFA.Reset.All"
Threshold: 1

Stages and Predicates

Fires on Okta.SystemLog events when the condition below holds.

Condition

  • eventtype is user.mfa.factor.reset_all

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses
alternateIdactor.alternateId

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "homer.simpson@duff.com",
    "displayName": "Homer Simpson",
    "id": "00abc123",
    "type": "User"
  },
  "authenticationcontext": {
    "authenticationStep": 0,
    "externalSessionId": "100-abc-9999"
  },
  "client": {
    "device": "Computer",
    "geographicalContext": {
      "city": "Springfield",
      "country": "United States",
      "geolocation": {
        "lat": 20,
        "lon": -25
      },
      "postalCode": "12345",
      "state": "Ohio"
    },
    "ipAddress": "1.3.2.4",
    "userAgent": {
      "browser": "CHROME",
      "os": "Mac OS X",
      "rawUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36"
    },
    "zone": "null"
  },
  "debugcontext": {
    "debugData": {
      "requestId": "AbCdEf12G",
      "requestUri": "/api/v1/users/AbCdEfG/lifecycle/reset_factors",
      "url": "/api/v1/users/AbCdEfG/lifecycle/reset_factors?"
    }
  },
  "displaymessage": "Reset all factors for user",
  "eventtype": "user.mfa.factor.reset_all",
  "legacyeventtype": "core.user.factor.reset_all",
  "outcome": {
    "result": "SUCCESS"
  },
  "published": "2022-06-22 18:18:29.015",
  "request": {
    "ipChain": [
      {
        "geographicalContext": {
          "city": "Springfield",
          "country": "United States",
          "geolocation": {
            "lat": 20,
            "lon": -25
          },
          "postalCode": "12345",
          "state": "Ohio"
        },
        "ip": "1.3.2.4",
        "version": "V4"
      }
    ]
  },
  "securitycontext": {
    "asNumber": 701,
    "asOrg": "verizon",
    "domain": "verizon.net",
    "isProxy": false,
    "isp": "verizon"
  },
  "severity": "INFO",
  "target": [
    {
      "alternateId": "peter.griffin@company.com",
      "displayName": "Peter Griffin",
      "id": "0002222AAAA",
      "type": "User"
    }
  ],
  "transaction": {
    "detail": {},
    "id": "ABcDeFgG",
    "type": "WEB"
  },
  "uuid": "AbC-123-XyZ",
  "version": "0"
}

Okta User Reported Suspicious Activity

#
Severity
high
Log types
Okta.SystemLog
Reference
help.okta.com
Source
github.com/panther-labs/panther-analysis

Suspicious Activity Reporting provides an end user with the option to report unrecognized activity from an account activity email notification. This detection alerts when a user marks the raised activity as suspicious.

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_okta_helpers import okta_alert_context


def rule(event):
    return event.get("eventtype") == "user.account.report_suspicious_activity_by_enduser"


def title(event):
    reported_event_type = (
        event.get("debugcontext", {})
        .get("debugData", {})
        .get("suspiciousActivityEventType", "<event-type-not-found>")
    )
    return (
        f"Okta: [{event.get('actor',{}).get('alternateId','<id-not-found>')}] "
        f"reported suspicious account activity [{reported_event_type}]."
    )


def alert_context(event):
    return okta_alert_context(event)

Rule specification

AnalysisType: rule
Description: |-
  Suspicious Activity Reporting provides an end user with the option to report unrecognized activity from an account activity email notification.
  This detection alerts when a user marks the raised activity as suspicious.
Reference: https://help.okta.com/en-us/Content/Topics/Security/suspicious-activity-reporting.htm
DisplayName: "Okta User Reported Suspicious Activity"
Enabled: true
Filename: okta_user_reported_suspicious_activity.py
Severity: High
DedupPeriodMinutes: 60
LogTypes:
  - Okta.SystemLog
RuleID: "Okta.User.Reported.Suspicious.Activity"
Threshold: 1

Stages and Predicates

Fires on Okta.SystemLog events when the condition below holds.

Condition

  • eventtype is user.account.report_suspicious_activity_by_enduser

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
event_typeeventtype
severity
actor
client
request
outcome
target
debug_contextdebugcontext
authentication_contextauthenticationcontext
security_contextsecuritycontext
ipsp_any_ip_addresses
alternateIdactor.alternateId
suspiciousActivityEventTypedebugcontext.debugData.suspiciousActivityEventType

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actor": {
    "alternateId": "homer.simpson@duff.com",
    "displayName": "Homer Simpson",
    "id": "00ABC123",
    "type": "User"
  },
  "authenticationcontext": {
    "authenticationStep": 0,
    "externalSessionId": "aaa1234"
  },
  "client": {
    "device": "Computer",
    "geographicalContext": {
      "city": "Springfield",
      "country": "United States",
      "geolocation": {
        "lat": 30,
        "lon": -55
      },
      "postalCode": "12345",
      "state": "Texas"
    },
    "ipAddress": "1.2.3.4",
    "userAgent": {
      "browser": "CHROME",
      "os": "Mac OS X",
      "rawUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
    },
    "zone": "null"
  },
  "debugcontext": {
    "debugData": {
      "dtHash": "ab123542525",
      "requestId": "A1S05S00000",
      "requestUri": "/api/internal/users/me/report-suspicious-activity",
      "suspiciousActivityBrowser": "SAFARI",
      "suspiciousActivityEventCity": "Springfield",
      "suspiciousActivityEventCountry": "United States",
      "suspiciousActivityEventId": "aaa-123-bbb",
      "suspiciousActivityEventIp": "9.8.7.6",
      "suspiciousActivityEventLatitude": "30.000",
      "suspiciousActivityEventLongitude": "-55.000",
      "suspiciousActivityEventState": "Texas",
      "suspiciousActivityEventTransactionId": "ABC12345",
      "suspiciousActivityEventType": "system.email.new_device_notification.sent_message",
      "suspiciousActivityOs": "Mac OS X (iPhone)",
      "suspiciousActivityTimestamp": "2022-12-14T15:58:50.347Z",
      "url": "/api/internal/users/me/report-suspicious-activity?i=aaaaaa"
    }
  },
  "displaymessage": "User report suspicious activity",
  "eventtype": "user.account.report_suspicious_activity_by_enduser",
  "legacyeventtype": "core.user.account.report_suspicious_activity_by_enduser",
  "outcome": {
    "result": "SUCCESS"
  },
  "published": "2022-12-14 15:58:58.851",
  "request": {
    "ipChain": [
      {
        "geographicalContext": {
          "city": "Austin",
          "country": "United States",
          "geolocation": {
            "lat": 30,
            "lon": -55
          },
          "postalCode": "12345",
          "state": "Texas"
        },
        "ip": "9.8.7.6",
        "version": "V4"
      }
    ]
  },
  "securitycontext": {
    "asNumber": 11427,
    "asOrg": "charter communications inc",
    "domain": "spectrum.com",
    "isProxy": false,
    "isp": "charter communications inc"
  },
  "severity": "WARN",
  "target": [
    {
      "alternateId": "homer.simpson@duff.com",
      "displayName": "Homer Simpson",
      "id": "01234",
      "type": "User"
    }
  ],
  "transaction": {
    "detail": {},
    "id": "1234ABC",
    "type": "WEB"
  },
  "uuid": "ABC1234",
  "version": "0"
}

Okta Username Above 52 Characters Security Advisory

#
Source
github.com/panther-labs/panther-analysis

On October 30, 2024, a vulnerability was internally identified in generating the cache key for AD/LDAP DelAuth. The Bcrypt algorithm was used to generate the cache key where we hash a combined string of userId + username + password. Under a specific set of conditions, listed below, this could allow users to authenticate by providing the username with the stored cache key of a previous successful authentication. Customers meeting the pre-conditions should investigate their Okta System Log for unexpected authentications from usernames greater than 52 characters between the period of July 23rd, 2024 to October 30th, 2024. https://trust.okta.com/security-advisories/okta-ad-ldap-delegated-authentication-username/

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule specification

AnalysisType: saved_query
QueryName: "Okta Username Above 52 Characters Security Advisory"
Description: >
  On October 30, 2024, a vulnerability was internally identified in generating the cache key for AD/LDAP DelAuth. The Bcrypt algorithm was used to generate the cache key where we hash a combined string of userId + username + password. Under a specific set of conditions, listed below, this could allow users to authenticate by providing the username with the stored cache key of a previous successful authentication.
  Customers meeting the pre-conditions should investigate their Okta System Log for unexpected authentications from usernames greater than 52 characters between the period of July 23rd, 2024 to October 30th, 2024.
  https://trust.okta.com/security-advisories/okta-ad-ldap-delegated-authentication-username/
SnowflakeQuery: |
  SELECT
    p_event_time as p_timeline,
    *
  FROM
    panther_logs.public.okta_systemlog
  WHERE
    p_occurs_between('2024-07-22 00:00:00Z','2024-11-01 00:00:00Z')
    AND actor:type = 'User'
    AND eventType = 'user.session.start'
    AND outcome:result = 'SUCCESS'
    AND LEN(actor:alternateId) >= 52
  ORDER by p_event_time ASC NULLS LAST
  LIMIT 100

DatabricksQuery: |
  SELECT
    p_event_time as p_timeline,
    *
  FROM
    panther_logs.okta_systemlog
  WHERE
    p_occurs_between('2024-07-22 00:00:00Z','2024-11-01 00:00:00Z')
    AND actor:type = 'User'
    AND eventType = 'user.session.start'
    AND outcome:result = 'SUCCESS'
    AND LENGTH(actor:alternateId) >= 52
  ORDER by p_event_time ASC NULLS LAST
  LIMIT 100

Stages and Predicates

Stage 1: source

Table
panther_logs.public.okta_systemlog

Stage 2: filter

  • actor:type is User
  • eventType is user.session.start
  • outcome:result is SUCCESS

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
p_timelinep_event_time
*

Query.Okta.ADAgentAuthZScoreAnomaly

#
Tags
Okta, Active Directory, Anomaly Detection, Statistical Analysis, Token Theft
Source
github.com/panther-labs/panther-analysis

Detects anomalous authentication patterns via Okta AD Agent using z-score statistical analysis. Reads behavioral baseline from lookup table and alerts when recent activity shows volume spikes combined with geographic/IP diversity anomalies - indicators of token theft and credential abuse. PREREQUISITE: Requires the baseline builder query to populate the lookup table first.

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule specification

AnalysisType: scheduled_query
QueryName: "Query.Okta.ADAgentAuthZScoreAnomaly"
Enabled: false
Description: |
  Detects anomalous authentication patterns via Okta AD Agent using z-score statistical analysis.
  Reads behavioral baseline from lookup table and alerts when recent activity shows volume spikes
  combined with geographic/IP diversity anomalies - indicators of token theft and credential abuse.

  PREREQUISITE: Requires the baseline builder query to populate the lookup table first.
SnowflakeQuery: |
  -- OKTA AD AGENT AUTHENTICATION ANOMALY DETECTION (Z-SCORE)
  -- Uses lookup table baseline for efficient anomaly detection
  -- Detects: Volume spike + Geographic diversity spike = Token being used from multiple locations

  WITH recent_activity AS (
      SELECT
          actor:alternateId::string AS user_email,
          published,
          outcome:result::string AS outcome,
          client:geographicalContext:country::string AS country,
          client:geographicalContext:city::string AS city,
          client:ipAddress::string AS ip_address,
          securityContext:asNumber::string AS asn,
          debugContext:debugData:deviceFingerprint::string AS device_fingerprint,
          DATE_TRUNC('hour', published) AS event_hour
      FROM panther_logs.public.okta_systemlog
      WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '7 days'
          AND actor:alternateId::string LIKE '%@%'
          AND eventType = 'user.authentication.auth_via_AD_agent'
          AND outcome:result = 'SUCCESS'
  ),

  -- Recent activity aggregated by hour
  recent_hourly AS (
      SELECT
          user_email,
          event_hour,
          COUNT(*) AS hourly_total_events,
          COUNT(DISTINCT ip_address) AS hourly_ip_diversity,
          COUNT(DISTINCT country) AS hourly_country_diversity,
          COUNT(DISTINCT city) AS hourly_city_diversity,
          COUNT(DISTINCT device_fingerprint) AS hourly_device_diversity,
          ARRAY_AGG(DISTINCT ip_address) AS sample_ips,
          ARRAY_AGG(DISTINCT country) AS sample_countries
      FROM recent_activity
      GROUP BY user_email, event_hour
  ),

  -- Recent activity summary statistics (no flatten here to avoid cartesian product)
  recent_stats_agg AS (
      SELECT
          user_email,
          SUM(hourly_total_events) AS recent_total_events,
          COUNT(DISTINCT DATE(event_hour)) AS recent_active_days,

          -- Max per hour (kept for analyst context, not used for alert gating)
          MAX(hourly_total_events) AS recent_max_events_per_hour,
          MAX(hourly_ip_diversity) AS recent_max_ip_diversity_per_hour,
          MAX(hourly_country_diversity) AS recent_max_country_diversity_per_hour,
          MAX(hourly_city_diversity) AS recent_max_city_diversity_per_hour,
          MAX(hourly_device_diversity) AS recent_max_device_diversity_per_hour,

          -- Average per hour (used for z-score gating)
          AVG(hourly_total_events)::FLOAT AS recent_avg_events_per_hour,
          AVG(hourly_ip_diversity)::FLOAT AS recent_avg_ip_diversity_per_hour,
          AVG(hourly_country_diversity)::FLOAT AS recent_avg_country_diversity_per_hour,
          AVG(hourly_city_diversity)::FLOAT AS recent_avg_city_diversity_per_hour,
          AVG(hourly_device_diversity)::FLOAT AS recent_avg_device_diversity_per_hour,

          -- Temporal context
          MIN(event_hour) AS first_anomaly_hour,
          MAX(event_hour) AS last_anomaly_hour

      FROM recent_hourly
      GROUP BY user_email
  ),

  -- Collect unique IPs across all hourly windows (separate flatten to avoid cartesian product)
  recent_ips AS (
      SELECT
          user_email,
          ARRAY_AGG(DISTINCT ip.value::string) AS all_recent_ips
      FROM recent_hourly
      CROSS JOIN LATERAL FLATTEN(input => sample_ips) AS ip
      GROUP BY user_email
  ),

  -- Collect unique countries across all hourly windows
  recent_countries AS (
      SELECT
          user_email,
          ARRAY_AGG(DISTINCT country.value::string) AS all_recent_countries
      FROM recent_hourly
      CROSS JOIN LATERAL FLATTEN(input => sample_countries) AS country
      GROUP BY user_email
  ),

  -- Join aggregates with IP/country collections
  recent_stats AS (
      SELECT
          a.*,
          i.all_recent_ips,
          c.all_recent_countries
      FROM recent_stats_agg a
      LEFT JOIN recent_ips i ON a.user_email = i.user_email
      LEFT JOIN recent_countries c ON a.user_email = c.user_email
  ),

  -- Join with baseline from lookup table and calculate z-scores
  anomaly_detection AS (
      SELECT
          r.user_email,

          -- Baseline metrics (from lookup table)
          b.baseline_total_events,
          b.baseline_active_days,
          b.baseline_mean_events_per_hour,
          b.baseline_stddev_events_per_hour,
          b.baseline_mean_ip_diversity_per_hour,
          b.baseline_stddev_ip_diversity_per_hour,
          b.baseline_mean_country_diversity_per_hour,
          b.baseline_stddev_country_diversity_per_hour,
          b.baseline_mean_city_diversity_per_hour,
          b.baseline_stddev_city_diversity_per_hour,
          b.baseline_mean_device_diversity_per_hour,
          b.baseline_stddev_device_diversity_per_hour,
          b.primary_country,
          b.primary_city,
          b.primary_ip,
          b.baseline_updated_at,

          -- Recent activity metrics
          r.recent_total_events,
          r.recent_active_days,
          r.recent_max_events_per_hour,
          r.recent_max_ip_diversity_per_hour,
          r.recent_max_country_diversity_per_hour,
          r.recent_max_city_diversity_per_hour,
          r.recent_max_device_diversity_per_hour,
          r.recent_avg_events_per_hour,
          r.recent_avg_ip_diversity_per_hour,
          r.recent_avg_country_diversity_per_hour,
          r.recent_avg_city_diversity_per_hour,
          r.recent_avg_device_diversity_per_hour,
          r.all_recent_ips,
          r.all_recent_countries,
          r.first_anomaly_hour,
          r.last_anomaly_hour,

          -- Z-SCORES (avg-based: statistically valid comparison against baseline distribution)
          ROUND(
              (r.recent_avg_events_per_hour - b.baseline_mean_events_per_hour) /
              NULLIF(b.baseline_stddev_events_per_hour, 0),
              2
          ) AS z_score_volume,

          ROUND(
              (r.recent_avg_ip_diversity_per_hour - b.baseline_mean_ip_diversity_per_hour) /
              NULLIF(b.baseline_stddev_ip_diversity_per_hour, 0),
              2
          ) AS z_score_ip_diversity,

          ROUND(
              (r.recent_avg_country_diversity_per_hour - b.baseline_mean_country_diversity_per_hour) /
              NULLIF(b.baseline_stddev_country_diversity_per_hour, 0),
              2
          ) AS z_score_country_diversity,

          ROUND(
              (r.recent_avg_city_diversity_per_hour - b.baseline_mean_city_diversity_per_hour) /
              NULLIF(b.baseline_stddev_city_diversity_per_hour, 0),
              2
          ) AS z_score_city_diversity,

          ROUND(
              (r.recent_avg_device_diversity_per_hour - b.baseline_mean_device_diversity_per_hour) /
              NULLIF(b.baseline_stddev_device_diversity_per_hour, 0),
              2
          ) AS z_score_device_diversity,

          -- Anomaly severity score (sum of positive z-scores)
          ROUND(
              GREATEST(COALESCE((r.recent_avg_events_per_hour - b.baseline_mean_events_per_hour) / NULLIF(b.baseline_stddev_events_per_hour, 0), 0), 0) +
              GREATEST(COALESCE((r.recent_avg_ip_diversity_per_hour - b.baseline_mean_ip_diversity_per_hour) / NULLIF(b.baseline_stddev_ip_diversity_per_hour, 0), 0), 0) +
              GREATEST(COALESCE((r.recent_avg_country_diversity_per_hour - b.baseline_mean_country_diversity_per_hour) / NULLIF(b.baseline_stddev_country_diversity_per_hour, 0), 0), 0) +
              GREATEST(COALESCE((r.recent_avg_city_diversity_per_hour - b.baseline_mean_city_diversity_per_hour) / NULLIF(b.baseline_stddev_city_diversity_per_hour, 0), 0), 0) +
              GREATEST(COALESCE((r.recent_avg_device_diversity_per_hour - b.baseline_mean_device_diversity_per_hour) / NULLIF(b.baseline_stddev_device_diversity_per_hour, 0), 0), 0),
              2
          ) AS anomaly_severity_score,

          -- Multi-dimensional anomaly flag
          CASE
              WHEN b.user_email IS NULL THEN FALSE  -- No baseline yet; cold-start handled below
              WHEN (
                  -- Sustained volume spike (z > 3)
                  (r.recent_avg_events_per_hour - b.baseline_mean_events_per_hour) /
                      NULLIF(b.baseline_stddev_events_per_hour, 0) > 3
                  AND (
                      -- AND either IP diversity spike (z > 2) OR country diversity spike (z > 2)
                      (r.recent_avg_ip_diversity_per_hour - b.baseline_mean_ip_diversity_per_hour) /
                          NULLIF(b.baseline_stddev_ip_diversity_per_hour, 0) > 2
                      OR (r.recent_avg_country_diversity_per_hour - b.baseline_mean_country_diversity_per_hour) /
                          NULLIF(b.baseline_stddev_country_diversity_per_hour, 0) > 2
                  )
              ) THEN TRUE
              ELSE FALSE
          END AS is_anomalous,

          -- Cold-start: no baseline yet, but recent activity shows high IP or country diversity
          CASE
              WHEN b.user_email IS NULL
                  AND r.recent_max_ip_diversity_per_hour >= 3
              THEN TRUE
              ELSE FALSE
          END AS is_cold_start_anomaly,

          -- Detection metadata
          CURRENT_TIMESTAMP AS detection_timestamp

      FROM recent_stats r
      LEFT JOIN panther_lookups.public.okta_ad_baseline_90d b
          ON r.user_email = b.user_email
      WHERE r.recent_total_events >= 5  -- Minimum recent activity threshold
  )

  -- Final output: Only anomalous users
  SELECT
      user_email,

      -- Baseline context
      baseline_total_events,
      baseline_active_days,
      baseline_mean_events_per_hour,
      baseline_stddev_events_per_hour,
      baseline_mean_ip_diversity_per_hour,
      baseline_stddev_ip_diversity_per_hour,
      baseline_mean_country_diversity_per_hour,
      baseline_stddev_country_diversity_per_hour,
      baseline_mean_city_diversity_per_hour,
      baseline_stddev_city_diversity_per_hour,
      baseline_mean_device_diversity_per_hour,
      baseline_stddev_device_diversity_per_hour,
      primary_country,
      primary_city,
      primary_ip,
      baseline_updated_at,

      -- Recent activity context
      recent_total_events,
      recent_active_days,
      recent_max_events_per_hour,
      recent_max_ip_diversity_per_hour,
      recent_max_country_diversity_per_hour,
      recent_max_city_diversity_per_hour,
      recent_max_device_diversity_per_hour,
      recent_avg_events_per_hour,
      recent_avg_ip_diversity_per_hour,
      recent_avg_country_diversity_per_hour,
      recent_avg_city_diversity_per_hour,
      recent_avg_device_diversity_per_hour,
      all_recent_ips,
      all_recent_countries,
      first_anomaly_hour,
      last_anomaly_hour,

      -- Z-scores and anomaly metrics
      z_score_volume,
      z_score_ip_diversity,
      z_score_country_diversity,
      z_score_city_diversity,
      z_score_device_diversity,
      anomaly_severity_score,

      -- Detection metadata
      detection_timestamp,
      is_anomalous,
      is_cold_start_anomaly

  FROM anomaly_detection
  WHERE is_anomalous = TRUE OR is_cold_start_anomaly = TRUE
  ORDER BY anomaly_severity_score DESC
  LIMIT 100

DatabricksQuery: |
  -- OKTA AD AGENT AUTHENTICATION ANOMALY DETECTION (Z-SCORE)

  WITH recent_activity AS (
      SELECT
          actor:alternateId::string AS user_email,
          published,
          outcome:result::string AS outcome,
          client:geographicalContext:country::string AS country,
          client:geographicalContext:city::string AS city,
          client:ipAddress::string AS ip_address,
          securityContext:asNumber::string AS asn,
          debugContext:debugData:deviceFingerprint::string AS device_fingerprint,
          DATE_TRUNC('hour', published) AS event_hour
      FROM panther_logs.okta_systemlog
      WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '7 days'
          AND actor:alternateId::string LIKE '%@%'
          AND eventType = 'user.authentication.auth_via_AD_agent'
          AND outcome:result::string = 'SUCCESS'
  ),

  recent_hourly AS (
      SELECT
          user_email,
          event_hour,
          COUNT(*) AS hourly_total_events,
          COUNT(DISTINCT ip_address) AS hourly_ip_diversity,
          COUNT(DISTINCT country) AS hourly_country_diversity,
          COUNT(DISTINCT city) AS hourly_city_diversity,
          COUNT(DISTINCT device_fingerprint) AS hourly_device_diversity,
          COLLECT_SET(ip_address) AS sample_ips,
          COLLECT_SET(country) AS sample_countries
      FROM recent_activity
      GROUP BY user_email, event_hour
  ),

  recent_stats_agg AS (
      SELECT
          user_email,
          SUM(hourly_total_events) AS recent_total_events,
          COUNT(DISTINCT CAST(event_hour AS DATE)) AS recent_active_days,
          MAX(hourly_total_events) AS recent_max_events_per_hour,
          MAX(hourly_ip_diversity) AS recent_max_ip_diversity_per_hour,
          MAX(hourly_country_diversity) AS recent_max_country_diversity_per_hour,
          MAX(hourly_city_diversity) AS recent_max_city_diversity_per_hour,
          MAX(hourly_device_diversity) AS recent_max_device_diversity_per_hour,
          AVG(hourly_total_events) AS recent_avg_events_per_hour,
          AVG(hourly_ip_diversity) AS recent_avg_ip_diversity_per_hour,
          AVG(hourly_country_diversity) AS recent_avg_country_diversity_per_hour,
          AVG(hourly_city_diversity) AS recent_avg_city_diversity_per_hour,
          AVG(hourly_device_diversity) AS recent_avg_device_diversity_per_hour,
          MIN(event_hour) AS first_anomaly_hour,
          MAX(event_hour) AS last_anomaly_hour
      FROM recent_hourly
      GROUP BY user_email
  ),

  recent_ips AS (
      SELECT
          user_email,
          COLLECT_SET(ip_value) AS all_recent_ips
      FROM recent_hourly
      LATERAL VIEW OUTER EXPLODE(from_json(TO_JSON(sample_ips), 'ARRAY<STRING>')) exploded AS ip_value
      GROUP BY user_email
  ),

  recent_countries AS (
      SELECT
          user_email,
          COLLECT_SET(country_value) AS all_recent_countries
      FROM recent_hourly
      LATERAL VIEW OUTER EXPLODE(from_json(TO_JSON(sample_countries), 'ARRAY<STRING>')) exploded AS country_value
      GROUP BY user_email
  ),

  recent_stats AS (
      SELECT
          a.*,
          i.all_recent_ips,
          c.all_recent_countries
      FROM recent_stats_agg a
      LEFT JOIN recent_ips i ON a.user_email = i.user_email
      LEFT JOIN recent_countries c ON a.user_email = c.user_email
  ),

  anomaly_detection AS (
      SELECT
          r.user_email,
          b.baseline_total_events,
          b.baseline_active_days,
          b.baseline_mean_events_per_hour,
          b.baseline_stddev_events_per_hour,
          b.baseline_mean_ip_diversity_per_hour,
          b.baseline_stddev_ip_diversity_per_hour,
          b.baseline_mean_country_diversity_per_hour,
          b.baseline_stddev_country_diversity_per_hour,
          b.baseline_mean_city_diversity_per_hour,
          b.baseline_stddev_city_diversity_per_hour,
          b.baseline_mean_device_diversity_per_hour,
          b.baseline_stddev_device_diversity_per_hour,
          b.primary_country,
          b.primary_city,
          b.primary_ip,
          b.baseline_updated_at,
          r.recent_total_events,
          r.recent_active_days,
          r.recent_max_events_per_hour,
          r.recent_max_ip_diversity_per_hour,
          r.recent_max_country_diversity_per_hour,
          r.recent_max_city_diversity_per_hour,
          r.recent_max_device_diversity_per_hour,
          r.recent_avg_events_per_hour,
          r.recent_avg_ip_diversity_per_hour,
          r.recent_avg_country_diversity_per_hour,
          r.recent_avg_city_diversity_per_hour,
          r.recent_avg_device_diversity_per_hour,
          r.all_recent_ips,
          r.all_recent_countries,
          r.first_anomaly_hour,
          r.last_anomaly_hour,
          ROUND(
              (r.recent_avg_events_per_hour - b.baseline_mean_events_per_hour) /
              NULLIF(b.baseline_stddev_events_per_hour, 0),
              2
          ) AS z_score_volume,
          ROUND(
              (r.recent_avg_ip_diversity_per_hour - b.baseline_mean_ip_diversity_per_hour) /
              NULLIF(b.baseline_stddev_ip_diversity_per_hour, 0),
              2
          ) AS z_score_ip_diversity,
          ROUND(
              (r.recent_avg_country_diversity_per_hour - b.baseline_mean_country_diversity_per_hour) /
              NULLIF(b.baseline_stddev_country_diversity_per_hour, 0),
              2
          ) AS z_score_country_diversity,
          ROUND(
              (r.recent_avg_city_diversity_per_hour - b.baseline_mean_city_diversity_per_hour) /
              NULLIF(b.baseline_stddev_city_diversity_per_hour, 0),
              2
          ) AS z_score_city_diversity,
          ROUND(
              (r.recent_avg_device_diversity_per_hour - b.baseline_mean_device_diversity_per_hour) /
              NULLIF(b.baseline_stddev_device_diversity_per_hour, 0),
              2
          ) AS z_score_device_diversity,
          ROUND(
              GREATEST(COALESCE((r.recent_avg_events_per_hour - b.baseline_mean_events_per_hour) / NULLIF(b.baseline_stddev_events_per_hour, 0), 0), 0) +
              GREATEST(COALESCE((r.recent_avg_ip_diversity_per_hour - b.baseline_mean_ip_diversity_per_hour) / NULLIF(b.baseline_stddev_ip_diversity_per_hour, 0), 0), 0) +
              GREATEST(COALESCE((r.recent_avg_country_diversity_per_hour - b.baseline_mean_country_diversity_per_hour) / NULLIF(b.baseline_stddev_country_diversity_per_hour, 0), 0), 0) +
              GREATEST(COALESCE((r.recent_avg_city_diversity_per_hour - b.baseline_mean_city_diversity_per_hour) / NULLIF(b.baseline_stddev_city_diversity_per_hour, 0), 0), 0) +
              GREATEST(COALESCE((r.recent_avg_device_diversity_per_hour - b.baseline_mean_device_diversity_per_hour) / NULLIF(b.baseline_stddev_device_diversity_per_hour, 0), 0), 0),
              2
          ) AS anomaly_severity_score,
          CASE
              WHEN b.user_email IS NULL THEN FALSE
              WHEN (
                  (r.recent_avg_events_per_hour - b.baseline_mean_events_per_hour) /
                      NULLIF(b.baseline_stddev_events_per_hour, 0) > 3
                  AND (
                      (r.recent_avg_ip_diversity_per_hour - b.baseline_mean_ip_diversity_per_hour) /
                          NULLIF(b.baseline_stddev_ip_diversity_per_hour, 0) > 2
                      OR (r.recent_avg_country_diversity_per_hour - b.baseline_mean_country_diversity_per_hour) /
                          NULLIF(b.baseline_stddev_country_diversity_per_hour, 0) > 2
                  )
              ) THEN TRUE
              ELSE FALSE
          END AS is_anomalous,
          CASE
              WHEN b.user_email IS NULL
                  AND r.recent_max_ip_diversity_per_hour >= 3
              THEN TRUE
              ELSE FALSE
          END AS is_cold_start_anomaly,
          CURRENT_TIMESTAMP AS detection_timestamp

      FROM recent_stats r
      LEFT JOIN panther_lookups.okta_ad_baseline_90d b
          ON r.user_email = b.user_email
      WHERE r.recent_total_events >= 5
  )

  SELECT
      user_email,
      baseline_total_events,
      baseline_active_days,
      baseline_mean_events_per_hour,
      baseline_stddev_events_per_hour,
      baseline_mean_ip_diversity_per_hour,
      baseline_stddev_ip_diversity_per_hour,
      baseline_mean_country_diversity_per_hour,
      baseline_stddev_country_diversity_per_hour,
      baseline_mean_city_diversity_per_hour,
      baseline_stddev_city_diversity_per_hour,
      baseline_mean_device_diversity_per_hour,
      baseline_stddev_device_diversity_per_hour,
      primary_country,
      primary_city,
      primary_ip,
      baseline_updated_at,
      recent_total_events,
      recent_active_days,
      recent_max_events_per_hour,
      recent_max_ip_diversity_per_hour,
      recent_max_country_diversity_per_hour,
      recent_max_city_diversity_per_hour,
      recent_max_device_diversity_per_hour,
      recent_avg_events_per_hour,
      recent_avg_ip_diversity_per_hour,
      recent_avg_country_diversity_per_hour,
      recent_avg_city_diversity_per_hour,
      recent_avg_device_diversity_per_hour,
      all_recent_ips,
      all_recent_countries,
      first_anomaly_hour,
      last_anomaly_hour,
      z_score_volume,
      z_score_ip_diversity,
      z_score_country_diversity,
      z_score_city_diversity,
      z_score_device_diversity,
      anomaly_severity_score,
      detection_timestamp,
      is_anomalous,
      is_cold_start_anomaly

  FROM anomaly_detection
  WHERE is_anomalous = TRUE OR is_cold_start_anomaly = TRUE
  ORDER BY anomaly_severity_score DESC
  LIMIT 100

Schedule:
  RateMinutes: 360  # Run every 6 hours
  TimeoutMinutes: 10
Tags:
  - Okta
  - Active Directory
  - Anomaly Detection
  - Statistical Analysis
  - Token Theft

Stages and Predicates

Stage 1: source

Table
anomaly_detection

Stage 2: filter

  • any of:
    • is_anomalous is TRUE
    • is_cold_start_anomaly is TRUE

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
user_email
baseline_total_events
baseline_active_days
baseline_mean_events_per_hour
baseline_stddev_events_per_hour
baseline_mean_ip_diversity_per_hour
baseline_stddev_ip_diversity_per_hour
baseline_mean_country_diversity_per_hour
baseline_stddev_country_diversity_per_hour
baseline_mean_city_diversity_per_hour
baseline_stddev_city_diversity_per_hour
baseline_mean_device_diversity_per_hour
baseline_stddev_device_diversity_per_hour
primary_country
primary_city
primary_ip
baseline_updated_at
recent_total_events
recent_active_days
recent_max_events_per_hour
recent_max_ip_diversity_per_hour
recent_max_country_diversity_per_hour
recent_max_city_diversity_per_hour
recent_max_device_diversity_per_hour
recent_avg_events_per_hour
recent_avg_ip_diversity_per_hour
recent_avg_country_diversity_per_hour
recent_avg_city_diversity_per_hour
recent_avg_device_diversity_per_hour
all_recent_ips
all_recent_countries
first_anomaly_hour
last_anomaly_hour
z_score_volume
z_score_ip_diversity
z_score_country_diversity
z_score_city_diversity
z_score_device_diversity
anomaly_severity_score
detection_timestamp
is_anomalous
is_cold_start_anomaly

Query.Okta.ADAgentTokenAbuseBehavioral

#
Tags
Okta, Anomaly Detection, Behavioral Analytics
Source
github.com/panther-labs/panther-analysis

Detects API token creation and AD agent activity from previously unseen IP addresses or user agents. Uses behavioral analysis to identify anomalous token creation patterns.

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule specification

AnalysisType: scheduled_query
QueryName: "Query.Okta.ADAgentTokenAbuseBehavioral"
Enabled: false
Description: |
  Detects API token creation and AD agent activity from previously unseen IP addresses or user agents.
  Uses behavioral analysis to identify anomalous token creation patterns.
SnowflakeQuery: |
  -- Detects AD agent-related events from IP addresses or user agents not seen
  -- in the prior 29 days for the same actor (i.e., new in the last 24 hours).

  WITH all_events AS (
    SELECT
      p_event_time,
      actor:alternateId::string AS actorId,
      actor:displayName::string AS actorName,
      eventType::string AS eventType,
      client:ipAddress::string AS sourceIP,
      client:userAgent:rawUserAgent::string AS userAgent,
      outcome:result::string AS result,
      target
    FROM panther_logs.public.okta_systemlog
    WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '30 days'
      AND (
        eventType = 'system.api_token.create'
        OR eventType IN (
          'system.agent.ad.config_change_detected',
          'system.agent.ad.agent_instance_added'
        )
      )
      AND outcome:result = 'SUCCESS'
      AND actor:alternateId IS NOT NULL
  ),

  -- IPs seen per actor in the historical window (older than 1 day); NULLs excluded
  historical_ips AS (
    SELECT DISTINCT actorId, sourceIP
    FROM all_events
    WHERE p_event_time < CURRENT_TIMESTAMP - INTERVAL '1 day'
      AND sourceIP IS NOT NULL
  ),

  -- User agents seen per actor in the historical window; NULLs excluded
  historical_user_agents AS (
    SELECT DISTINCT actorId, userAgent
    FROM all_events
    WHERE p_event_time < CURRENT_TIMESTAMP - INTERVAL '1 day'
      AND userAgent IS NOT NULL
  ),

  -- Events from the last 1 day
  recent_events AS (
    SELECT *
    FROM all_events
    WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '1 day'
  )

  SELECT
    r.p_event_time,
    r.actorId,
    r.actorName,
    r.eventType,
    r.sourceIP,
    r.userAgent,
    r.result,
    r.target,
    ARRAY_TO_STRING(
      ARRAY_COMPACT(ARRAY_CONSTRUCT(
        CASE WHEN r.sourceIP IS NOT NULL AND NOT EXISTS (
          SELECT 1 FROM historical_ips h
          WHERE h.actorId = r.actorId AND h.sourceIP = r.sourceIP
        ) THEN 'New IP Address' END,
        CASE WHEN r.userAgent IS NOT NULL AND NOT EXISTS (
          SELECT 1 FROM historical_user_agents h
          WHERE h.actorId = r.actorId AND h.userAgent = r.userAgent
        ) THEN 'New User Agent' END
      )),
      ', '
    ) AS anomaly_type
  FROM recent_events r
  WHERE
    (r.sourceIP IS NOT NULL AND NOT EXISTS (
      SELECT 1 FROM historical_ips h
      WHERE h.actorId = r.actorId AND h.sourceIP = r.sourceIP
    ))
    OR (r.userAgent IS NOT NULL AND NOT EXISTS (
      SELECT 1 FROM historical_user_agents h
      WHERE h.actorId = r.actorId AND h.userAgent = r.userAgent
    ))
  ORDER BY p_event_time DESC
  LIMIT 100

DatabricksQuery: |
  -- Detects AD agent-related events from IP addresses or user agents not seen
  -- in the prior 29 days for the same actor (i.e., new in the last 24 hours).

  WITH all_events AS (
    SELECT
      p_event_time,
      actor:alternateId::string AS actorId,
      actor:displayName::string AS actorName,
      eventType AS eventType,
      client:ipAddress::string AS sourceIP,
      client:userAgent:rawUserAgent::string AS userAgent,
      outcome:result::string AS result,
      target
    FROM panther_logs.okta_systemlog
    WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '30 days'
      AND (
        eventType = 'system.api_token.create'
        OR eventType IN (
          'system.agent.ad.config_change_detected',
          'system.agent.ad.agent_instance_added'
        )
      )
      AND outcome:result::string = 'SUCCESS'
      AND actor:alternateId::string IS NOT NULL
  ),

  -- IPs seen per actor in the historical window (older than 1 day); NULLs excluded
  historical_ips AS (
    SELECT DISTINCT actorId, sourceIP
    FROM all_events
    WHERE p_event_time < CURRENT_TIMESTAMP - INTERVAL '1 day'
      AND sourceIP IS NOT NULL
  ),

  -- User agents seen per actor in the historical window; NULLs excluded
  historical_user_agents AS (
    SELECT DISTINCT actorId, userAgent
    FROM all_events
    WHERE p_event_time < CURRENT_TIMESTAMP - INTERVAL '1 day'
      AND userAgent IS NOT NULL
  ),

  -- Events from the last 1 day
  recent_events AS (
    SELECT *
    FROM all_events
    WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '1 day'
  )

  SELECT
    r.p_event_time,
    r.actorId,
    r.actorName,
    r.eventType,
    r.sourceIP,
    r.userAgent,
    r.result,
    r.target,
    ARRAY_JOIN(
      FILTER(ARRAY(
        CASE WHEN r.sourceIP IS NOT NULL AND NOT EXISTS (
          SELECT 1 FROM historical_ips h
          WHERE h.actorId = r.actorId AND h.sourceIP = r.sourceIP
        ) THEN 'New IP Address' END,
        CASE WHEN r.userAgent IS NOT NULL AND NOT EXISTS (
          SELECT 1 FROM historical_user_agents h
          WHERE h.actorId = r.actorId AND h.userAgent = r.userAgent
        ) THEN 'New User Agent' END
      ), x -> x IS NOT NULL),
      ', '
    ) AS anomaly_type
  FROM recent_events r
  WHERE
    (r.sourceIP IS NOT NULL AND NOT EXISTS (
      SELECT 1 FROM historical_ips h
      WHERE h.actorId = r.actorId AND h.sourceIP = r.sourceIP
    ))
    OR (r.userAgent IS NOT NULL AND NOT EXISTS (
      SELECT 1 FROM historical_user_agents h
      WHERE h.actorId = r.actorId AND h.userAgent = r.userAgent
    ))
  ORDER BY p_event_time DESC
  LIMIT 100
Schedule:
  RateMinutes: 60
  TimeoutMinutes: 5
Tags:
  - Okta
  - Anomaly Detection
  - Behavioral Analytics

Stages and Predicates

Stage 1: source

Table
recent_events

Stage 2: filter

  • any of:
    • r.sourceIP is present
    • r.userAgent is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
r.sourceIPis_not_null
  • (no value, null check)
field:"r.sourceIP" kind:is_not_null
r.userAgentis_not_null
  • (no value, null check)
field:"r.userAgent" kind:is_not_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
r.p_event_time
r.actorId
r.actorName
r.eventType
r.sourceIP
r.userAgent
r.result
r.target
anomaly_typeARRAY_TO_STRING ( ARRAY_COMPACT ( ARRAY_CONSTRUCT ( CASE WHEN r.sourceIP IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM historical_ips h WHERE h.actorId = r.actorId AND h.sourceIP = r.sourceIP ) THEN 'New IP Address' END , CASE WHEN r.userAgent IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM historical_user_agents h WHERE h.actorId = r.actorId AND h.userAgent = r.userAgent ) THEN 'New User Agent' END ) ) , ', ' )

Query.Okta.SkeletonKeyBypassBehavioral

#
Tags
Okta, Active Directory, Skeleton Key, Anomaly Detection, Statistical Analysis
Source
github.com/panther-labs/panther-analysis

Detects Okta authentication bypass attempts via skeleton key injection using behavioral z-score analysis. Reads pre-computed 90-day baselines from the okta_baseline_90d lookup table, then compares recent (last 7 days) admin policy change and MFA factor enrollment patterns against those baselines. DETECTION LOGIC: - Z-score: Security-weakening policy changes (requireFactor=false, maxSessionLifetime=0) > 2σ - Z-score: Admin-on-behalf-of MFA factor enrollments for other users > 3σ - Cold-start: First-time security weakening with no prior baseline - Cold-start: First-time admin-enrolled factors for other users PREREQUISITE: okta_baseline_90d lookup table must be populated.

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule specification

AnalysisType: scheduled_query
QueryName: "Query.Okta.SkeletonKeyBypassBehavioral"
Enabled: false
Description: |
  Detects Okta authentication bypass attempts via skeleton key injection using behavioral z-score analysis.
  Reads pre-computed 90-day baselines from the okta_baseline_90d lookup table, then compares recent
  (last 7 days) admin policy change and MFA factor enrollment patterns against those baselines.

  DETECTION LOGIC:
  - Z-score: Security-weakening policy changes (requireFactor=false, maxSessionLifetime=0) > 2σ
  - Z-score: Admin-on-behalf-of MFA factor enrollments for other users > 3σ
  - Cold-start: First-time security weakening with no prior baseline
  - Cold-start: First-time admin-enrolled factors for other users

  PREREQUISITE: okta_baseline_90d lookup table must be populated.
SnowflakeQuery: |
  -- OKTA SKELETON KEY BYPASS BEHAVIORAL DETECTION
  -- Reads 90-day baseline from lookup table; scans only last 7 days of raw logs
  -- Flags: security-weakening policy changes + admin bulk factor enrollments

  WITH policy_recent_hourly AS (
      SELECT
          actor:alternateId::string AS admin_email,
          DATE_TRUNC('hour', published) AS event_hour,
          COUNT(*) AS hourly_policy_changes,
          SUM(CASE
              WHEN debugContext:debugData:changedAttributes::string LIKE '%requireFactor%false%' THEN 1
              WHEN debugContext:debugData:changedAttributes::string LIKE '%maxSessionLifetimeMinutes%0%' THEN 1
              WHEN debugContext:debugData:changedAttributes::string RLIKE '.*minLength.*[1-6][^0-9].*' THEN 1
              ELSE 0
          END) AS hourly_security_weakenings
      FROM panther_logs.public.okta_systemlog
      WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '7 days'
          AND eventType IN ('policy.rule.update', 'policy.lifecycle.update')
          AND actor:alternateId::string LIKE '%@%'
      GROUP BY admin_email, event_hour
  ),

  policy_recent_stats AS (
      SELECT
          admin_email,
          SUM(hourly_policy_changes) AS recent_total_policy_changes,
          MAX(hourly_policy_changes) AS recent_max_policy_changes_per_hour,
          SUM(hourly_security_weakenings) AS recent_total_weakenings,
          MAX(hourly_security_weakenings) AS recent_max_weakenings_per_hour,
          MIN(event_hour) AS recent_policy_first_event,
          MAX(event_hour) AS recent_policy_last_event
      FROM policy_recent_hourly
      GROUP BY admin_email
  ),

  enrollment_recent_hourly AS (
      SELECT
          actor:alternateId::string AS admin_email,
          DATE_TRUNC('hour', published) AS event_hour,
          COUNT(*) AS hourly_enrollments,
          SUM(CASE
              WHEN actor:alternateId::string != target[0]:alternateId::string THEN 1
              ELSE 0
          END) AS hourly_admin_enrollments,
          COUNT(DISTINCT target[0]:alternateId::string) AS hourly_unique_targets
      FROM panther_logs.public.okta_systemlog
      WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '7 days'
          AND eventType = 'user.mfa.factor.activate'
          AND actor:alternateId::string LIKE '%@%'
      GROUP BY admin_email, event_hour
  ),

  enrollment_recent_stats AS (
      SELECT
          admin_email,
          SUM(hourly_enrollments) AS recent_total_enrollments,
          MAX(hourly_enrollments) AS recent_max_enrollments_per_hour,
          SUM(hourly_admin_enrollments) AS recent_total_admin_enrollments,
          MAX(hourly_admin_enrollments) AS recent_max_admin_enrollments_per_hour,
          MAX(hourly_unique_targets) AS recent_max_targets_per_hour,
          MIN(event_hour) AS recent_enrollment_first_event,
          MAX(event_hour) AS recent_enrollment_last_event
      FROM enrollment_recent_hourly
      GROUP BY admin_email
  ),

  admin_anomalies AS (
      SELECT
          COALESCE(pr.admin_email, er.admin_email) AS admin_email,

          -- POLICY BASELINE from lookup table
          COALESCE(b.baseline_total_policy_changes, 0) AS baseline_total_policy_changes,
          COALESCE(b.baseline_active_days_policy, 0) AS baseline_active_days_policy,
          ROUND(COALESCE(b.mean_policy_changes_per_hour, 0), 2) AS baseline_mean_policy_changes_per_hour,
          ROUND(COALESCE(b.stddev_policy_changes_per_hour, 0), 2) AS baseline_stddev_policy_changes_per_hour,
          COALESCE(b.baseline_total_weakenings, 0) AS baseline_total_weakenings,
          ROUND(COALESCE(b.mean_weakenings_per_hour, 0), 2) AS baseline_mean_weakenings_per_hour,

          -- POLICY RECENT ACTIVITY
          COALESCE(pr.recent_total_policy_changes, 0) AS recent_total_policy_changes,
          COALESCE(pr.recent_max_policy_changes_per_hour, 0) AS recent_max_policy_changes_per_hour,
          COALESCE(pr.recent_total_weakenings, 0) AS recent_total_weakenings,
          COALESCE(pr.recent_max_weakenings_per_hour, 0) AS recent_max_weakenings_per_hour,
          pr.recent_policy_first_event,
          pr.recent_policy_last_event,

          -- ENROLLMENT BASELINE from lookup table
          COALESCE(b.baseline_total_enrollments, 0) AS baseline_total_enrollments,
          COALESCE(b.baseline_active_days_enrollment, 0) AS baseline_active_days_enrollment,
          ROUND(COALESCE(b.mean_enrollments_per_hour, 0), 2) AS baseline_mean_enrollments_per_hour,
          ROUND(COALESCE(b.stddev_enrollments_per_hour, 0), 2) AS baseline_stddev_enrollments_per_hour,
          COALESCE(b.baseline_total_admin_enrollments, 0) AS baseline_total_admin_enrollments,
          ROUND(COALESCE(b.mean_admin_enrollments_per_hour, 0), 2) AS baseline_mean_admin_enrollments_per_hour,

          -- ENROLLMENT RECENT ACTIVITY
          COALESCE(er.recent_total_enrollments, 0) AS recent_total_enrollments,
          COALESCE(er.recent_max_enrollments_per_hour, 0) AS recent_max_enrollments_per_hour,
          COALESCE(er.recent_total_admin_enrollments, 0) AS recent_total_admin_enrollments,
          COALESCE(er.recent_max_admin_enrollments_per_hour, 0) AS recent_max_admin_enrollments_per_hour,
          COALESCE(er.recent_max_targets_per_hour, 0) AS recent_max_targets_per_hour,
          er.recent_enrollment_first_event,
          er.recent_enrollment_last_event,

          -- Z-SCORES: POLICY CHANGES
          CASE
              WHEN b.baseline_total_policy_changes >= 3
              THEN ROUND(
                  (COALESCE(pr.recent_max_policy_changes_per_hour, 0) - b.mean_policy_changes_per_hour) /
                  NULLIF(b.stddev_policy_changes_per_hour, 0),
                  2)
              ELSE NULL
          END AS z_score_policy_changes,

          CASE
              WHEN b.baseline_total_policy_changes >= 3
              THEN ROUND(
                  (COALESCE(pr.recent_max_weakenings_per_hour, 0) - b.mean_weakenings_per_hour) /
                  NULLIF(b.stddev_weakenings_per_hour, 0),
                  2)
              ELSE NULL
          END AS z_score_security_weakenings,

          -- Z-SCORES: ENROLLMENTS
          CASE
              WHEN b.baseline_total_enrollments >= 3
              THEN ROUND(
                  (COALESCE(er.recent_max_enrollments_per_hour, 0) - b.mean_enrollments_per_hour) /
                  NULLIF(b.stddev_enrollments_per_hour, 0),
                  2)
              ELSE NULL
          END AS z_score_enrollments,

          CASE
              WHEN b.baseline_total_admin_enrollments >= 3
              THEN ROUND(
                  (COALESCE(er.recent_max_admin_enrollments_per_hour, 0) - b.mean_admin_enrollments_per_hour) /
                  NULLIF(b.stddev_admin_enrollments_per_hour, 0),
                  2)
              ELSE NULL
          END AS z_score_admin_enrollments,

          CASE
              WHEN b.baseline_total_enrollments >= 3
              THEN ROUND(
                  (COALESCE(er.recent_max_targets_per_hour, 0) - b.mean_targets_per_hour) /
                  NULLIF(b.stddev_targets_per_hour, 0),
                  2)
              ELSE NULL
          END AS z_score_targets,

          -- ANOMALY FLAGS: Z-SCORE BASED
          CASE
              WHEN b.baseline_total_policy_changes >= 3
                  AND (COALESCE(pr.recent_max_policy_changes_per_hour, 0) - b.mean_policy_changes_per_hour) /
                      NULLIF(b.stddev_policy_changes_per_hour, 0) > 3
              THEN TRUE ELSE FALSE
          END AS is_policy_volume_anomaly,

          CASE
              WHEN b.baseline_total_policy_changes >= 3
                  AND (COALESCE(pr.recent_max_weakenings_per_hour, 0) - b.mean_weakenings_per_hour) /
                      NULLIF(b.stddev_weakenings_per_hour, 0) > 2
              THEN TRUE ELSE FALSE
          END AS is_security_weakening_anomaly,

          CASE
              WHEN b.baseline_total_admin_enrollments >= 3
                  AND (COALESCE(er.recent_max_admin_enrollments_per_hour, 0) - b.mean_admin_enrollments_per_hour) /
                      NULLIF(b.stddev_admin_enrollments_per_hour, 0) > 3
              THEN TRUE ELSE FALSE
          END AS is_admin_enrollment_anomaly,

          -- COLD START FLAGS
          CASE
              WHEN pr.recent_total_weakenings > 0
                  AND (b.baseline_total_weakenings IS NULL OR b.baseline_total_weakenings = 0)
              THEN TRUE ELSE FALSE
          END AS is_first_time_security_weakening,

          CASE
              WHEN (b.baseline_total_admin_enrollments IS NULL OR b.baseline_total_admin_enrollments = 0)
                  AND er.recent_total_admin_enrollments > 0
              THEN TRUE ELSE FALSE
          END AS is_first_time_admin_enrollment,

          -- OVERALL ANOMALY FLAG
          CASE
              WHEN (
                  (b.baseline_total_policy_changes >= 3
                      AND (COALESCE(pr.recent_max_weakenings_per_hour, 0) - b.mean_weakenings_per_hour) /
                          NULLIF(b.stddev_weakenings_per_hour, 0) > 2)
                  OR
                  (b.baseline_total_admin_enrollments >= 3
                      AND (COALESCE(er.recent_max_admin_enrollments_per_hour, 0) - b.mean_admin_enrollments_per_hour) /
                          NULLIF(b.stddev_admin_enrollments_per_hour, 0) > 3)
                  OR
                  (pr.recent_total_weakenings > 0
                      AND (b.baseline_total_weakenings IS NULL OR b.baseline_total_weakenings = 0))
                  OR
                  ((b.baseline_total_admin_enrollments IS NULL OR b.baseline_total_admin_enrollments = 0)
                      AND er.recent_total_admin_enrollments > 0)
              ) THEN TRUE
              ELSE FALSE
          END AS is_anomalous,

          -- ANOMALY SEVERITY SCORE
          CASE
              WHEN b.baseline_total_policy_changes >= 3 OR b.baseline_total_enrollments >= 3
              THEN
                  ROUND(
                      GREATEST(
                          COALESCE((COALESCE(pr.recent_max_weakenings_per_hour, 0) - b.mean_weakenings_per_hour) /
                              NULLIF(b.stddev_weakenings_per_hour, 0), 0) * 2,
                          0
                      ) +
                      GREATEST(
                          COALESCE((COALESCE(er.recent_max_admin_enrollments_per_hour, 0) - b.mean_admin_enrollments_per_hour) /
                              NULLIF(b.stddev_admin_enrollments_per_hour, 0), 0),
                          0
                      ),
                      2
                  )
              ELSE
                  ROUND(
                      (COALESCE(pr.recent_total_weakenings, 0) * 10) +
                      (COALESCE(er.recent_total_admin_enrollments, 0) * 3),
                      2
                  )
          END AS anomaly_severity_score

      FROM policy_recent_stats pr
      FULL OUTER JOIN enrollment_recent_stats er ON pr.admin_email = er.admin_email
      LEFT JOIN panther_lookups.public.okta_baseline_90d b
          ON COALESCE(pr.admin_email, er.admin_email) = b.user_email
  )

  SELECT
      admin_email,
      baseline_total_policy_changes,
      baseline_active_days_policy,
      baseline_mean_policy_changes_per_hour,
      baseline_stddev_policy_changes_per_hour,
      baseline_total_weakenings,
      baseline_mean_weakenings_per_hour,
      recent_total_policy_changes,
      recent_max_policy_changes_per_hour,
      recent_total_weakenings,
      recent_max_weakenings_per_hour,
      recent_policy_first_event,
      recent_policy_last_event,
      baseline_total_enrollments,
      baseline_active_days_enrollment,
      baseline_mean_enrollments_per_hour,
      baseline_stddev_enrollments_per_hour,
      baseline_total_admin_enrollments,
      baseline_mean_admin_enrollments_per_hour,
      recent_total_enrollments,
      recent_max_enrollments_per_hour,
      recent_total_admin_enrollments,
      recent_max_admin_enrollments_per_hour,
      recent_max_targets_per_hour,
      recent_enrollment_first_event,
      recent_enrollment_last_event,
      z_score_policy_changes,
      z_score_security_weakenings,
      z_score_enrollments,
      z_score_admin_enrollments,
      z_score_targets,
      is_policy_volume_anomaly,
      is_security_weakening_anomaly,
      is_admin_enrollment_anomaly,
      is_first_time_security_weakening,
      is_first_time_admin_enrollment,
      is_anomalous,
      anomaly_severity_score

  FROM admin_anomalies
  WHERE is_anomalous = TRUE
  ORDER BY anomaly_severity_score DESC
  LIMIT 100

DatabricksQuery: |
  -- OKTA SKELETON KEY BYPASS BEHAVIORAL DETECTION
  -- Reads 90-day baseline from lookup table; scans only last 7 days of raw logs
  -- Flags: security-weakening policy changes + admin bulk factor enrollments

  WITH policy_recent_hourly AS (
      SELECT
          actor:alternateId::string AS admin_email,
          DATE_TRUNC('hour', published) AS event_hour,
          COUNT(*) AS hourly_policy_changes,
          SUM(CASE
              WHEN debugContext:debugData:changedAttributes::string LIKE '%requireFactor%false%' THEN 1
              WHEN debugContext:debugData:changedAttributes::string LIKE '%maxSessionLifetimeMinutes%0%' THEN 1
              WHEN debugContext:debugData:changedAttributes::string RLIKE '.*minLength.*[1-6][^0-9].*' THEN 1
              ELSE 0
          END) AS hourly_security_weakenings
      FROM panther_logs.okta_systemlog
      WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '7 days'
          AND eventType IN ('policy.rule.update', 'policy.lifecycle.update')
          AND actor:alternateId::string LIKE '%@%'
      GROUP BY admin_email, event_hour
  ),

  policy_recent_stats AS (
      SELECT
          admin_email,
          SUM(hourly_policy_changes) AS recent_total_policy_changes,
          MAX(hourly_policy_changes) AS recent_max_policy_changes_per_hour,
          SUM(hourly_security_weakenings) AS recent_total_weakenings,
          MAX(hourly_security_weakenings) AS recent_max_weakenings_per_hour,
          MIN(event_hour) AS recent_policy_first_event,
          MAX(event_hour) AS recent_policy_last_event
      FROM policy_recent_hourly
      GROUP BY admin_email
  ),

  enrollment_recent_hourly AS (
      SELECT
          actor:alternateId::string AS admin_email,
          DATE_TRUNC('hour', published) AS event_hour,
          COUNT(*) AS hourly_enrollments,
          SUM(CASE
              WHEN actor:alternateId::string != target[0]:alternateId::string THEN 1
              ELSE 0
          END) AS hourly_admin_enrollments,
          COUNT(DISTINCT target[0]:alternateId::string) AS hourly_unique_targets
      FROM panther_logs.okta_systemlog
      WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '7 days'
          AND eventType = 'user.mfa.factor.activate'
          AND actor:alternateId::string LIKE '%@%'
      GROUP BY admin_email, event_hour
  ),

  enrollment_recent_stats AS (
      SELECT
          admin_email,
          SUM(hourly_enrollments) AS recent_total_enrollments,
          MAX(hourly_enrollments) AS recent_max_enrollments_per_hour,
          SUM(hourly_admin_enrollments) AS recent_total_admin_enrollments,
          MAX(hourly_admin_enrollments) AS recent_max_admin_enrollments_per_hour,
          MAX(hourly_unique_targets) AS recent_max_targets_per_hour,
          MIN(event_hour) AS recent_enrollment_first_event,
          MAX(event_hour) AS recent_enrollment_last_event
      FROM enrollment_recent_hourly
      GROUP BY admin_email
  ),

  admin_anomalies AS (
      SELECT
          COALESCE(pr.admin_email, er.admin_email) AS admin_email,
          COALESCE(b.baseline_total_policy_changes, 0) AS baseline_total_policy_changes,
          COALESCE(b.baseline_active_days_policy, 0) AS baseline_active_days_policy,
          ROUND(COALESCE(b.mean_policy_changes_per_hour, 0), 2) AS baseline_mean_policy_changes_per_hour,
          ROUND(COALESCE(b.stddev_policy_changes_per_hour, 0), 2) AS baseline_stddev_policy_changes_per_hour,
          COALESCE(b.baseline_total_weakenings, 0) AS baseline_total_weakenings,
          ROUND(COALESCE(b.mean_weakenings_per_hour, 0), 2) AS baseline_mean_weakenings_per_hour,
          COALESCE(pr.recent_total_policy_changes, 0) AS recent_total_policy_changes,
          COALESCE(pr.recent_max_policy_changes_per_hour, 0) AS recent_max_policy_changes_per_hour,
          COALESCE(pr.recent_total_weakenings, 0) AS recent_total_weakenings,
          COALESCE(pr.recent_max_weakenings_per_hour, 0) AS recent_max_weakenings_per_hour,
          pr.recent_policy_first_event,
          pr.recent_policy_last_event,
          COALESCE(b.baseline_total_enrollments, 0) AS baseline_total_enrollments,
          COALESCE(b.baseline_active_days_enrollment, 0) AS baseline_active_days_enrollment,
          ROUND(COALESCE(b.mean_enrollments_per_hour, 0), 2) AS baseline_mean_enrollments_per_hour,
          ROUND(COALESCE(b.stddev_enrollments_per_hour, 0), 2) AS baseline_stddev_enrollments_per_hour,
          COALESCE(b.baseline_total_admin_enrollments, 0) AS baseline_total_admin_enrollments,
          ROUND(COALESCE(b.mean_admin_enrollments_per_hour, 0), 2) AS baseline_mean_admin_enrollments_per_hour,
          COALESCE(er.recent_total_enrollments, 0) AS recent_total_enrollments,
          COALESCE(er.recent_max_enrollments_per_hour, 0) AS recent_max_enrollments_per_hour,
          COALESCE(er.recent_total_admin_enrollments, 0) AS recent_total_admin_enrollments,
          COALESCE(er.recent_max_admin_enrollments_per_hour, 0) AS recent_max_admin_enrollments_per_hour,
          COALESCE(er.recent_max_targets_per_hour, 0) AS recent_max_targets_per_hour,
          er.recent_enrollment_first_event,
          er.recent_enrollment_last_event,
          CASE
              WHEN b.baseline_total_policy_changes >= 3
              THEN ROUND(
                  (COALESCE(pr.recent_max_policy_changes_per_hour, 0) - b.mean_policy_changes_per_hour) /
                  NULLIF(b.stddev_policy_changes_per_hour, 0),
                  2)
              ELSE NULL
          END AS z_score_policy_changes,
          CASE
              WHEN b.baseline_total_policy_changes >= 3
              THEN ROUND(
                  (COALESCE(pr.recent_max_weakenings_per_hour, 0) - b.mean_weakenings_per_hour) /
                  NULLIF(b.stddev_weakenings_per_hour, 0),
                  2)
              ELSE NULL
          END AS z_score_security_weakenings,
          CASE
              WHEN b.baseline_total_enrollments >= 3
              THEN ROUND(
                  (COALESCE(er.recent_max_enrollments_per_hour, 0) - b.mean_enrollments_per_hour) /
                  NULLIF(b.stddev_enrollments_per_hour, 0),
                  2)
              ELSE NULL
          END AS z_score_enrollments,
          CASE
              WHEN b.baseline_total_admin_enrollments >= 3
              THEN ROUND(
                  (COALESCE(er.recent_max_admin_enrollments_per_hour, 0) - b.mean_admin_enrollments_per_hour) /
                  NULLIF(b.stddev_admin_enrollments_per_hour, 0),
                  2)
              ELSE NULL
          END AS z_score_admin_enrollments,
          CASE
              WHEN b.baseline_total_enrollments >= 3
              THEN ROUND(
                  (COALESCE(er.recent_max_targets_per_hour, 0) - b.mean_targets_per_hour) /
                  NULLIF(b.stddev_targets_per_hour, 0),
                  2)
              ELSE NULL
          END AS z_score_targets,
          CASE
              WHEN b.baseline_total_policy_changes >= 3
                  AND (COALESCE(pr.recent_max_policy_changes_per_hour, 0) - b.mean_policy_changes_per_hour) /
                      NULLIF(b.stddev_policy_changes_per_hour, 0) > 3
              THEN TRUE ELSE FALSE
          END AS is_policy_volume_anomaly,
          CASE
              WHEN b.baseline_total_policy_changes >= 3
                  AND (COALESCE(pr.recent_max_weakenings_per_hour, 0) - b.mean_weakenings_per_hour) /
                      NULLIF(b.stddev_weakenings_per_hour, 0) > 2
              THEN TRUE ELSE FALSE
          END AS is_security_weakening_anomaly,
          CASE
              WHEN b.baseline_total_admin_enrollments >= 3
                  AND (COALESCE(er.recent_max_admin_enrollments_per_hour, 0) - b.mean_admin_enrollments_per_hour) /
                      NULLIF(b.stddev_admin_enrollments_per_hour, 0) > 3
              THEN TRUE ELSE FALSE
          END AS is_admin_enrollment_anomaly,
          CASE
              WHEN pr.recent_total_weakenings > 0
                  AND (b.baseline_total_weakenings IS NULL OR b.baseline_total_weakenings = 0)
              THEN TRUE ELSE FALSE
          END AS is_first_time_security_weakening,
          CASE
              WHEN (b.baseline_total_admin_enrollments IS NULL OR b.baseline_total_admin_enrollments = 0)
                  AND er.recent_total_admin_enrollments > 0
              THEN TRUE ELSE FALSE
          END AS is_first_time_admin_enrollment,
          CASE
              WHEN (
                  (b.baseline_total_policy_changes >= 3
                      AND (COALESCE(pr.recent_max_weakenings_per_hour, 0) - b.mean_weakenings_per_hour) /
                          NULLIF(b.stddev_weakenings_per_hour, 0) > 2)
                  OR
                  (b.baseline_total_admin_enrollments >= 3
                      AND (COALESCE(er.recent_max_admin_enrollments_per_hour, 0) - b.mean_admin_enrollments_per_hour) /
                          NULLIF(b.stddev_admin_enrollments_per_hour, 0) > 3)
                  OR
                  (pr.recent_total_weakenings > 0
                      AND (b.baseline_total_weakenings IS NULL OR b.baseline_total_weakenings = 0))
                  OR
                  ((b.baseline_total_admin_enrollments IS NULL OR b.baseline_total_admin_enrollments = 0)
                      AND er.recent_total_admin_enrollments > 0)
              ) THEN TRUE
              ELSE FALSE
          END AS is_anomalous,
          CASE
              WHEN b.baseline_total_policy_changes >= 3 OR b.baseline_total_enrollments >= 3
              THEN
                  ROUND(
                      GREATEST(
                          COALESCE((COALESCE(pr.recent_max_weakenings_per_hour, 0) - b.mean_weakenings_per_hour) /
                              NULLIF(b.stddev_weakenings_per_hour, 0), 0) * 2,
                          0
                      ) +
                      GREATEST(
                          COALESCE((COALESCE(er.recent_max_admin_enrollments_per_hour, 0) - b.mean_admin_enrollments_per_hour) /
                              NULLIF(b.stddev_admin_enrollments_per_hour, 0), 0),
                          0
                      ),
                      2
                  )
              ELSE
                  ROUND(
                      (COALESCE(pr.recent_total_weakenings, 0) * 10) +
                      (COALESCE(er.recent_total_admin_enrollments, 0) * 3),
                      2
                  )
          END AS anomaly_severity_score

      FROM policy_recent_stats pr
      FULL OUTER JOIN enrollment_recent_stats er ON pr.admin_email = er.admin_email
      LEFT JOIN panther_lookups.okta_baseline_90d b
          ON COALESCE(pr.admin_email, er.admin_email) = b.user_email
  )

  SELECT
      admin_email,
      baseline_total_policy_changes,
      baseline_active_days_policy,
      baseline_mean_policy_changes_per_hour,
      baseline_stddev_policy_changes_per_hour,
      baseline_total_weakenings,
      baseline_mean_weakenings_per_hour,
      recent_total_policy_changes,
      recent_max_policy_changes_per_hour,
      recent_total_weakenings,
      recent_max_weakenings_per_hour,
      recent_policy_first_event,
      recent_policy_last_event,
      baseline_total_enrollments,
      baseline_active_days_enrollment,
      baseline_mean_enrollments_per_hour,
      baseline_stddev_enrollments_per_hour,
      baseline_total_admin_enrollments,
      baseline_mean_admin_enrollments_per_hour,
      recent_total_enrollments,
      recent_max_enrollments_per_hour,
      recent_total_admin_enrollments,
      recent_max_admin_enrollments_per_hour,
      recent_max_targets_per_hour,
      recent_enrollment_first_event,
      recent_enrollment_last_event,
      z_score_policy_changes,
      z_score_security_weakenings,
      z_score_enrollments,
      z_score_admin_enrollments,
      z_score_targets,
      is_policy_volume_anomaly,
      is_security_weakening_anomaly,
      is_admin_enrollment_anomaly,
      is_first_time_security_weakening,
      is_first_time_admin_enrollment,
      is_anomalous,
      anomaly_severity_score

  FROM admin_anomalies
  WHERE is_anomalous = TRUE
  ORDER BY anomaly_severity_score DESC
  LIMIT 100

Schedule:
  RateMinutes: 1440  # Run once per day
  TimeoutMinutes: 10
Tags:
  - Okta
  - Active Directory
  - Skeleton Key
  - Anomaly Detection
  - Statistical Analysis

Stages and Predicates

Stage 1: source

Table
admin_anomalies

Stage 2: filter

  • is_anomalous is TRUE

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
admin_email
baseline_total_policy_changes
baseline_active_days_policy
baseline_mean_policy_changes_per_hour
baseline_stddev_policy_changes_per_hour
baseline_total_weakenings
baseline_mean_weakenings_per_hour
recent_total_policy_changes
recent_max_policy_changes_per_hour
recent_total_weakenings
recent_max_weakenings_per_hour
recent_policy_first_event
recent_policy_last_event
baseline_total_enrollments
baseline_active_days_enrollment
baseline_mean_enrollments_per_hour
baseline_stddev_enrollments_per_hour
baseline_total_admin_enrollments
baseline_mean_admin_enrollments_per_hour
recent_total_enrollments
recent_max_enrollments_per_hour
recent_total_admin_enrollments
recent_max_admin_enrollments_per_hour
recent_max_targets_per_hour
recent_enrollment_first_event
recent_enrollment_last_event
z_score_policy_changes
z_score_security_weakenings
z_score_enrollments
z_score_admin_enrollments
z_score_targets
is_policy_volume_anomaly
is_security_weakening_anomaly
is_admin_enrollment_anomaly
is_first_time_security_weakening
is_first_time_admin_enrollment
is_anomalous
anomaly_severity_score

Query.Okta.SWABulkAccessBehavioral

#
Tags
Okta, SWA, Credential Access, Anomaly Detection, Statistical Analysis
Source
github.com/panther-labs/panther-analysis

Detects Okta SWA bulk credential extraction, abuse, and access from new network sources using behavioral z-score analysis. Reads pre-computed 90-day baselines from the okta_baseline_90d lookup table, then compares recent (last 7 days) admin SWA access and credential extraction patterns against those baselines. DETECTION LOGIC: - Z-score: SWA authentication volume spike (> 3σ above baseline) - Z-score: Unique SWA app diversity spike (accessing many different apps in an hour) (> 3σ) - Z-score: Credential extraction volume spike (> 3σ) - Z-score: Victim diversity spike (targeting many different users) (> 2σ) - Cold-start: First-time bulk SWA access (>= 10 events, no baseline) - Cold-start: First-time credential extraction (>= 5 events, no baseline) - New source: SWA access from IP address not seen in 90-day baseline (requires baseline or >= 3 recent events) - New source: SWA access from user agent not seen in 90-day baseline - Critical compound: New IP + any credential extraction events PREREQUISITE: okta_baseline_90d lookup table must be populated.

MITRE ATT&CK coverage

TacticTechniques
Credential AccessNo specific technique

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule specification

AnalysisType: scheduled_query
QueryName: "Query.Okta.SWABulkAccessBehavioral"
Enabled: false
Description: |
  Detects Okta SWA bulk credential extraction, abuse, and access from new network sources using
  behavioral z-score analysis. Reads pre-computed 90-day baselines from the okta_baseline_90d
  lookup table, then compares recent (last 7 days) admin SWA access and credential extraction
  patterns against those baselines.

  DETECTION LOGIC:
  - Z-score: SWA authentication volume spike (> 3σ above baseline)
  - Z-score: Unique SWA app diversity spike (accessing many different apps in an hour) (> 3σ)
  - Z-score: Credential extraction volume spike (> 3σ)
  - Z-score: Victim diversity spike (targeting many different users) (> 2σ)
  - Cold-start: First-time bulk SWA access (>= 10 events, no baseline)
  - Cold-start: First-time credential extraction (>= 5 events, no baseline)
  - New source: SWA access from IP address not seen in 90-day baseline (requires baseline or >= 3 recent events)
  - New source: SWA access from user agent not seen in 90-day baseline
  - Critical compound: New IP + any credential extraction events

  PREREQUISITE: okta_baseline_90d lookup table must be populated.
SnowflakeQuery: |
  -- OKTA SWA BULK CREDENTIAL EXTRACTION BEHAVIORAL DETECTION
  -- Reads 90-day baseline from lookup table; scans only last 7 days of raw logs
  -- Flags: bulk SWA authentication spikes + credential extraction anomalies

  WITH swa_access_recent_hourly AS (
      SELECT
          actor:alternateId::string AS admin_email,
          DATE_TRUNC('hour', published) AS event_hour,
          COUNT(*) AS hourly_swa_events,
          COUNT(DISTINCT target[0]:displayName::string) AS hourly_app_diversity,
          COUNT(DISTINCT client:geographicalContext:country::string) AS hourly_country_diversity,
          COUNT(DISTINCT client:ipAddress::string) AS hourly_ip_diversity
      FROM panther_logs.public.okta_systemlog
      WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '7 days'
          AND eventType = 'user.authentication.sso'
          AND actor:alternateId::string LIKE '%@%'
          -- NOTE: swa_mode and signOnMode are internal Okta debug fields.
          -- Availability varies by tenant. Validate against actual log samples
          -- and adjust conditions if SWA events are not being detected.
          AND (
              debugContext:debugData:swa_mode::string = 'true'
              OR debugContext:debugData:signOnMode::string LIKE '%AUTO%LOGIN%'
              OR debugContext:debugData:signOnMode::string LIKE '%BROWSER%PLUGIN%'
          )
      GROUP BY admin_email, event_hour
  ),

  swa_access_recent_stats AS (
      SELECT
          admin_email,
          SUM(hourly_swa_events) AS recent_total_swa_events,
          MAX(hourly_swa_events) AS recent_max_swa_events_per_hour,
          MAX(hourly_app_diversity) AS recent_max_app_diversity_per_hour,
          MAX(hourly_country_diversity) AS recent_max_country_diversity_per_hour,
          MAX(hourly_ip_diversity) AS recent_max_ip_diversity_per_hour,
          AVG(hourly_swa_events)::FLOAT AS recent_avg_swa_events_per_hour,
          MIN(event_hour) AS recent_swa_first_event,
          MAX(event_hour) AS recent_swa_last_event
      FROM swa_access_recent_hourly
      GROUP BY admin_email
  ),

  recent_sources AS (
      SELECT
          actor:alternateId::string AS admin_email,
          ARRAY_COMPACT(ARRAY_AGG(DISTINCT client:ipAddress::string)) AS recent_ips,
          ARRAY_COMPACT(ARRAY_AGG(DISTINCT client:userAgent:rawUserAgent::string)) AS recent_user_agents
      FROM panther_logs.public.okta_systemlog
      WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '7 days'
          AND actor:alternateId::string LIKE '%@%'
          AND (
              (eventType = 'user.authentication.sso'
                  AND (
                      debugContext:debugData:swa_mode::string = 'true'
                      OR debugContext:debugData:signOnMode::string LIKE '%AUTO%LOGIN%'
                      OR debugContext:debugData:signOnMode::string LIKE '%BROWSER%PLUGIN%'
                  ))
              OR eventType = 'application.user_membership.change_username'
          )
      GROUP BY admin_email
  ),

  extraction_by_ip AS (
      SELECT
          actor:alternateId::string AS admin_email,
          client:ipAddress::string AS source_ip,
          COUNT(*) AS extractions_from_ip,
          COUNT(DISTINCT target[0]:alternateId::string) AS victims_from_ip
      FROM panther_logs.public.okta_systemlog
      WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '7 days'
          AND eventType = 'application.user_membership.change_username'
          AND actor:alternateId::string LIKE '%@%'
      GROUP BY admin_email, source_ip
  ),

  credential_extraction_recent_hourly AS (
      SELECT
          actor:alternateId::string AS admin_email,
          DATE_TRUNC('hour', published) AS event_hour,
          COUNT(*) AS hourly_credential_extractions,
          COUNT(DISTINCT target[0]:alternateId::string) AS hourly_victim_diversity,
          COUNT(DISTINCT client:geographicalContext:country::string) AS hourly_country_diversity
      FROM panther_logs.public.okta_systemlog
      WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '7 days'
          AND eventType = 'application.user_membership.change_username'
          AND actor:alternateId::string LIKE '%@%'
      GROUP BY admin_email, event_hour
  ),

  credential_extraction_recent_stats AS (
      SELECT
          admin_email,
          SUM(hourly_credential_extractions) AS recent_total_extractions,
          MAX(hourly_credential_extractions) AS recent_max_extractions_per_hour,
          MAX(hourly_victim_diversity) AS recent_max_victim_diversity_per_hour,
          MAX(hourly_country_diversity) AS recent_max_extraction_country_diversity_per_hour,
          AVG(hourly_credential_extractions)::FLOAT AS recent_avg_extractions_per_hour,
          MIN(event_hour) AS recent_extraction_first_event,
          MAX(event_hour) AS recent_extraction_last_event
      FROM credential_extraction_recent_hourly
      GROUP BY admin_email
  ),

  -- Pre-compute new-IP extraction stats to avoid repeated correlated subqueries
  new_ip_extraction_stats AS (
      SELECT
          eip.admin_email,
          SUM(eip.extractions_from_ip) AS new_ip_extraction_count,
          SUM(eip.victims_from_ip) AS new_ip_victim_count
      FROM extraction_by_ip eip
      LEFT JOIN panther_lookups.public.okta_baseline_90d b ON eip.admin_email = b.user_email
      WHERE b.swa_known_ips IS NULL
          OR NOT ARRAY_CONTAINS(eip.source_ip::variant, b.swa_known_ips)
      GROUP BY eip.admin_email
  ),

  admin_anomalies AS (
      SELECT
          COALESCE(sr.admin_email, cr.admin_email) AS admin_email,

          -- SWA ACCESS: BASELINE from lookup table
          COALESCE(b.baseline_total_swa_events, 0) AS baseline_total_swa_events,
          COALESCE(b.baseline_active_days_swa, 0) AS baseline_active_days_swa,
          ROUND(COALESCE(b.mean_swa_events_per_hour, 0), 2) AS baseline_mean_swa_events_per_hour,
          ROUND(COALESCE(b.stddev_swa_events_per_hour, 0), 2) AS baseline_stddev_swa_events_per_hour,
          ROUND(COALESCE(b.mean_app_diversity_per_hour, 0), 2) AS baseline_mean_app_diversity_per_hour,
          ROUND(COALESCE(b.stddev_app_diversity_per_hour, 0), 2) AS baseline_stddev_app_diversity_per_hour,
          ROUND(COALESCE(b.mean_country_diversity_per_hour, 0), 2) AS baseline_mean_country_diversity_per_hour,

          -- SWA ACCESS: RECENT ACTIVITY
          COALESCE(sr.recent_total_swa_events, 0) AS recent_total_swa_events,
          COALESCE(sr.recent_max_swa_events_per_hour, 0) AS recent_max_swa_events_per_hour,
          COALESCE(sr.recent_max_app_diversity_per_hour, 0) AS recent_max_app_diversity_per_hour,
          COALESCE(sr.recent_max_country_diversity_per_hour, 0) AS recent_max_country_diversity_per_hour,
          COALESCE(sr.recent_max_ip_diversity_per_hour, 0) AS recent_max_ip_diversity_per_hour,
          ROUND(COALESCE(sr.recent_avg_swa_events_per_hour, 0), 2) AS recent_avg_swa_events_per_hour,
          sr.recent_swa_first_event,
          sr.recent_swa_last_event,

          -- CREDENTIAL EXTRACTION: BASELINE from lookup table
          COALESCE(b.baseline_total_extractions, 0) AS baseline_total_extractions,
          COALESCE(b.baseline_active_days_extraction, 0) AS baseline_active_days_extraction,
          ROUND(COALESCE(b.mean_extractions_per_hour, 0), 2) AS baseline_mean_extractions_per_hour,
          ROUND(COALESCE(b.stddev_extractions_per_hour, 0), 2) AS baseline_stddev_extractions_per_hour,
          ROUND(COALESCE(b.mean_victim_diversity_per_hour, 0), 2) AS baseline_mean_victim_diversity_per_hour,
          ROUND(COALESCE(b.stddev_victim_diversity_per_hour, 0), 2) AS baseline_stddev_victim_diversity_per_hour,

          -- CREDENTIAL EXTRACTION: RECENT ACTIVITY
          COALESCE(cr.recent_total_extractions, 0) AS recent_total_extractions,
          COALESCE(cr.recent_max_extractions_per_hour, 0) AS recent_max_extractions_per_hour,
          COALESCE(cr.recent_max_victim_diversity_per_hour, 0) AS recent_max_victim_diversity_per_hour,
          COALESCE(cr.recent_max_extraction_country_diversity_per_hour, 0) AS recent_max_extraction_country_diversity_per_hour,
          ROUND(COALESCE(cr.recent_avg_extractions_per_hour, 0), 2) AS recent_avg_extractions_per_hour,
          cr.recent_extraction_first_event,
          cr.recent_extraction_last_event,

          -- Z-SCORES: SWA ACCESS
          CASE
              WHEN b.baseline_total_swa_events >= 3
              THEN ROUND(
                  (COALESCE(sr.recent_max_swa_events_per_hour, 0) - b.mean_swa_events_per_hour) /
                  NULLIF(b.stddev_swa_events_per_hour, 0),
                  2)
              ELSE NULL
          END AS z_score_swa_volume,

          CASE
              WHEN b.baseline_total_swa_events >= 3
              THEN ROUND(
                  (COALESCE(sr.recent_max_app_diversity_per_hour, 0) - b.mean_app_diversity_per_hour) /
                  NULLIF(b.stddev_app_diversity_per_hour, 0),
                  2)
              ELSE NULL
          END AS z_score_app_diversity,

          CASE
              WHEN b.baseline_total_swa_events >= 3
              THEN ROUND(
                  (COALESCE(sr.recent_max_country_diversity_per_hour, 0) - b.mean_country_diversity_per_hour) /
                  NULLIF(b.stddev_country_diversity_per_hour, 0),
                  2)
              ELSE NULL
          END AS z_score_swa_country_diversity,

          -- Z-SCORES: CREDENTIAL EXTRACTION
          CASE
              WHEN b.baseline_total_extractions >= 3
              THEN ROUND(
                  (COALESCE(cr.recent_max_extractions_per_hour, 0) - b.mean_extractions_per_hour) /
                  NULLIF(b.stddev_extractions_per_hour, 0),
                  2)
              ELSE NULL
          END AS z_score_extraction_volume,

          CASE
              WHEN b.baseline_total_extractions >= 3
              THEN ROUND(
                  (COALESCE(cr.recent_max_victim_diversity_per_hour, 0) - b.mean_victim_diversity_per_hour) /
                  NULLIF(b.stddev_victim_diversity_per_hour, 0),
                  2)
              ELSE NULL
          END AS z_score_victim_diversity,

          -- NEW SOURCE FLAGS
          CASE
              WHEN rs.admin_email IS NULL THEN FALSE
              WHEN b.swa_known_ips IS NULL THEN TRUE
              WHEN ARRAY_SIZE(ARRAY_EXCEPT(COALESCE(rs.recent_ips, ARRAY_CONSTRUCT()), b.swa_known_ips)) > 0 THEN TRUE
              ELSE FALSE
          END AS has_new_ip,

          CASE
              WHEN rs.admin_email IS NULL THEN FALSE
              WHEN b.swa_known_user_agents IS NULL THEN TRUE
              WHEN ARRAY_SIZE(ARRAY_EXCEPT(COALESCE(rs.recent_user_agents, ARRAY_CONSTRUCT()), b.swa_known_user_agents)) > 0 THEN TRUE
              ELSE FALSE
          END AS has_new_user_agent,

          CASE
              WHEN rs.admin_email IS NULL THEN 0
              WHEN b.swa_known_ips IS NULL THEN ARRAY_SIZE(COALESCE(rs.recent_ips, ARRAY_CONSTRUCT()))
              ELSE ARRAY_SIZE(ARRAY_EXCEPT(COALESCE(rs.recent_ips, ARRAY_CONSTRUCT()), b.swa_known_ips))
          END AS new_ip_count,

          COALESCE(niex.new_ip_extraction_count, 0) AS new_ip_extraction_count,
          COALESCE(niex.new_ip_victim_count, 0) AS new_ip_victim_count,

          -- ANOMALY FLAGS: Z-SCORE BASED
          CASE
              WHEN b.baseline_total_swa_events >= 3
                  AND (COALESCE(sr.recent_max_swa_events_per_hour, 0) - b.mean_swa_events_per_hour) /
                      NULLIF(b.stddev_swa_events_per_hour, 0) > 3
              THEN TRUE ELSE FALSE
          END AS is_swa_volume_anomaly,

          CASE
              WHEN b.baseline_total_swa_events >= 3
                  AND (COALESCE(sr.recent_max_app_diversity_per_hour, 0) - b.mean_app_diversity_per_hour) /
                      NULLIF(b.stddev_app_diversity_per_hour, 0) > 3
              THEN TRUE ELSE FALSE
          END AS is_app_diversity_anomaly,

          CASE
              WHEN b.baseline_total_extractions >= 3
                  AND (COALESCE(cr.recent_max_extractions_per_hour, 0) - b.mean_extractions_per_hour) /
                      NULLIF(b.stddev_extractions_per_hour, 0) > 3
              THEN TRUE ELSE FALSE
          END AS is_extraction_volume_anomaly,

          CASE
              WHEN b.baseline_total_extractions >= 3
                  AND (COALESCE(cr.recent_max_victim_diversity_per_hour, 0) - b.mean_victim_diversity_per_hour) /
                      NULLIF(b.stddev_victim_diversity_per_hour, 0) > 2
              THEN TRUE ELSE FALSE
          END AS is_victim_diversity_anomaly,

          -- COLD START FLAGS
          CASE
              WHEN (b.baseline_total_swa_events IS NULL OR b.baseline_total_swa_events < 3)
                  AND sr.recent_total_swa_events >= 10
              THEN TRUE ELSE FALSE
          END AS is_first_time_bulk_swa_access,

          CASE
              WHEN (b.baseline_total_extractions IS NULL OR b.baseline_total_extractions < 3)
                  AND cr.recent_total_extractions >= 5
              THEN TRUE ELSE FALSE
          END AS is_first_time_credential_extraction,

          -- OVERALL ANOMALY FLAG
          CASE
              WHEN (
                  (b.baseline_total_swa_events >= 3
                      AND (COALESCE(sr.recent_max_swa_events_per_hour, 0) - b.mean_swa_events_per_hour) /
                          NULLIF(b.stddev_swa_events_per_hour, 0) > 3)
                  OR
                  (b.baseline_total_swa_events >= 3
                      AND (COALESCE(sr.recent_max_app_diversity_per_hour, 0) - b.mean_app_diversity_per_hour) /
                          NULLIF(b.stddev_app_diversity_per_hour, 0) > 3)
                  OR
                  (b.baseline_total_extractions >= 3
                      AND (COALESCE(cr.recent_max_extractions_per_hour, 0) - b.mean_extractions_per_hour) /
                          NULLIF(b.stddev_extractions_per_hour, 0) > 3)
                  OR
                  (b.baseline_total_extractions >= 3
                      AND (COALESCE(cr.recent_max_victim_diversity_per_hour, 0) - b.mean_victim_diversity_per_hour) /
                          NULLIF(b.stddev_victim_diversity_per_hour, 0) > 2)
                  OR
                  ((b.baseline_total_swa_events IS NULL OR b.baseline_total_swa_events < 3)
                      AND sr.recent_total_swa_events >= 10)
                  OR
                  ((b.baseline_total_extractions IS NULL OR b.baseline_total_extractions < 3)
                      AND cr.recent_total_extractions >= 5)
                  OR
                  -- New IP: requires baseline to exist (known IPs list present) + new IP detected,
                  -- OR no baseline yet but minimum activity threshold to suppress new-employee noise
                  (
                      rs.admin_email IS NOT NULL
                      AND (
                          (b.swa_known_ips IS NOT NULL
                           AND ARRAY_SIZE(ARRAY_EXCEPT(COALESCE(rs.recent_ips, ARRAY_CONSTRUCT()), b.swa_known_ips)) > 0)
                          OR
                          (b.swa_known_ips IS NULL
                           AND (COALESCE(sr.recent_total_swa_events, 0) >= 3
                                OR COALESCE(cr.recent_total_extractions, 0) >= 1))
                      )
                      AND (COALESCE(sr.recent_total_swa_events, 0) > 0
                           OR COALESCE(cr.recent_total_extractions, 0) > 0)
                  )
                  OR
                  -- New user agent with any activity (requires existing baseline to avoid over-firing)
                  (
                      rs.admin_email IS NOT NULL
                      AND b.swa_known_user_agents IS NOT NULL
                      AND ARRAY_SIZE(ARRAY_EXCEPT(COALESCE(rs.recent_user_agents, ARRAY_CONSTRUCT()), b.swa_known_user_agents)) > 0
                      AND (COALESCE(sr.recent_total_swa_events, 0) > 0
                           OR COALESCE(cr.recent_total_extractions, 0) > 0)
                  )
              ) THEN TRUE
              ELSE FALSE
          END AS is_anomalous,

          -- ANOMALY SEVERITY SCORE
          CASE
              WHEN b.baseline_total_swa_events >= 3 OR b.baseline_total_extractions >= 3
              THEN
                  ROUND(
                      GREATEST(
                          COALESCE((COALESCE(sr.recent_max_swa_events_per_hour, 0) - b.mean_swa_events_per_hour) /
                              NULLIF(b.stddev_swa_events_per_hour, 0), 0),
                          0
                      ) +
                      GREATEST(
                          COALESCE((COALESCE(sr.recent_max_app_diversity_per_hour, 0) - b.mean_app_diversity_per_hour) /
                              NULLIF(b.stddev_app_diversity_per_hour, 0), 0),
                          0
                      ) +
                      GREATEST(
                          COALESCE((COALESCE(cr.recent_max_extractions_per_hour, 0) - b.mean_extractions_per_hour) /
                              NULLIF(b.stddev_extractions_per_hour, 0), 0) * 3,
                          0
                      ) +
                      GREATEST(
                          COALESCE((COALESCE(cr.recent_max_victim_diversity_per_hour, 0) - b.mean_victim_diversity_per_hour) /
                              NULLIF(b.stddev_victim_diversity_per_hour, 0), 0) * 2,
                          0
                      ) +
                      -- New IP + extraction: weight 5x extraction count from new IPs
                      GREATEST(COALESCE(niex.new_ip_extraction_count, 0) * 5.0, 0),
                      2
                  )
              ELSE
                  ROUND(
                      (COALESCE(sr.recent_total_swa_events, 0) * 0.5) +
                      (COALESCE(cr.recent_total_extractions, 0) * 10) +
                      (COALESCE(cr.recent_max_victim_diversity_per_hour, 0) * 5) +
                      COALESCE(niex.new_ip_extraction_count, 0) * 5.0,
                      2
                  )
          END AS anomaly_severity_score

      FROM swa_access_recent_stats sr
      FULL OUTER JOIN credential_extraction_recent_stats cr ON sr.admin_email = cr.admin_email
      LEFT JOIN panther_lookups.public.okta_baseline_90d b
          ON COALESCE(sr.admin_email, cr.admin_email) = b.user_email
      LEFT JOIN recent_sources rs ON COALESCE(sr.admin_email, cr.admin_email) = rs.admin_email
      LEFT JOIN new_ip_extraction_stats niex ON COALESCE(sr.admin_email, cr.admin_email) = niex.admin_email
  )

  SELECT
      admin_email,
      baseline_total_swa_events,
      baseline_active_days_swa,
      baseline_mean_swa_events_per_hour,
      baseline_stddev_swa_events_per_hour,
      baseline_mean_app_diversity_per_hour,
      baseline_stddev_app_diversity_per_hour,
      baseline_mean_country_diversity_per_hour,
      recent_total_swa_events,
      recent_max_swa_events_per_hour,
      recent_max_app_diversity_per_hour,
      recent_max_country_diversity_per_hour,
      recent_max_ip_diversity_per_hour,
      recent_avg_swa_events_per_hour,
      recent_swa_first_event,
      recent_swa_last_event,
      baseline_total_extractions,
      baseline_active_days_extraction,
      baseline_mean_extractions_per_hour,
      baseline_stddev_extractions_per_hour,
      baseline_mean_victim_diversity_per_hour,
      baseline_stddev_victim_diversity_per_hour,
      recent_total_extractions,
      recent_max_extractions_per_hour,
      recent_max_victim_diversity_per_hour,
      recent_max_extraction_country_diversity_per_hour,
      recent_avg_extractions_per_hour,
      recent_extraction_first_event,
      recent_extraction_last_event,
      z_score_swa_volume,
      z_score_app_diversity,
      z_score_swa_country_diversity,
      z_score_extraction_volume,
      z_score_victim_diversity,
      is_swa_volume_anomaly,
      is_app_diversity_anomaly,
      is_extraction_volume_anomaly,
      is_victim_diversity_anomaly,
      is_first_time_bulk_swa_access,
      is_first_time_credential_extraction,
      has_new_ip,
      has_new_user_agent,
      new_ip_count,
      new_ip_extraction_count,
      new_ip_victim_count,
      is_anomalous,
      anomaly_severity_score

  FROM admin_anomalies
  WHERE is_anomalous = TRUE
  ORDER BY anomaly_severity_score DESC
  LIMIT 100

DatabricksQuery: |
  -- OKTA SWA BULK CREDENTIAL EXTRACTION BEHAVIORAL DETECTION (Databricks)

  WITH swa_access_recent_hourly AS (
      SELECT
          actor:alternateId::string AS admin_email,
          DATE_TRUNC('hour', published) AS event_hour,
          COUNT(*) AS hourly_swa_events,
          COUNT(DISTINCT target[0]:displayName::string) AS hourly_app_diversity,
          COUNT(DISTINCT client:geographicalContext:country::string) AS hourly_country_diversity,
          COUNT(DISTINCT client:ipAddress::string) AS hourly_ip_diversity
      FROM panther_logs.okta_systemlog
      WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '7 days'
          AND eventType = 'user.authentication.sso'
          AND actor:alternateId::string LIKE '%@%'
          AND (
              debugContext:debugData:swa_mode::string = 'true'
              OR debugContext:debugData:signOnMode::string LIKE '%AUTO%LOGIN%'
              OR debugContext:debugData:signOnMode::string LIKE '%BROWSER%PLUGIN%'
          )
      GROUP BY admin_email, event_hour
  ),

  swa_access_recent_stats AS (
      SELECT
          admin_email,
          SUM(hourly_swa_events) AS recent_total_swa_events,
          MAX(hourly_swa_events) AS recent_max_swa_events_per_hour,
          MAX(hourly_app_diversity) AS recent_max_app_diversity_per_hour,
          MAX(hourly_country_diversity) AS recent_max_country_diversity_per_hour,
          MAX(hourly_ip_diversity) AS recent_max_ip_diversity_per_hour,
          AVG(hourly_swa_events) AS recent_avg_swa_events_per_hour,
          MIN(event_hour) AS recent_swa_first_event,
          MAX(event_hour) AS recent_swa_last_event
      FROM swa_access_recent_hourly
      GROUP BY admin_email
  ),

  recent_sources AS (
      SELECT
          actor:alternateId::string AS admin_email,
          COLLECT_SET(client:ipAddress::string) AS recent_ips,
          COLLECT_SET(client:userAgent:rawUserAgent::string) AS recent_user_agents
      FROM panther_logs.okta_systemlog
      WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '7 days'
          AND actor:alternateId::string LIKE '%@%'
          AND (
              (eventType = 'user.authentication.sso'
                  AND (
                      debugContext:debugData:swa_mode::string = 'true'
                      OR debugContext:debugData:signOnMode::string LIKE '%AUTO%LOGIN%'
                      OR debugContext:debugData:signOnMode::string LIKE '%BROWSER%PLUGIN%'
                  ))
              OR eventType = 'application.user_membership.change_username'
          )
      GROUP BY admin_email
  ),

  extraction_by_ip AS (
      SELECT
          actor:alternateId::string AS admin_email,
          client:ipAddress::string AS source_ip,
          COUNT(*) AS extractions_from_ip,
          COUNT(DISTINCT target[0]:alternateId::string) AS victims_from_ip
      FROM panther_logs.okta_systemlog
      WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '7 days'
          AND eventType = 'application.user_membership.change_username'
          AND actor:alternateId::string LIKE '%@%'
      GROUP BY admin_email, source_ip
  ),

  credential_extraction_recent_hourly AS (
      SELECT
          actor:alternateId::string AS admin_email,
          DATE_TRUNC('hour', published) AS event_hour,
          COUNT(*) AS hourly_credential_extractions,
          COUNT(DISTINCT target[0]:alternateId::string) AS hourly_victim_diversity,
          COUNT(DISTINCT client:geographicalContext:country::string) AS hourly_country_diversity
      FROM panther_logs.okta_systemlog
      WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '7 days'
          AND eventType = 'application.user_membership.change_username'
          AND actor:alternateId::string LIKE '%@%'
      GROUP BY admin_email, event_hour
  ),

  credential_extraction_recent_stats AS (
      SELECT
          admin_email,
          SUM(hourly_credential_extractions) AS recent_total_extractions,
          MAX(hourly_credential_extractions) AS recent_max_extractions_per_hour,
          MAX(hourly_victim_diversity) AS recent_max_victim_diversity_per_hour,
          MAX(hourly_country_diversity) AS recent_max_extraction_country_diversity_per_hour,
          AVG(hourly_credential_extractions) AS recent_avg_extractions_per_hour,
          MIN(event_hour) AS recent_extraction_first_event,
          MAX(event_hour) AS recent_extraction_last_event
      FROM credential_extraction_recent_hourly
      GROUP BY admin_email
  ),

  new_ip_extraction_stats AS (
      SELECT
          eip.admin_email,
          SUM(eip.extractions_from_ip) AS new_ip_extraction_count,
          SUM(eip.victims_from_ip) AS new_ip_victim_count
      FROM extraction_by_ip eip
      LEFT JOIN panther_lookups.okta_baseline_90d b ON eip.admin_email = b.user_email
      WHERE b.swa_known_ips IS NULL
          OR NOT ARRAY_CONTAINS(b.swa_known_ips, eip.source_ip)
      GROUP BY eip.admin_email
  ),

  admin_anomalies AS (
      SELECT
          COALESCE(sr.admin_email, cr.admin_email) AS admin_email,
          COALESCE(b.baseline_total_swa_events, 0) AS baseline_total_swa_events,
          COALESCE(b.baseline_active_days_swa, 0) AS baseline_active_days_swa,
          ROUND(COALESCE(b.mean_swa_events_per_hour, 0), 2) AS baseline_mean_swa_events_per_hour,
          ROUND(COALESCE(b.stddev_swa_events_per_hour, 0), 2) AS baseline_stddev_swa_events_per_hour,
          ROUND(COALESCE(b.mean_app_diversity_per_hour, 0), 2) AS baseline_mean_app_diversity_per_hour,
          ROUND(COALESCE(b.stddev_app_diversity_per_hour, 0), 2) AS baseline_stddev_app_diversity_per_hour,
          ROUND(COALESCE(b.mean_country_diversity_per_hour, 0), 2) AS baseline_mean_country_diversity_per_hour,
          COALESCE(sr.recent_total_swa_events, 0) AS recent_total_swa_events,
          COALESCE(sr.recent_max_swa_events_per_hour, 0) AS recent_max_swa_events_per_hour,
          COALESCE(sr.recent_max_app_diversity_per_hour, 0) AS recent_max_app_diversity_per_hour,
          COALESCE(sr.recent_max_country_diversity_per_hour, 0) AS recent_max_country_diversity_per_hour,
          COALESCE(sr.recent_max_ip_diversity_per_hour, 0) AS recent_max_ip_diversity_per_hour,
          ROUND(COALESCE(sr.recent_avg_swa_events_per_hour, 0), 2) AS recent_avg_swa_events_per_hour,
          sr.recent_swa_first_event,
          sr.recent_swa_last_event,
          COALESCE(b.baseline_total_extractions, 0) AS baseline_total_extractions,
          COALESCE(b.baseline_active_days_extraction, 0) AS baseline_active_days_extraction,
          ROUND(COALESCE(b.mean_extractions_per_hour, 0), 2) AS baseline_mean_extractions_per_hour,
          ROUND(COALESCE(b.stddev_extractions_per_hour, 0), 2) AS baseline_stddev_extractions_per_hour,
          ROUND(COALESCE(b.mean_victim_diversity_per_hour, 0), 2) AS baseline_mean_victim_diversity_per_hour,
          ROUND(COALESCE(b.stddev_victim_diversity_per_hour, 0), 2) AS baseline_stddev_victim_diversity_per_hour,
          COALESCE(cr.recent_total_extractions, 0) AS recent_total_extractions,
          COALESCE(cr.recent_max_extractions_per_hour, 0) AS recent_max_extractions_per_hour,
          COALESCE(cr.recent_max_victim_diversity_per_hour, 0) AS recent_max_victim_diversity_per_hour,
          COALESCE(cr.recent_max_extraction_country_diversity_per_hour, 0) AS recent_max_extraction_country_diversity_per_hour,
          ROUND(COALESCE(cr.recent_avg_extractions_per_hour, 0), 2) AS recent_avg_extractions_per_hour,
          cr.recent_extraction_first_event,
          cr.recent_extraction_last_event,
          CASE WHEN b.baseline_total_swa_events >= 3 THEN ROUND((COALESCE(sr.recent_max_swa_events_per_hour, 0) - b.mean_swa_events_per_hour) / NULLIF(b.stddev_swa_events_per_hour, 0), 2) ELSE NULL END AS z_score_swa_volume,
          CASE WHEN b.baseline_total_swa_events >= 3 THEN ROUND((COALESCE(sr.recent_max_app_diversity_per_hour, 0) - b.mean_app_diversity_per_hour) / NULLIF(b.stddev_app_diversity_per_hour, 0), 2) ELSE NULL END AS z_score_app_diversity,
          CASE WHEN b.baseline_total_swa_events >= 3 THEN ROUND((COALESCE(sr.recent_max_country_diversity_per_hour, 0) - b.mean_country_diversity_per_hour) / NULLIF(b.stddev_country_diversity_per_hour, 0), 2) ELSE NULL END AS z_score_swa_country_diversity,
          CASE WHEN b.baseline_total_extractions >= 3 THEN ROUND((COALESCE(cr.recent_max_extractions_per_hour, 0) - b.mean_extractions_per_hour) / NULLIF(b.stddev_extractions_per_hour, 0), 2) ELSE NULL END AS z_score_extraction_volume,
          CASE WHEN b.baseline_total_extractions >= 3 THEN ROUND((COALESCE(cr.recent_max_victim_diversity_per_hour, 0) - b.mean_victim_diversity_per_hour) / NULLIF(b.stddev_victim_diversity_per_hour, 0), 2) ELSE NULL END AS z_score_victim_diversity,
          CASE WHEN rs.admin_email IS NULL THEN FALSE WHEN b.swa_known_ips IS NULL THEN TRUE WHEN SIZE(ARRAY_EXCEPT(COALESCE(rs.recent_ips, ARRAY()), b.swa_known_ips)) > 0 THEN TRUE ELSE FALSE END AS has_new_ip,
          CASE WHEN rs.admin_email IS NULL THEN FALSE WHEN b.swa_known_user_agents IS NULL THEN TRUE WHEN SIZE(ARRAY_EXCEPT(COALESCE(rs.recent_user_agents, ARRAY()), b.swa_known_user_agents)) > 0 THEN TRUE ELSE FALSE END AS has_new_user_agent,
          CASE WHEN rs.admin_email IS NULL THEN 0 WHEN b.swa_known_ips IS NULL THEN SIZE(COALESCE(rs.recent_ips, ARRAY())) ELSE SIZE(ARRAY_EXCEPT(COALESCE(rs.recent_ips, ARRAY()), b.swa_known_ips)) END AS new_ip_count,
          COALESCE(niex.new_ip_extraction_count, 0) AS new_ip_extraction_count,
          COALESCE(niex.new_ip_victim_count, 0) AS new_ip_victim_count,
          CASE WHEN b.baseline_total_swa_events >= 3 AND (COALESCE(sr.recent_max_swa_events_per_hour, 0) - b.mean_swa_events_per_hour) / NULLIF(b.stddev_swa_events_per_hour, 0) > 3 THEN TRUE ELSE FALSE END AS is_swa_volume_anomaly,
          CASE WHEN b.baseline_total_swa_events >= 3 AND (COALESCE(sr.recent_max_app_diversity_per_hour, 0) - b.mean_app_diversity_per_hour) / NULLIF(b.stddev_app_diversity_per_hour, 0) > 3 THEN TRUE ELSE FALSE END AS is_app_diversity_anomaly,
          CASE WHEN b.baseline_total_extractions >= 3 AND (COALESCE(cr.recent_max_extractions_per_hour, 0) - b.mean_extractions_per_hour) / NULLIF(b.stddev_extractions_per_hour, 0) > 3 THEN TRUE ELSE FALSE END AS is_extraction_volume_anomaly,
          CASE WHEN b.baseline_total_extractions >= 3 AND (COALESCE(cr.recent_max_victim_diversity_per_hour, 0) - b.mean_victim_diversity_per_hour) / NULLIF(b.stddev_victim_diversity_per_hour, 0) > 2 THEN TRUE ELSE FALSE END AS is_victim_diversity_anomaly,
          CASE WHEN (b.baseline_total_swa_events IS NULL OR b.baseline_total_swa_events < 3) AND sr.recent_total_swa_events >= 10 THEN TRUE ELSE FALSE END AS is_first_time_bulk_swa_access,
          CASE WHEN (b.baseline_total_extractions IS NULL OR b.baseline_total_extractions < 3) AND cr.recent_total_extractions >= 5 THEN TRUE ELSE FALSE END AS is_first_time_credential_extraction,
          CASE WHEN (
              (b.baseline_total_swa_events >= 3 AND (COALESCE(sr.recent_max_swa_events_per_hour, 0) - b.mean_swa_events_per_hour) / NULLIF(b.stddev_swa_events_per_hour, 0) > 3)
              OR (b.baseline_total_swa_events >= 3 AND (COALESCE(sr.recent_max_app_diversity_per_hour, 0) - b.mean_app_diversity_per_hour) / NULLIF(b.stddev_app_diversity_per_hour, 0) > 3)
              OR (b.baseline_total_extractions >= 3 AND (COALESCE(cr.recent_max_extractions_per_hour, 0) - b.mean_extractions_per_hour) / NULLIF(b.stddev_extractions_per_hour, 0) > 3)
              OR (b.baseline_total_extractions >= 3 AND (COALESCE(cr.recent_max_victim_diversity_per_hour, 0) - b.mean_victim_diversity_per_hour) / NULLIF(b.stddev_victim_diversity_per_hour, 0) > 2)
              OR ((b.baseline_total_swa_events IS NULL OR b.baseline_total_swa_events < 3) AND sr.recent_total_swa_events >= 10)
              OR ((b.baseline_total_extractions IS NULL OR b.baseline_total_extractions < 3) AND cr.recent_total_extractions >= 5)
              OR (rs.admin_email IS NOT NULL AND ((b.swa_known_ips IS NOT NULL AND SIZE(ARRAY_EXCEPT(COALESCE(rs.recent_ips, ARRAY()), b.swa_known_ips)) > 0) OR (b.swa_known_ips IS NULL AND (COALESCE(sr.recent_total_swa_events, 0) >= 3 OR COALESCE(cr.recent_total_extractions, 0) >= 1))) AND (COALESCE(sr.recent_total_swa_events, 0) > 0 OR COALESCE(cr.recent_total_extractions, 0) > 0))
              OR (rs.admin_email IS NOT NULL AND b.swa_known_user_agents IS NOT NULL AND SIZE(ARRAY_EXCEPT(COALESCE(rs.recent_user_agents, ARRAY()), b.swa_known_user_agents)) > 0 AND (COALESCE(sr.recent_total_swa_events, 0) > 0 OR COALESCE(cr.recent_total_extractions, 0) > 0))
          ) THEN TRUE ELSE FALSE END AS is_anomalous,
          CASE
              WHEN b.baseline_total_swa_events >= 3 OR b.baseline_total_extractions >= 3
              THEN ROUND(
                  GREATEST(COALESCE((COALESCE(sr.recent_max_swa_events_per_hour, 0) - b.mean_swa_events_per_hour) / NULLIF(b.stddev_swa_events_per_hour, 0), 0), 0) +
                  GREATEST(COALESCE((COALESCE(sr.recent_max_app_diversity_per_hour, 0) - b.mean_app_diversity_per_hour) / NULLIF(b.stddev_app_diversity_per_hour, 0), 0), 0) +
                  GREATEST(COALESCE((COALESCE(cr.recent_max_extractions_per_hour, 0) - b.mean_extractions_per_hour) / NULLIF(b.stddev_extractions_per_hour, 0), 0) * 3, 0) +
                  GREATEST(COALESCE((COALESCE(cr.recent_max_victim_diversity_per_hour, 0) - b.mean_victim_diversity_per_hour) / NULLIF(b.stddev_victim_diversity_per_hour, 0), 0) * 2, 0) +
                  GREATEST(COALESCE(niex.new_ip_extraction_count, 0) * 5.0, 0),
                  2)
              ELSE ROUND(
                  (COALESCE(sr.recent_total_swa_events, 0) * 0.5) +
                  (COALESCE(cr.recent_total_extractions, 0) * 10) +
                  (COALESCE(cr.recent_max_victim_diversity_per_hour, 0) * 5) +
                  COALESCE(niex.new_ip_extraction_count, 0) * 5.0,
                  2)
          END AS anomaly_severity_score

      FROM swa_access_recent_stats sr
      FULL OUTER JOIN credential_extraction_recent_stats cr ON sr.admin_email = cr.admin_email
      LEFT JOIN panther_lookups.okta_baseline_90d b ON COALESCE(sr.admin_email, cr.admin_email) = b.user_email
      LEFT JOIN recent_sources rs ON COALESCE(sr.admin_email, cr.admin_email) = rs.admin_email
      LEFT JOIN new_ip_extraction_stats niex ON COALESCE(sr.admin_email, cr.admin_email) = niex.admin_email
  )

  SELECT
      admin_email,
      baseline_total_swa_events, baseline_active_days_swa, baseline_mean_swa_events_per_hour, baseline_stddev_swa_events_per_hour,
      baseline_mean_app_diversity_per_hour, baseline_stddev_app_diversity_per_hour, baseline_mean_country_diversity_per_hour,
      recent_total_swa_events, recent_max_swa_events_per_hour, recent_max_app_diversity_per_hour, recent_max_country_diversity_per_hour,
      recent_max_ip_diversity_per_hour, recent_avg_swa_events_per_hour, recent_swa_first_event, recent_swa_last_event,
      baseline_total_extractions, baseline_active_days_extraction, baseline_mean_extractions_per_hour, baseline_stddev_extractions_per_hour,
      baseline_mean_victim_diversity_per_hour, baseline_stddev_victim_diversity_per_hour,
      recent_total_extractions, recent_max_extractions_per_hour, recent_max_victim_diversity_per_hour,
      recent_max_extraction_country_diversity_per_hour, recent_avg_extractions_per_hour,
      recent_extraction_first_event, recent_extraction_last_event,
      z_score_swa_volume, z_score_app_diversity, z_score_swa_country_diversity, z_score_extraction_volume, z_score_victim_diversity,
      is_swa_volume_anomaly, is_app_diversity_anomaly, is_extraction_volume_anomaly, is_victim_diversity_anomaly,
      is_first_time_bulk_swa_access, is_first_time_credential_extraction,
      has_new_ip, has_new_user_agent, new_ip_count, new_ip_extraction_count, new_ip_victim_count,
      is_anomalous, anomaly_severity_score

  FROM admin_anomalies
  WHERE is_anomalous = TRUE
  ORDER BY anomaly_severity_score DESC
  LIMIT 100

Schedule:
  RateMinutes: 1440  # Run once per day
  TimeoutMinutes: 10
Tags:
  - Okta
  - SWA
  - Credential Access
  - Anomaly Detection
  - Statistical Analysis

Stages and Predicates

Stage 1: source

Table
admin_anomalies

Stage 2: filter

  • is_anomalous is TRUE

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
admin_email
baseline_total_swa_events
baseline_active_days_swa
baseline_mean_swa_events_per_hour
baseline_stddev_swa_events_per_hour
baseline_mean_app_diversity_per_hour
baseline_stddev_app_diversity_per_hour
baseline_mean_country_diversity_per_hour
recent_total_swa_events
recent_max_swa_events_per_hour
recent_max_app_diversity_per_hour
recent_max_country_diversity_per_hour
recent_max_ip_diversity_per_hour
recent_avg_swa_events_per_hour
recent_swa_first_event
recent_swa_last_event
baseline_total_extractions
baseline_active_days_extraction
baseline_mean_extractions_per_hour
baseline_stddev_extractions_per_hour
baseline_mean_victim_diversity_per_hour
baseline_stddev_victim_diversity_per_hour
recent_total_extractions
recent_max_extractions_per_hour
recent_max_victim_diversity_per_hour
recent_max_extraction_country_diversity_per_hour
recent_avg_extractions_per_hour
recent_extraction_first_event
recent_extraction_last_event
z_score_swa_volume
z_score_app_diversity
z_score_swa_country_diversity
z_score_extraction_volume
z_score_victim_diversity
is_swa_volume_anomaly
is_app_diversity_anomaly
is_extraction_volume_anomaly
is_victim_diversity_anomaly
is_first_time_bulk_swa_access
is_first_time_credential_extraction
has_new_ip
has_new_user_agent
new_ip_count
new_ip_extraction_count
new_ip_victim_count
is_anomalous
anomaly_severity_score

Query.Okta.SWAOffHoursAccessBehavioral

#
Tags
Okta, SWA, Credential Access, Anomaly Detection, Statistical Analysis
Source
github.com/panther-labs/panther-analysis

Detects Okta SWA credential access during time windows that are unusual for the specific user, using per-user UTC hour slot behavioral analysis. Reads pre-computed 90-day baselines from the okta_baseline_90d lookup table which stores each user's historically active UTC hour-of-week slots. DETECTION LOGIC: - Inactive hour: Recent credential access in UTC hour slots the user is not historically active in - Cold-start: Any credential access when no baseline exists (>= 3 events) - Compound: Geographic shift combined with any credential access UTC hour slots are encoded as DAYOFWEEK * 24 + HOUR (0-167), capturing per-user schedules regardless of timezone. A slot is considered active if the user had activity in it on >= 3 distinct days during the 90-day baseline window. PREREQUISITE: okta_baseline_90d lookup table must be populated.

MITRE ATT&CK coverage

TacticTechniques
Credential AccessNo specific technique

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule specification

AnalysisType: scheduled_query
QueryName: "Query.Okta.SWAOffHoursAccessBehavioral"
Enabled: false
Description: |
  Detects Okta SWA credential access during time windows that are unusual for the specific user,
  using per-user UTC hour slot behavioral analysis. Reads pre-computed 90-day baselines from the
  okta_baseline_90d lookup table which stores each user's historically active UTC hour-of-week slots.

  DETECTION LOGIC:
  - Inactive hour: Recent credential access in UTC hour slots the user is not historically active in
  - Cold-start: Any credential access when no baseline exists (>= 3 events)
  - Compound: Geographic shift combined with any credential access

  UTC hour slots are encoded as DAYOFWEEK * 24 + HOUR (0-167), capturing per-user schedules
  regardless of timezone. A slot is considered active if the user had activity in it on >= 3
  distinct days during the 90-day baseline window.

  PREREQUISITE: okta_baseline_90d lookup table must be populated.
SnowflakeQuery: |
  -- OKTA SWA OFF-HOURS CREDENTIAL ACCESS BEHAVIORAL DETECTION
  -- Reads 90-day baseline from lookup table; scans only last 7 days of raw logs
  -- Flags: activity in UTC hour slots outside the user's historical active schedule

  WITH recent_credential_access AS (
      SELECT
          actor:alternateId::string AS admin_email,
          published,
          target[0]:alternateId::string AS victim_email,
          client:geographicalContext:country::string AS country,
          client:geographicalContext:city::string AS city,
          DATE_TRUNC('hour', published) AS event_hour,
          -- Encode as DAYOFWEEK * 24 + HOUR (0-167) for per-user schedule comparison
          DAYOFWEEK(published) * 24 + HOUR(published) AS utc_hour_slot
      FROM panther_logs.public.okta_systemlog
      WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '7 days'
          AND eventType = 'application.user_membership.change_username'
          AND actor:alternateId::string LIKE '%@%'
  ),

  -- Early join with baseline to classify each event as active or inactive slot for this user
  recent_classified AS (
      SELECT
          r.admin_email,
          r.published,
          r.victim_email,
          r.country,
          r.city,
          r.event_hour,
          r.utc_hour_slot,
          CASE
              WHEN b.active_utc_hour_slots IS NULL THEN NULL  -- no baseline yet
              WHEN ARRAY_CONTAINS(r.utc_hour_slot::VARIANT, b.active_utc_hour_slots) THEN 0
              ELSE 1
          END AS is_inactive_slot
      FROM recent_credential_access r
      LEFT JOIN panther_lookups.public.okta_baseline_90d b ON r.admin_email = b.user_email
  ),

  recent_hourly AS (
      SELECT
          admin_email,
          event_hour,
          COUNT(*) AS hourly_credential_access,
          SUM(CASE WHEN is_inactive_slot = 1 THEN 1 ELSE 0 END) AS hourly_inactive_slot_events,
          COUNT(DISTINCT victim_email) AS hourly_victim_diversity,
          COUNT(DISTINCT country) AS hourly_country_diversity
      FROM recent_classified
      GROUP BY admin_email, event_hour
  ),

  recent_stats AS (
      SELECT
          admin_email,
          SUM(hourly_credential_access) AS recent_total_credential_access,
          COUNT(DISTINCT DATE(event_hour)) AS recent_active_days,
          MAX(hourly_credential_access) AS recent_max_per_hour,
          AVG(hourly_credential_access)::FLOAT AS recent_avg_per_hour,
          SUM(hourly_inactive_slot_events) AS recent_inactive_slot_events,
          SUM(hourly_inactive_slot_events)::FLOAT /
              NULLIF(SUM(hourly_credential_access), 0) AS recent_inactive_slot_ratio,
          AVG(hourly_inactive_slot_events::FLOAT / NULLIF(hourly_credential_access, 0)) AS recent_avg_inactive_slot_ratio_per_hour,
          MAX(hourly_victim_diversity) AS recent_max_victim_diversity_per_hour,
          MIN(event_hour) AS recent_first_event,
          MAX(event_hour) AS recent_last_event
      FROM recent_hourly
      GROUP BY admin_email
  ),

  recent_geo AS (
      SELECT
          admin_email,
          COUNT(DISTINCT country) AS recent_country_diversity,
          COUNT(DISTINCT city) AS recent_city_diversity,
          MODE(country) AS recent_primary_country,
          MODE(city) AS recent_primary_city
      FROM recent_credential_access
      GROUP BY admin_email
  ),

  admin_anomalies AS (
      SELECT
          r.admin_email,

          -- BASELINE from lookup table
          COALESCE(b.baseline_total_credential_access, 0) AS baseline_total_credential_access,
          COALESCE(b.baseline_active_days, 0) AS baseline_active_days,
          COALESCE(b.baseline_hours_with_activity, 0) AS baseline_hours_with_activity,
          ROUND(COALESCE(b.mean_credential_access_per_hour, 0), 2) AS baseline_mean_credential_access_per_hour,
          ROUND(COALESCE(b.stddev_credential_access_per_hour, 0), 2) AS baseline_stddev_credential_access_per_hour,
          b.active_utc_hour_slots,
          COALESCE(b.active_slot_count, 0) AS baseline_active_slot_count,

          -- Baseline geographic from lookup table
          b.primary_country AS baseline_primary_country,
          b.primary_city AS baseline_primary_city,

          -- RECENT ACTIVITY
          COALESCE(r.recent_total_credential_access, 0) AS recent_total_credential_access,
          COALESCE(r.recent_active_days, 0) AS recent_active_days,
          COALESCE(r.recent_max_per_hour, 0) AS recent_max_per_hour,
          ROUND(COALESCE(r.recent_avg_per_hour, 0), 2) AS recent_avg_per_hour,
          COALESCE(r.recent_inactive_slot_events, 0) AS recent_inactive_slot_events,
          ROUND(COALESCE(r.recent_inactive_slot_ratio, 0), 4) AS recent_inactive_slot_ratio,
          ROUND(COALESCE(r.recent_avg_inactive_slot_ratio_per_hour, 0), 4) AS recent_avg_inactive_slot_ratio_per_hour,
          COALESCE(rg.recent_country_diversity, 0) AS recent_country_diversity,
          COALESCE(rg.recent_city_diversity, 0) AS recent_city_diversity,
          rg.recent_primary_country,
          rg.recent_primary_city,
          r.recent_first_event,
          r.recent_last_event,

          -- Z-SCORE: inactive-slot ratio vs baseline distribution
          ROUND(
              (COALESCE(r.recent_avg_inactive_slot_ratio_per_hour, 0) - b.mean_inactive_slot_ratio_per_hour) /
              NULLIF(b.stddev_inactive_slot_ratio_per_hour, 0),
              2
          ) AS z_score_inactive_slot_ratio,

          -- GEOGRAPHIC SHIFT FLAG
          CASE
              WHEN b.primary_country IS NOT NULL
                  AND rg.recent_primary_country IS NOT NULL
                  AND b.primary_country != rg.recent_primary_country
              THEN TRUE
              ELSE FALSE
          END AS is_geographic_shift,

          -- INACTIVE HOUR FLAG: inactive-slot ratio is statistically anomalous (z > 3)
          CASE
              WHEN b.active_utc_hour_slots IS NOT NULL
                  AND b.stddev_inactive_slot_ratio_per_hour IS NOT NULL
                  AND (COALESCE(r.recent_avg_inactive_slot_ratio_per_hour, 0) - b.mean_inactive_slot_ratio_per_hour) /
                      NULLIF(b.stddev_inactive_slot_ratio_per_hour, 0) > 3
              THEN TRUE ELSE FALSE
          END AS is_inactive_hour_anomaly,

          -- COLD START FLAG: no baseline exists yet
          CASE
              WHEN b.active_utc_hour_slots IS NULL
                  AND r.recent_total_credential_access >= 3
              THEN TRUE ELSE FALSE
          END AS is_cold_start,

          -- OVERALL ANOMALY FLAG
          CASE
              WHEN (
                  -- Inactive-slot ratio is statistically anomalous (z > 3)
                  (b.active_utc_hour_slots IS NOT NULL
                      AND b.stddev_inactive_slot_ratio_per_hour IS NOT NULL
                      AND (COALESCE(r.recent_avg_inactive_slot_ratio_per_hour, 0) - b.mean_inactive_slot_ratio_per_hour) /
                          NULLIF(b.stddev_inactive_slot_ratio_per_hour, 0) > 3)
                  OR
                  -- Cold start: credential access with no baseline
                  (b.active_utc_hour_slots IS NULL
                      AND r.recent_total_credential_access >= 3)
                  OR
                  -- Geographic shift with any credential access
                  (b.primary_country IS NOT NULL
                      AND rg.recent_primary_country IS NOT NULL
                      AND b.primary_country != rg.recent_primary_country)
              ) THEN TRUE
              ELSE FALSE
          END AS is_anomalous,

          -- ANOMALY SEVERITY SCORE
          ROUND(
              -- Weight inactive slot events (more events in unusual hours = higher score)
              COALESCE(r.recent_inactive_slot_events, 0) * 2 +
              -- Geographic shift adds flat weight
              CASE
                  WHEN b.primary_country IS NOT NULL
                      AND rg.recent_primary_country IS NOT NULL
                      AND b.primary_country != rg.recent_primary_country THEN 5
                  ELSE 0
              END +
              -- Cold start: weight by total events
              CASE
                  WHEN b.active_utc_hour_slots IS NULL
                  THEN COALESCE(r.recent_total_credential_access, 0) * 0.5
                  ELSE 0
              END,
              2
          ) AS anomaly_severity_score

      FROM recent_stats r
      LEFT JOIN recent_geo rg ON r.admin_email = rg.admin_email
      LEFT JOIN panther_lookups.public.okta_baseline_90d b ON r.admin_email = b.user_email
  )

  SELECT
      admin_email,

      -- Baseline context
      baseline_total_credential_access,
      baseline_active_days,
      baseline_hours_with_activity,
      baseline_mean_credential_access_per_hour,
      baseline_stddev_credential_access_per_hour,
      baseline_active_slot_count,
      baseline_primary_country,
      baseline_primary_city,

      -- Recent activity
      recent_total_credential_access,
      recent_active_days,
      recent_max_per_hour,
      recent_avg_per_hour,
      recent_inactive_slot_events,
      recent_inactive_slot_ratio,
      recent_avg_inactive_slot_ratio_per_hour,
      z_score_inactive_slot_ratio,
      recent_country_diversity,
      recent_city_diversity,
      recent_primary_country,
      recent_primary_city,
      recent_first_event,
      recent_last_event,

      -- Anomaly flags
      is_inactive_hour_anomaly,
      is_cold_start,
      is_geographic_shift,
      is_anomalous,
      anomaly_severity_score

  FROM admin_anomalies
  WHERE is_anomalous = TRUE
  ORDER BY anomaly_severity_score DESC
  LIMIT 100

DatabricksQuery: |
  -- OKTA SWA OFF-HOURS CREDENTIAL ACCESS BEHAVIORAL DETECTION (Databricks)

  WITH recent_credential_access AS (
      SELECT
          actor:alternateId::string AS admin_email,
          published,
          target[0]:alternateId::string AS victim_email,
          client:geographicalContext:country::string AS country,
          client:geographicalContext:city::string AS city,
          DATE_TRUNC('hour', published) AS event_hour,
          (DAYOFWEEK(published) - 1) * 24 + HOUR(published) AS utc_hour_slot
      FROM panther_logs.okta_systemlog
      WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '7 days'
          AND eventType = 'application.user_membership.change_username'
          AND actor:alternateId::string LIKE '%@%'
  ),

  recent_classified AS (
      SELECT
          r.admin_email,
          r.published,
          r.victim_email,
          r.country,
          r.city,
          r.event_hour,
          r.utc_hour_slot,
          CASE
              WHEN b.active_utc_hour_slots IS NULL THEN NULL
              WHEN ARRAY_CONTAINS(b.active_utc_hour_slots, r.utc_hour_slot) THEN 0
              ELSE 1
          END AS is_inactive_slot
      FROM recent_credential_access r
      LEFT JOIN panther_lookups.okta_baseline_90d b ON r.admin_email = b.user_email
  ),

  recent_hourly AS (
      SELECT
          admin_email,
          event_hour,
          COUNT(*) AS hourly_credential_access,
          SUM(CASE WHEN is_inactive_slot = 1 THEN 1 ELSE 0 END) AS hourly_inactive_slot_events,
          COUNT(DISTINCT victim_email) AS hourly_victim_diversity,
          COUNT(DISTINCT country) AS hourly_country_diversity
      FROM recent_classified
      GROUP BY admin_email, event_hour
  ),

  recent_stats AS (
      SELECT
          admin_email,
          SUM(hourly_credential_access) AS recent_total_credential_access,
          COUNT(DISTINCT CAST(event_hour AS DATE)) AS recent_active_days,
          MAX(hourly_credential_access) AS recent_max_per_hour,
          AVG(hourly_credential_access) AS recent_avg_per_hour,
          SUM(hourly_inactive_slot_events) AS recent_inactive_slot_events,
          SUM(hourly_inactive_slot_events) / NULLIF(SUM(hourly_credential_access), 0) AS recent_inactive_slot_ratio,
          AVG(hourly_inactive_slot_events / NULLIF(CAST(hourly_credential_access AS DOUBLE), 0)) AS recent_avg_inactive_slot_ratio_per_hour,
          MAX(hourly_victim_diversity) AS recent_max_victim_diversity_per_hour,
          MIN(event_hour) AS recent_first_event,
          MAX(event_hour) AS recent_last_event
      FROM recent_hourly
      GROUP BY admin_email
  ),

  recent_geo AS (
      SELECT
          admin_email,
          COUNT(DISTINCT country) AS recent_country_diversity,
          COUNT(DISTINCT city) AS recent_city_diversity,
          mode(country) AS recent_primary_country,
          mode(city) AS recent_primary_city
      FROM recent_credential_access
      GROUP BY admin_email
  ),

  admin_anomalies AS (
      SELECT
          r.admin_email,
          COALESCE(b.baseline_total_credential_access, 0) AS baseline_total_credential_access,
          COALESCE(b.baseline_active_days, 0) AS baseline_active_days,
          COALESCE(b.baseline_hours_with_activity, 0) AS baseline_hours_with_activity,
          ROUND(COALESCE(b.mean_credential_access_per_hour, 0), 2) AS baseline_mean_credential_access_per_hour,
          ROUND(COALESCE(b.stddev_credential_access_per_hour, 0), 2) AS baseline_stddev_credential_access_per_hour,
          b.active_utc_hour_slots,
          COALESCE(b.active_slot_count, 0) AS baseline_active_slot_count,
          b.primary_country AS baseline_primary_country,
          b.primary_city AS baseline_primary_city,
          COALESCE(r.recent_total_credential_access, 0) AS recent_total_credential_access,
          COALESCE(r.recent_active_days, 0) AS recent_active_days,
          COALESCE(r.recent_max_per_hour, 0) AS recent_max_per_hour,
          ROUND(COALESCE(r.recent_avg_per_hour, 0), 2) AS recent_avg_per_hour,
          COALESCE(r.recent_inactive_slot_events, 0) AS recent_inactive_slot_events,
          ROUND(COALESCE(r.recent_inactive_slot_ratio, 0), 4) AS recent_inactive_slot_ratio,
          ROUND(COALESCE(r.recent_avg_inactive_slot_ratio_per_hour, 0), 4) AS recent_avg_inactive_slot_ratio_per_hour,
          COALESCE(rg.recent_country_diversity, 0) AS recent_country_diversity,
          COALESCE(rg.recent_city_diversity, 0) AS recent_city_diversity,
          rg.recent_primary_country,
          rg.recent_primary_city,
          r.recent_first_event,
          r.recent_last_event,
          ROUND(
              (COALESCE(r.recent_avg_inactive_slot_ratio_per_hour, 0) - b.mean_inactive_slot_ratio_per_hour) /
              NULLIF(b.stddev_inactive_slot_ratio_per_hour, 0),
              2
          ) AS z_score_inactive_slot_ratio,
          CASE
              WHEN b.primary_country IS NOT NULL AND rg.recent_primary_country IS NOT NULL
                  AND b.primary_country != rg.recent_primary_country
              THEN TRUE ELSE FALSE
          END AS is_geographic_shift,
          CASE
              WHEN b.active_utc_hour_slots IS NOT NULL
                  AND b.stddev_inactive_slot_ratio_per_hour IS NOT NULL
                  AND (COALESCE(r.recent_avg_inactive_slot_ratio_per_hour, 0) - b.mean_inactive_slot_ratio_per_hour) /
                      NULLIF(b.stddev_inactive_slot_ratio_per_hour, 0) > 3
              THEN TRUE ELSE FALSE
          END AS is_inactive_hour_anomaly,
          CASE
              WHEN b.active_utc_hour_slots IS NULL AND r.recent_total_credential_access >= 3
              THEN TRUE ELSE FALSE
          END AS is_cold_start,
          CASE
              WHEN (
                  (b.active_utc_hour_slots IS NOT NULL AND b.stddev_inactive_slot_ratio_per_hour IS NOT NULL
                      AND (COALESCE(r.recent_avg_inactive_slot_ratio_per_hour, 0) - b.mean_inactive_slot_ratio_per_hour) /
                          NULLIF(b.stddev_inactive_slot_ratio_per_hour, 0) > 3)
                  OR (b.active_utc_hour_slots IS NULL AND r.recent_total_credential_access >= 3)
                  OR (b.primary_country IS NOT NULL AND rg.recent_primary_country IS NOT NULL
                      AND b.primary_country != rg.recent_primary_country)
              ) THEN TRUE ELSE FALSE
          END AS is_anomalous,
          ROUND(
              COALESCE(r.recent_inactive_slot_events, 0) * 2 +
              CASE WHEN b.primary_country IS NOT NULL AND rg.recent_primary_country IS NOT NULL
                  AND b.primary_country != rg.recent_primary_country THEN 5 ELSE 0 END +
              CASE WHEN b.active_utc_hour_slots IS NULL
                  THEN COALESCE(r.recent_total_credential_access, 0) * 0.5 ELSE 0 END,
              2
          ) AS anomaly_severity_score

      FROM recent_stats r
      LEFT JOIN recent_geo rg ON r.admin_email = rg.admin_email
      LEFT JOIN panther_lookups.okta_baseline_90d b ON r.admin_email = b.user_email
  )

  SELECT
      admin_email,
      baseline_total_credential_access, baseline_active_days, baseline_hours_with_activity,
      baseline_mean_credential_access_per_hour, baseline_stddev_credential_access_per_hour,
      baseline_active_slot_count, baseline_primary_country, baseline_primary_city,
      recent_total_credential_access, recent_active_days, recent_max_per_hour, recent_avg_per_hour,
      recent_inactive_slot_events, recent_inactive_slot_ratio, recent_avg_inactive_slot_ratio_per_hour,
      z_score_inactive_slot_ratio,
      recent_country_diversity, recent_city_diversity, recent_primary_country, recent_primary_city,
      recent_first_event, recent_last_event,
      is_inactive_hour_anomaly, is_cold_start, is_geographic_shift,
      is_anomalous, anomaly_severity_score

  FROM admin_anomalies
  WHERE is_anomalous = TRUE
  ORDER BY anomaly_severity_score DESC
  LIMIT 100

Schedule:
  RateMinutes: 1440  # Run once per day
  TimeoutMinutes: 10
Tags:
  - Okta
  - SWA
  - Credential Access
  - Anomaly Detection
  - Statistical Analysis

Stages and Predicates

Stage 1: source

Table
admin_anomalies

Stage 2: filter

  • is_anomalous is TRUE

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
admin_email
baseline_total_credential_access
baseline_active_days
baseline_hours_with_activity
baseline_mean_credential_access_per_hour
baseline_stddev_credential_access_per_hour
baseline_active_slot_count
baseline_primary_country
baseline_primary_city
recent_total_credential_access
recent_active_days
recent_max_per_hour
recent_avg_per_hour
recent_inactive_slot_events
recent_inactive_slot_ratio
recent_avg_inactive_slot_ratio_per_hour
z_score_inactive_slot_ratio
recent_country_diversity
recent_city_diversity
recent_primary_country
recent_primary_city
recent_first_event
recent_last_event
is_inactive_hour_anomaly
is_cold_start
is_geographic_shift
is_anomalous
anomaly_severity_score

SIGNAL - Okta SSO to AWS

#
Severity
informational
Log types
Okta.SystemLog
Source
github.com/panther-labs/panther-analysis

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

def rule(event):
    return all(
        [
            event.get("eventType") == "user.authentication.sso",
            event.deep_get("outcome", "result") == "SUCCESS",
            "AWS IAM Identity Center" in event.deep_walk("target", "displayName", default=[]),
        ]
    )


def alert_context(event):
    return {
        "actor": event.deep_get("actor", "alternateId", default="").split("@")[0],
    }

Rule specification

AnalysisType: rule
Filename: okta_sso_to_aws.py
RuleID: "Okta.SSO.to.AWS"
DisplayName: "SIGNAL - Okta SSO to AWS"
Enabled: true
CreateAlert: false
LogTypes:
    - Okta.SystemLog
Severity: Info
DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on Okta.SystemLog events when all of the conditions below hold.

Condition

  • eventType is user.authentication.sso
  • outcome.result is SUCCESS
  • target.displayName contains AWS IAM Identity Center

Indicators

These rows show field, operator, and value matches.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "displayMessage": "User single sign on to app",
  "eventType": "user.authentication.sso",
  "legacyEventType": "app.auth.sso",
  "outcome": {
    "result": "SUCCESS"
  },
  "securityContext": {},
  "severity": "INFO",
  "target": [
    {
      "alternateId": "AWS Production",
      "detailEntry": {
        "signOnModeType": "SAML_2_0"
      },
      "displayName": "AWS IAM Identity Center",
      "id": "0oaua5ldoougycQAO696",
      "type": "AppInstance"
    },
    {
      "alternateId": "aardvark",
      "displayName": "aardvark",
      "id": "0ua8aardvarkD697",
      "type": "AppUser"
    }
  ],
  "transaction": {
    "detail": {},
    "id": "1a3852fc0d172ecdad0e2447e47fbc98",
    "type": "WEB"
  },
  "uuid": "35cae732-21bd-11ef-a011-dd05aa53a11a",
  "version": "0"
}