Detection rules › Panther
Panther rules: okta
Okta AD Agent Authentication Anomaly - Z-Score Detection
#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
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type user.authentication.auth_via_AD_agent: Authenticate user with AD agent. |
Rules detecting the same action
These rules filter on the same operation.
- Query.Okta.ADAgentAuthZScoreAnomaly (Panther)
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_emailis present
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
user_email | is_not_null | field:"user_email" kind:is_not_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
user_email | |
baseline_total_events | |
baseline_active_days | |
baseline_mean_events_per_hour | |
baseline_mean_ip_diversity | baseline_mean_ip_diversity_per_hour |
baseline_mean_country_diversity | baseline_mean_country_diversity_per_hour |
recent_total_events | |
recent_max_events_per_hour | |
recent_max_ip_diversity | recent_max_ip_diversity_per_hour |
recent_max_country_diversity | recent_max_country_diversity_per_hour |
recent_max_device_diversity | recent_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_addresses | all_recent_ips |
recent_countries | all_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
#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
| Tactic | Techniques |
|---|---|
| Persistence | |
| Credential Access |
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- Attempt to Create Okta API Token (Elastic)
- High-Risk Admin Activity (Kusto)
- New Device/Location sign-in along with critical operation (Kusto)
- Okta API Key Created (Panther)
- Okta API Token Created (Sigma)
- Okta New API Token Created (YARA-L)
- Okta New API Token Created (Splunk)
- Query.Okta.ADAgentTokenAbuseBehavioral (Panther)
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
actorIdis present
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
actorId | is_not_null | field:"actorId" kind:is_not_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
actor_id | actorId |
actor_name | actorName |
event_type | eventType |
source_ip | sourceIP |
user_agent | userAgent |
anomaly_type | |
target | |
event_time | p_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.
Audit instances of admin access granted in your okta tenant
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- Administrator Privileges Assigned to an Okta Group (Elastic)
- High-Risk Admin Activity (Kusto)
- Okta Admin Role Assigned (Panther)
- Okta Admin Role Assigned to an User or Group (Sigma)
- Okta Group Admin Role Assigned (Panther)
- Okta User Assigned Administrator Role (Elastic)
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
Stage 2: filter
any of:
eventTypeisuser.account.privilege.grantall of:
eventTypeisgroup.privilege.grantdebugContext:debugData:privilegeGrantedmatches 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.
| Field | Kind | Values | Search |
|---|---|---|---|
debugContext:debugData:privilegeGranted | wildcard |
| field:"debugContext:debugData:privilegeGranted" kind:wildcard value:"*Admin*" |
eventType | eq |
| field:"eventType" kind:eq |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
event_time | p_event_time |
actor_email | actor:alternateId |
actor_name | actor:displayName |
displayMessage | |
eventType | |
priv_granted | debugContext:debugData:privilegeGranted |
target_name | target |
src_ip | client:ipAddress |
city | client:geographicalContext:city |
country | client:geographicalContext:country |
user_agent | client:userAgent:rawUserAgent |
Okta Admin Role Assigned
#A user has been granted administrative privileges in Okta
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type user.account.privilege.grant: User admin privileges changed |
Rules detecting the same action
These rules filter on the same operation.
- High-Risk Admin Activity (Kusto)
- Okta Admin Access Granted (Panther)
- Okta Admin Role Assigned to an User or Group (Sigma)
- Okta User Assigned Administrator Role (Elastic)
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
eventTypeisuser.account.privilege.grantoutcome.resultisSUCCESS
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.
| Field | Kind | Values | Search |
|---|---|---|---|
eventType | eq |
| field:"eventType" kind:eq value:"user.account.privilege.grant" |
outcome.result | eq |
| field:"outcome.result" kind:eq value:"SUCCESS" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
event_type | eventtype |
severity | |
actor | |
client | |
request | |
outcome | |
target | |
debug_context | debugcontext |
authentication_context | authenticationcontext |
security_context | securitycontext |
ips | p_any_ip_addresses |
displayName | actor.displayName |
alternateId | actor.alternateId |
privilegeGranted | debugContext.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
#Okta FastPass detected a user targeted by attackers wielding real-time (AiTM) proxies.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence | |
| Credential Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type user.authentication.auth_via_mfa: User authenticated via MFA |
Rules detecting the same action
These rules filter on the same operation.
- First Occurrence of Okta User Session Started via Proxy (Elastic)
- MFA Fatigue (OKTA) (Kusto)
- Okta Authentication Failed During MFA Challenge (Splunk)
- Okta Fast Pass phishing Detection (Kusto)
- Okta FastPass Phishing Detection (Sigma)
- Okta FastPass Phishing Detection (Elastic)
- Okta MFA Bruteforce Attack (YARA-L)
- Okta Mismatch Between Source And Response For Verify Push Request (YARA-L)
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
eventTypeisuser.authentication.auth_via_mfaoutcome.resultisFAILUREoutcome.reasonisFastPass declined phishing attempt
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
eventType | eq |
| field:"eventType" kind:eq value:"user.authentication.auth_via_mfa" |
outcome.reason | eq |
| field:"outcome.reason" kind:eq value:"FastPass declined phishing attempt" |
outcome.result | eq |
| field:"outcome.result" kind:eq value:"FAILURE" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
event_type | eventtype |
severity | |
actor | |
client | |
request | |
outcome | |
target | |
debug_context | debugcontext |
authentication_context | authenticationcontext |
security_context | securitycontext |
ips | p_any_ip_addresses |
displayName | actor.displayName |
alternateId | actor.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
#A user created an API Key in Okta
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type system.api_token.create: API token created |
Rules detecting the same action
These rules filter on the same operation.
- Attempt to Create Okta API Token (Elastic)
- High-Risk Admin Activity (Kusto)
- New Device/Location sign-in along with critical operation (Kusto)
- Okta AD Agent Token Abuse - Behavioral (Panther)
- Okta API Token Created (Sigma)
- Okta New API Token Created (YARA-L)
- Okta New API Token Created (Splunk)
- Query.Okta.ADAgentTokenAbuseBehavioral (Panther)
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
eventTypeissystem.api_token.createoutcome.resultisSUCCESS
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
eventType | eq |
| field:"eventType" kind:eq value:"system.api_token.create" |
outcome.result | eq |
| field:"outcome.result" kind:eq value:"SUCCESS" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
event_type | eventtype |
severity | |
actor | |
client | |
request | |
outcome | |
target | |
debug_context | debugcontext |
authentication_context | authenticationcontext |
security_context | securitycontext |
ips | p_any_ip_addresses |
displayName | actor.displayName |
alternateId | actor.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
#A user has revoked an API Key in Okta
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type system.api_token.revoke: API token revoked |
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
eventTypeissystem.api_token.revokeoutcome.resultisSUCCESS
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
eventType | eq |
| field:"eventType" kind:eq value:"system.api_token.revoke" |
outcome.result | eq |
| field:"outcome.result" kind:eq value:"SUCCESS" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
event_type | eventtype |
severity | |
actor | |
client | |
request | |
outcome | |
target | |
debug_context | debugcontext |
authentication_context | authenticationcontext |
security_context | securitycontext |
ips | p_any_ip_addresses |
displayName | actor.displayName |
alternateId | actor.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
#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
eventtypeis one ofapp.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.
| Field | Source |
|---|---|
event_type | eventtype |
severity | |
actor | |
client | |
request | |
outcome | |
target | |
debug_context | debugcontext |
authentication_context | authenticationcontext |
security_context | securitycontext |
ips | p_any_ip_addresses |
ipAddress | client.ipAddress |
displayName | actor.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 Authentication Bypass via Skeleton Key Injection - Behavioral
#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
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- Attempt to Modify an Okta Policy (Elastic)
- Attempt to Modify an Okta Policy Rule (Elastic)
- MFA Deactivation with no Re-Activation for Okta User Account (Elastic)
- New Device/Location sign-in along with critical operation (Kusto)
- Okta Policy Modified or Deleted (Sigma)
- Okta Policy Rule Modified or Deleted (Sigma)
- Query.Okta.SkeletonKeyBypassBehavioral (Panther)
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_emailis present
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
admin_email | is_not_null | 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
#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
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type application.lifecycle.update: Application updated |
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
eventTypeisapplication.lifecycle.updateoutcome.reasoncontainsPushing user passwords
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
eventType | eq |
| field:"eventType" kind:eq value:"application.lifecycle.update" |
outcome.reason | contains |
| field:"outcome.reason" kind:contains value:"Pushing user passwords" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
event_type | eventtype |
severity | |
actor | |
client | |
request | |
outcome | |
target | |
debug_context | debugcontext |
authentication_context | authenticationcontext |
security_context | securitycontext |
ips | p_any_ip_addresses |
displayName | actor.displayName |
alternateId | actor.alternateId |
alternateId | target.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
#Detect when an admin role is assigned to a group
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type group.privilege.grant: Group admin privilege granted |
Rules detecting the same action
These rules filter on the same operation.
- Administrator Privileges Assigned to an Okta Group (Elastic)
- High-Risk Admin Activity (Kusto)
- Okta Admin Access Granted (Panther)
- Okta Admin Role Assigned to an User or Group (Sigma)
- Okta User Assigned Administrator Role (Elastic)
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
eventtypeisgroup.privilege.grant
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
event_type | eventtype |
severity | |
actor | |
client | |
request | |
outcome | |
target | |
debug_context | debugcontext |
authentication_context | authenticationcontext |
security_context | securitycontext |
ips | p_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
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
#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
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence | |
| Credential Access |
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
eventTypecontainssystem.idp.lifecycle
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
eventType | contains |
| field:"eventType" kind:contains value:"system.idp.lifecycle" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
event_type | eventtype |
severity | |
actor | |
client | |
request | |
outcome | |
target | |
debug_context | debugcontext |
authentication_context | authenticationcontext |
security_context | securitycontext |
ips | p_any_ip_addresses |
displayName | actor.displayName |
alternateId | actor.alternateId |
displayName | target.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
#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
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type user.authentication.auth_via_IDP: Authenticate user via IDP. |
Rules detecting the same action
These rules filter on the same operation.
- Okta Sign-In Events via Third-Party IdP (Elastic)
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
eventTypeisuser.authentication.auth_via_IDP
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
eventType | eq |
| 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.
| Field | Source |
|---|---|
event_type | eventtype |
severity | |
actor | |
client | |
request | |
outcome | |
target | |
debug_context | debugcontext |
authentication_context | authenticationcontext |
security_context | securitycontext |
ips | p_any_ip_addresses |
displayName | actor.displayName |
alternateId | actor.alternateId |
displayName | target.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.
Investigate Password and MFA resets for the last 7 days
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- Attempt to Reset MFA Factors for an Okta User Account (Elastic)
- High-Risk Admin Activity (Kusto)
- MFA Deactivation with no Re-Activation for Okta User Account (Elastic)
- New Device/Location sign-in along with critical operation (Kusto)
- Okta MFA Reset or Deactivated (Sigma)
- Okta Multi-Factor Authentication Disabled (Splunk)
- Okta Support Reset Credential (Panther)
- Okta User MFA Factor Suspend (Panther)
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
Stage 2: filter
eventTypeis one ofuser.mfa.factor.reset_all,user.mfa.factor.deactivate,user.mfa.factor.suspend,user.account.reset_password,user.account.update_password
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
eventType | in |
| field:"eventType" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
p_event_time | |
actor_user | actor:alternateId |
target_user | target [ 0 ] : alternateId |
eventType | |
ip_address | client: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.
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
Stage 2: filter
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
event_time | p_event_time |
actor_email | actor:alternateId |
actor_name | actor:displayName |
sessionId | authenticationContext:externalSessionId |
displayMessage | |
eventType | |
src_ip | client:ipAddress |
city | client:geographicalContext:city |
country | client:geographicalContext:country |
user_agent | client: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.
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
Stage 2: filter
actor:typeisUser
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
actor:type | eq |
| field:"actor:type" kind:eq value:"User" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
actor_name | actor:displayName |
actor_email | actor:alternateId |
eventType | |
activity_count | COUNT ( * ) |
Okta Login From CrowdStrike Unmanaged Device
#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
| Platform | Record / event type |
|---|---|
| Okta | System Log event type user.session.start: User logged in to Okta |
Rules detecting the same action
These rules filter on the same operation.
- Failed Logins from Unknown or Invalid User (Kusto)
- First Occurrence of Okta User Session Started via Proxy (Elastic)
- High-Risk Admin Activity (Kusto)
- Multiple Okta Sessions Detected for a Single User (Elastic)
- Multiple Okta User Authentication Events with Same Device Token Hash (Elastic)
- New Device/Location sign-in along with critical operation (Kusto)
- Okta AiTM Session Cookie Replay (Elastic)
- Okta Login From CrowdStrike Unmanaged Device (Panther)
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.
| Field | Source |
|---|---|
alternateId | actor.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
#Okta Logins from an IP Address not found in CrowdStrike's AIP List
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type user.session.start: User logged in to Okta |
Rules detecting the same action
These rules filter on the same operation.
- Failed Logins from Unknown or Invalid User (Kusto)
- First Occurrence of Okta User Session Started via Proxy (Elastic)
- High-Risk Admin Activity (Kusto)
- Multiple Okta Sessions Detected for a Single User (Elastic)
- Multiple Okta User Authentication Events with Same Device Token Hash (Elastic)
- New Device/Location sign-in along with critical operation (Kusto)
- Okta AiTM Session Cookie Replay (Elastic)
- Okta Login From CrowdStrike Unmanaged Device (Panther)
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
Stage 2: filter
eventtypeisuser.session.startoutcome:resultisSUCCESSclient:deviceisComputerclient:ipAddressmatches the pattern*.*.*.*client:ipAddressis not in the results of a subquery onpanther_logs.public.crowdstrike_aidmaster
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
client:device | eq |
| field:"client:device" kind:eq value:"Computer" |
client:ipAddress | wildcard |
| field:"client:ipAddress" kind:wildcard value:"*.*.*.*" |
outcome:result | eq |
| field:"outcome:result" kind:eq value:"SUCCESS" |
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)
#Okta Logins from an IP Address not found in CrowdStrike's AIP List (crowdstrike_fdrevent table)
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type user.session.start: User logged in to Okta |
Rules detecting the same action
These rules filter on the same operation.
- Failed Logins from Unknown or Invalid User (Kusto)
- First Occurrence of Okta User Session Started via Proxy (Elastic)
- High-Risk Admin Activity (Kusto)
- Multiple Okta Sessions Detected for a Single User (Elastic)
- Multiple Okta User Authentication Events with Same Device Token Hash (Elastic)
- New Device/Location sign-in along with critical operation (Kusto)
- Okta AiTM Session Cookie Replay (Elastic)
- Okta Login From CrowdStrike Unmanaged Device (Panther)
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
Stage 2: filter
eventtypeisuser.session.startoutcome:resultisSUCCESSclient:deviceisComputerclient:ipAddressmatches the pattern*.*.*.*client:ipAddressis not in the results of a subquery onpanther_logs.public.crowdstrike_fdrevent
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
client:device | eq |
| field:"client:device" kind:eq value:"Computer" |
client:ipAddress | wildcard |
| field:"client:ipAddress" kind:wildcard value:"*.*.*.*" |
outcome:result | eq |
| field:"outcome:result" kind:eq value:"SUCCESS" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
* |
Okta Login Signal
#Telemetry coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type user.session.start: User logged in to Okta |
Rules detecting the same action
These rules filter on the same operation.
- Failed Logins from Unknown or Invalid User (Kusto)
- First Occurrence of Okta User Session Started via Proxy (Elastic)
- High-Risk Admin Activity (Kusto)
- Multiple Okta Sessions Detected for a Single User (Elastic)
- Multiple Okta User Authentication Events with Same Device Token Hash (Elastic)
- New Device/Location sign-in along with critical operation (Kusto)
- Okta AiTM Session Cookie Replay (Elastic)
- Okta Login From CrowdStrike Unmanaged Device (Panther)
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
eventTypeisuser.session.startoutcome.resultisSUCCESS
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
eventType | eq |
| field:"eventType" kind:eq value:"user.session.start" |
outcome.result | eq |
| field:"outcome.result" kind:eq value:"SUCCESS" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
displayName | actor.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
#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
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access |
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
#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.rawUserAgentdoes not end withPS_mxzqarw
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
client.userAgent.rawUserAgent | ends_with | PS_mxzqarw | excludes: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.
| Field | Source |
|---|---|
displayName | actor.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
#An admin user has disabled the MFA requirement for your Okta account
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Defense Impairment | |
| Credential Access |
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_typeisadmin_mfa_disabled
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
event_type | eq |
| field:"event_type" kind:eq value:"admin_mfa_disabled" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
user | actor_user |
ip | source_ip |
event | eventType |
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
#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
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type policy.evaluate_sign_on: Sign-on policy evaluated |
Rules detecting the same action
These rules filter on the same operation.
- High-Risk Admin Activity (Kusto)
- Okta AiTM Session Cookie Replay (Elastic)
- Okta Multiple Failed Requests To Access Applications (YARA-L)
- Okta Multiple Failed Requests to Access Applications (Splunk)
- Okta New Admin Console Behaviours (Sigma)
- Okta Suspicious Use Of A Session Cookie (YARA-L)
- Okta Suspicious Use of a Session Cookie (Splunk)
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
eventtypeispolicy.evaluate_sign_ontarget.displayNamecontainsOkta Admin Consoleany of:
all of:
debugContext.debugData.behaviorsis presentdebugContext.debugData.behaviorscontainsNew Device=POSITIVEdebugContext.debugData.behaviorscontainsNew IP=POSITIVE
all of:
debugContext.debugData.behaviorsis emptydebugContext.debugData.logOnlySecurityData.behaviors.New DeviceisPOSITIVEdebugContext.debugData.logOnlySecurityData.behaviors.New IPisPOSITIVE
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.
| Field | Source |
|---|---|
event_type | eventtype |
severity | |
actor | |
client | |
request | |
outcome | |
target | |
debug_context | debugcontext |
authentication_context | authenticationcontext |
security_context | securitycontext |
ips | p_any_ip_addresses |
displayName | actor.displayName |
alternateId | actor.alternateId |
ipAddress | client.ipAddress |
name | device.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
#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
| Tactic | Techniques |
|---|---|
| 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
eventTypeis one ofapplication.lifecycle.update,application.lifecycle.create,application.lifecycle.activatetarget.displayNamecontainsOrg2Org
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
eventType | in |
| field:"eventType" kind:in |
target.displayName | contains |
| field:"target.displayName" kind:contains value:"Org2Org" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
event_type | eventtype |
severity | |
actor | |
client | |
request | |
outcome | |
target | |
debug_context | debugcontext |
authentication_context | authenticationcontext |
security_context | securitycontext |
ips | p_any_ip_addresses |
displayName | actor.displayName |
alternateId | actor.alternateId |
alternateId | target.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
#User accessed another user's application password
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type application.user_membership.show_password: User's password shown for application |
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
eventTypeisapplication.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.
| Field | Kind | Values | Search |
|---|---|---|---|
eventType | eq |
| 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.
| Field | Source |
|---|---|
alternateId | actor.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
#This rule looks for the same session being used from two devices, indicating a compromised session token.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- Failed Logins from Unknown or Invalid User (Kusto)
- First Occurrence of Okta User Session Started via Proxy (Elastic)
- High-Risk Admin Activity (Kusto)
- Multiple Okta Sessions Detected for a Single User (Elastic)
- Multiple Okta User Authentication Events with Same Device Token Hash (Elastic)
- New Device/Location sign-in along with critical operation (Kusto)
- Okta AiTM Session Cookie Replay (Elastic)
- Okta Login From CrowdStrike Unmanaged Device (Panther)
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.idis not one ofokta.b58d5b75-07d4-5f25-bf59-368a1261a405eventTypeis one ofuser.authentication.sso,user.session.startauthenticationContext.externalSessionIdis notunknowndebugContext.debugData.dtHashis notunknown
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.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
eventType | in | user.authentication.sso, user.session.start | excludes:eventType field:"eventType" value:"user.authentication.sso" field:"eventType" value:"user.session.start" |
authenticationContext.externalSessionId | eq | unknown | excludes:authenticationContext.externalSessionId field:"authenticationContext.externalSessionId" value:"unknown" |
debugContext.debugData.dtHash | eq | unknown | excludes:debugContext.debugData.dtHash field:"debugContext.debugData.dtHash" value:"unknown" |
client.id | eq | okta.b58d5b75-07d4-5f25-bf59-368a1261a405 | excludes:client.id field:"client.id" value:"okta.b58d5b75-07d4-5f25-bf59-368a1261a405" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
eventType | in |
| field:"eventType" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
event_type | eventtype |
severity | |
actor | |
client | |
request | |
outcome | |
target | |
debug_context | debugcontext |
authentication_context | authenticationcontext |
security_context | securitycontext |
ips | p_any_ip_addresses |
displayName | actor.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
#Potential DoS/Bruteforce attack or hitting limits (system degradation)
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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:
eventtypematches the patternapp.oauth2.client_id_rate_limit_warningeventtypecontainsviolation
all of:
eventtypematches the patternapplication.integration.rate_limit_exceededeventtypecontainsviolation
all of:
eventtypematches the patternsystem.client.rate_limit.*eventtypecontainsviolation
all of:
eventtypematches the patternsystem.client.concurrency_rate_limit.*eventtypecontainsviolation
all of:
eventtypematches the patternsystem.operation.rate_limit.*eventtypecontainsviolation
all of:
eventtypematches the patternsystem.org.rate_limit.*eventtypecontainsviolation
all of:
eventtypematches the patterncore.concurrency.org.limit.violationeventtypecontainsviolation
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
event_type | eventtype |
severity | |
actor | |
client | |
request | |
outcome | |
target | |
debug_context | debugcontext |
authentication_context | authenticationcontext |
security_context | securitycontext |
ips | p_any_ip_addresses |
displayName | actor.displayName |
type | actor.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
#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
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type user.session.start: User logged in to Okta |
Rules detecting the same action
These rules filter on the same operation.
- Failed Logins from Unknown or Invalid User (Kusto)
- First Occurrence of Okta User Session Started via Proxy (Elastic)
- High-Risk Admin Activity (Kusto)
- Multiple Okta Sessions Detected for a Single User (Elastic)
- Multiple Okta User Authentication Events with Same Device Token Hash (Elastic)
- New Device/Location sign-in along with critical operation (Kusto)
- Okta AiTM Session Cookie Replay (Elastic)
- Okta Login From CrowdStrike Unmanaged Device (Panther)
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
eventTypeisuser.session.startsecurityContext.isProxyis present
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
eventType | eq |
| field:"eventType" kind:eq value:"user.session.start" |
securityContext.isProxy | is_not_null | field:"securityContext.isProxy" kind:is_not_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
event_type | eventtype |
severity | |
actor | |
client | |
request | |
outcome | |
target | |
debug_context | debugcontext |
authentication_context | authenticationcontext |
security_context | securitycontext |
ips | p_any_ip_addresses |
displayName | actor.displayName |
alternateId | actor.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.
Show instances that Okta support was granted to your account
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- High-Risk Admin Activity (Kusto)
- Okta Support Access Granted (Panther)
- Okta User Session Impersonation (Elastic)
- User Session Impersonation(Okta) (Kusto)
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
Stage 2: filter
any of:
eventTypeisuser.session.impersonation.granteventTypeisuser.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.
| Field | Kind | Values | Search |
|---|---|---|---|
eventType | eq |
| field:"eventType" kind:eq |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
event_time | p_event_time |
actor_email | actor:alternateId |
actor_name | actor:displayName |
src_ip | client:ipAddress |
city | client:geographicalContext:city |
country | client:geographicalContext:country |
user_agent | client:userAgent:rawUserAgent |
displayMessage | |
eventType |
Okta Support Access Granted
#An admin user has granted access to Okta Support to your account
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- High-Risk Admin Activity (Kusto)
- Okta Support Access (Panther)
- Okta User Session Impersonation (Elastic)
- User Session Impersonation(Okta) (Kusto)
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
eventTypeis one ofuser.session.impersonation.grant,user.session.impersonation.initiate
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
eventType | in |
| field:"eventType" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
user | actor_user |
ip | source_ip |
event | eventType |
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
#A Password or MFA factor was reset by Okta Support
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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
eventTypeis one ofuser.account.reset_password,user.mfa.factor.update,system.mfa.factor.deactivate,user.mfa.attempt_bypassactor.alternateIdissystem@okta.comtransaction.idisunknownclient.userAgent.rawUserAgentis emptyclient.geographicalContext.countryis empty
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
actor.alternateId | eq |
| field:"actor.alternateId" kind:eq value:"system@okta.com" |
client.geographicalContext.country | is_null | field:"client.geographicalContext.country" kind:is_null | |
client.userAgent.rawUserAgent | is_null | field:"client.userAgent.rawUserAgent" kind:is_null | |
eventType | in |
| field:"eventType" kind:in |
transaction.id | eq |
| field:"transaction.id" kind:eq value:"unknown" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
event_type | eventtype |
severity | |
actor | |
client | |
request | |
outcome | |
target | |
debug_context | debugcontext |
authentication_context | authenticationcontext |
security_context | securitycontext |
ips | p_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
#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
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access | |
| Collection |
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- First Occurrence of Okta User Session Started via Proxy (Elastic)
- Okta AiTM Session Cookie Replay (Elastic)
- Okta Multiple Failed Requests To Access Applications (YARA-L)
- Okta Multiple Failed Requests to Access Applications (Splunk)
- Okta Potentially Stolen Session (Panther)
- Okta SWA Off-Hours Credential Access - Behavioral (Panther)
- Potential Okta MFA Bombing via Push Notifications (Elastic)
- Potentially Successful Okta MFA Bombing via Push Notifications (Elastic)
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_emailis present
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
admin_email | is_not_null | 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
#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
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type application.user_membership.change_username: Change user's application username. |
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_emailis present
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
admin_email | is_not_null | 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
#Okta ThreatInsight identified request from potentially malicious IP address
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type security.threat.detected: Request from malicious IP detected by ThreatInsight |
Rules detecting the same action
These rules filter on the same operation.
- Device Registration from Malicious IP (Kusto)
- Okta Security Threat Detected (Sigma)
- Okta ThreatInsight Login Failure With High Unknown Users (YARA-L)
- Okta ThreatInsight Suspected Bruteforce Attack (YARA-L)
- Okta ThreatInsight Suspected Password Spray Attack (YARA-L)
- Okta ThreatInsight Threat Detected (Splunk)
- Okta ThreatInsight Threat Suspected Promotion (Elastic)
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
eventtypeissecurity.threat.detected
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
event_type | eventtype |
severity | |
actor | |
client | |
request | |
outcome | |
target | |
debug_context | debugcontext |
authentication_context | authenticationcontext |
security_context | securitycontext |
ips | p_any_ip_addresses |
displayName | actor.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
#An Okta user has locked their account.
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- Attempts to Brute Force an Okta User Account (Elastic)
- Okta Multiple Accounts Locked Out (Splunk)
- Okta User Account Lockout (YARA-L)
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
eventtypeis one ofuser.account.lock,user.account.lock.limit
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
event_type | eventtype |
severity | |
actor | |
client | |
request | |
outcome | |
target | |
debug_context | debugcontext |
authentication_context | authenticationcontext |
security_context | securitycontext |
ips | p_any_ip_addresses |
alternateId | actor.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
#Suspend factor or authenticator enrollment method for user.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type user.mfa.factor.suspend: MFA factor suspended for user |
Rules detecting the same action
These rules filter on the same operation.
- High-Risk Admin Activity (Kusto)
- Okta Investigate MFA and Password resets (Panther)
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
eventtypeisuser.mfa.factor.suspendoutcome.resultisSUCCESS
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
outcome.result | eq |
| field:"outcome.result" kind:eq value:"SUCCESS" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
event_type | eventtype |
severity | |
actor | |
client | |
request | |
outcome | |
target | |
debug_context | debugcontext |
authentication_context | authenticationcontext |
security_context | securitycontext |
ips | p_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
#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_typeismfa_reset
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
event_type | eq |
| field:"event_type" kind:eq value:"mfa_reset" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
event_type | eventtype |
severity | |
actor | |
client | |
request | |
outcome | |
target | |
debug_context | debugcontext |
authentication_context | authenticationcontext |
security_context | securitycontext |
ips | p_any_ip_addresses |
alternateId | actor.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
#All MFA factors have been reset for a user.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type user.mfa.factor.reset_all: All MFA factors reset for user |
Rules detecting the same action
These rules filter on the same operation.
- Attempt to Reset MFA Factors for an Okta User Account (Elastic)
- MFA Deactivation with no Re-Activation for Okta User Account (Elastic)
- New Device/Location sign-in along with critical operation (Kusto)
- Okta Investigate MFA and Password resets (Panther)
- Okta MFA Reset or Deactivated (Sigma)
- Okta User Password and MFA Factor Reset or Deactivated (YARA-L)
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
eventtypeisuser.mfa.factor.reset_all
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
event_type | eventtype |
severity | |
actor | |
client | |
request | |
outcome | |
target | |
debug_context | debugcontext |
authentication_context | authenticationcontext |
security_context | securitycontext |
ips | p_any_ip_addresses |
alternateId | actor.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
#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
eventtypeisuser.account.report_suspicious_activity_by_enduser
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
event_type | eventtype |
severity | |
actor | |
client | |
request | |
outcome | |
target | |
debug_context | debugcontext |
authentication_context | authenticationcontext |
security_context | securitycontext |
ips | p_any_ip_addresses |
alternateId | actor.alternateId |
suspiciousActivityEventType | debugcontext.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
#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
| Platform | Record / event type |
|---|---|
| Okta | System Log event type user.session.start: User logged in to Okta |
Rules detecting the same action
These rules filter on the same operation.
- Failed Logins from Unknown or Invalid User (Kusto)
- First Occurrence of Okta User Session Started via Proxy (Elastic)
- High-Risk Admin Activity (Kusto)
- Multiple Okta Sessions Detected for a Single User (Elastic)
- Multiple Okta User Authentication Events with Same Device Token Hash (Elastic)
- New Device/Location sign-in along with critical operation (Kusto)
- Okta AiTM Session Cookie Replay (Elastic)
- Okta Login From CrowdStrike Unmanaged Device (Panther)
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
Stage 2: filter
actor:typeisUsereventTypeisuser.session.startoutcome:resultisSUCCESS
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.
| Field | Kind | Values | Search |
|---|---|---|---|
actor:type | eq |
| field:"actor:type" kind:eq value:"User" |
eventType | eq |
| field:"eventType" kind:eq value:"user.session.start" |
outcome:result | eq |
| field:"outcome:result" kind:eq value:"SUCCESS" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
p_timeline | p_event_time |
* |
Query.Okta.ADAgentAuthZScoreAnomaly
#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
| Platform | Record / event type |
|---|---|
| Okta | System Log event type user.authentication.auth_via_AD_agent: Authenticate user with AD agent. |
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
Stage 2: filter
any of:
is_anomalousisTRUEis_cold_start_anomalyisTRUE
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
is_anomalous | eq |
| field:"is_anomalous" kind:eq value:"TRUE" |
is_cold_start_anomaly | eq |
| field:"is_cold_start_anomaly" kind:eq value:"TRUE" |
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
#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.
- Attempt to Create Okta API Token (Elastic)
- High-Risk Admin Activity (Kusto)
- New Device/Location sign-in along with critical operation (Kusto)
- Okta AD Agent Token Abuse - Behavioral (Panther)
- Okta API Key Created (Panther)
- Okta API Token Created (Sigma)
- Okta New API Token Created (YARA-L)
- Okta New API Token Created (Splunk)
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
Stage 2: filter
any of:
r.sourceIPis presentr.userAgentis 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.
| Field | Kind | Values | Search |
|---|---|---|---|
r.sourceIP | is_not_null | field:"r.sourceIP" kind:is_not_null | |
r.userAgent | is_not_null | field:"r.userAgent" kind:is_not_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
r.p_event_time | |
r.actorId | |
r.actorName | |
r.eventType | |
r.sourceIP | |
r.userAgent | |
r.result | |
r.target | |
anomaly_type | 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 ) ) , ', ' ) |
Query.Okta.SkeletonKeyBypassBehavioral
#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.
- Attempt to Modify an Okta Policy (Elastic)
- Attempt to Modify an Okta Policy Rule (Elastic)
- MFA Deactivation with no Re-Activation for Okta User Account (Elastic)
- New Device/Location sign-in along with critical operation (Kusto)
- Okta Authentication Bypass via Skeleton Key Injection - Behavioral (Panther)
- Okta Policy Modified or Deleted (Sigma)
- Okta Policy Rule Modified or Deleted (Sigma)
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
Stage 2: filter
is_anomalousisTRUE
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
is_anomalous | eq |
| field:"is_anomalous" kind:eq value:"TRUE" |
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
#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
| Tactic | Techniques |
|---|---|
| Credential Access | No specific technique |
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- First Occurrence of Okta User Session Started via Proxy (Elastic)
- Okta AiTM Session Cookie Replay (Elastic)
- Okta Multiple Failed Requests To Access Applications (YARA-L)
- Okta Multiple Failed Requests to Access Applications (Splunk)
- Okta Potentially Stolen Session (Panther)
- Okta SWA Bulk Access, New Source, and Credential Extraction - Behavioral (Panther)
- Okta SWA Off-Hours Credential Access - Behavioral (Panther)
- Potential Okta MFA Bombing via Push Notifications (Elastic)
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
Stage 2: filter
is_anomalousisTRUE
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
is_anomalous | eq |
| field:"is_anomalous" kind:eq value:"TRUE" |
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
#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
| Tactic | Techniques |
|---|---|
| Credential Access | No specific technique |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type application.user_membership.change_username: Change user's application username. |
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
Stage 2: filter
is_anomalousisTRUE
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
is_anomalous | eq |
| field:"is_anomalous" kind:eq value:"TRUE" |
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
#Telemetry coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type user.authentication.sso: User SSO to application |
Rules detecting the same action
These rules filter on the same operation.
- First Occurrence of Okta User Session Started via Proxy (Elastic)
- Okta AiTM Session Cookie Replay (Elastic)
- Okta Multiple Failed Requests To Access Applications (YARA-L)
- Okta Multiple Failed Requests to Access Applications (Splunk)
- Okta Potentially Stolen Session (Panther)
- Okta SWA Bulk Access, New Source, and Credential Extraction - Behavioral (Panther)
- Potential Okta MFA Bombing via Push Notifications (Elastic)
- Potentially Successful Okta MFA Bombing via Push Notifications (Elastic)
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
eventTypeisuser.authentication.ssooutcome.resultisSUCCESStarget.displayNamecontainsAWS IAM Identity Center
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
eventType | eq |
| field:"eventType" kind:eq value:"user.authentication.sso" |
outcome.result | eq |
| field:"outcome.result" kind:eq value:"SUCCESS" |
target.displayName | contains |
| field:"target.displayName" kind:contains value:"AWS IAM Identity Center" |
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"
}