Detection rules › Kusto
Failed Logins from Unknown or Invalid User
This rule alerts when an unknown or invalid user generates more than 15 failed Okta management console login attempts from the same IP address within 5 minutes.
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.
- 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)
- Okta Login From CrowdStrike Unmanaged Device (Panther)
Rule body
id: 884be6e7-e568-418e-9c12-89229865ffde
name: Failed Logins from Unknown or Invalid User
description: |
This rule alerts when an unknown or invalid user generates more than 15 failed Okta management console login attempts from the same IP address within 5 minutes.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: OktaSSO
dataTypes:
- Okta_CL
- connectorId: OktaSSOv2
dataTypes:
- OktaSSO
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1110.001
query: |
// Adjust threshold for failed logins to reduce noise. This can be customized based on the organization's typical login patterns.
let FailureThreshold = 15;
// Adjust list of allowed users to reduce false positives. This can include service accounts, known test accounts, or any other accounts that are expected to generate failed login attempts.
let AllowedUsers = dynamic([]);
let FailedLogins = OktaSSO
| where eventType_s =~ "user.session.start" and outcome_reason_s =~ "VERIFICATION_ERROR"
| where isnotempty(actor_alternateId_s) and actor_alternateId_s !in (AllowedUsers)
| summarize count() by actor_alternateId_s, client_ipAddress_s, bin(TimeGenerated, 5m)
| where count_ > FailureThreshold
| project client_ipAddress_s, actor_alternateId_s;
OktaSSO
| join kind=inner (FailedLogins) on client_ipAddress_s, actor_alternateId_s
| where eventType_s =~ "user.session.start" and outcome_reason_s =~ "VERIFICATION_ERROR"
| summarize FailedLoginCount = count(), City = take_any(column_ifexists('client_geographicalContext_city_s', "")), Country = take_any(column_ifexists('client_geographicalContext_country_s', "")), LastSeen = max(column_ifexists('TimeGenerated', now())) by actor_alternateId_s, client_ipAddress_s
| sort by LastSeen desc
| extend AccountName = tostring(split(actor_alternateId_s, "@")[0]), AccountUPNSuffix = tostring(split(actor_alternateId_s, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: client_ipAddress_s
customDetails:
FailedLoginCount: FailedLoginCount
City: City
Country: Country
alertDetailsOverride:
alertDisplayNameFormat: 'Failed logins from unknown or invalid user: {{actor_alternateId_s}} from IP {{client_ipAddress_s}}'
alertDescriptionFormat: 'Account {{actor_alternateId_s}} generated {{FailedLoginCount}} failed logins from IP {{client_ipAddress_s}} in the last 5 minutes.'
version: 1.1.2
kind: Scheduled
Stages and Predicates
Stage 0: let
let FailureThreshold = 15;
let AllowedUsers = dynamic([]);
let FailedLogins = OktaSSO
| where eventType_s =~ "user.session.start" and outcome_reason_s =~ "VERIFICATION_ERROR"
| where isnotempty(actor_alternateId_s) and actor_alternateId_s !in (AllowedUsers)
| summarize count() by actor_alternateId_s, client_ipAddress_s, bin(TimeGenerated, 5m)
| where count_ > FailureThreshold
| project client_ipAddress_s, actor_alternateId_s;
Stage 1: source
OktaSSO
Stage 2: join
| join kind=inner (FailedLogins) on client_ipAddress_s, actor_alternateId_s
Stage 3: where
where eventType_s =~ "user.session.start" and outcome_reason_s =~ "VERIFICATION_ERROR"
Stage 4: summarize
summarize City, Country, FailedLoginCount, LastSeen by actor_alternateId_s, client_ipAddress_s
Stage 5: sort
sort by LastSeen
Stage 6: extend
extend AccountName, AccountUPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
actor_alternateId_s | is_not_null | field:"okta::actor.alternateId" kind:is_not_null | |
count_ | gt |
| field:"count_" kind:gt value:"15" |
eventType_s | eq |
| field:"okta::eventType" kind:eq value:"user.session.start" |
outcome_reason_s | eq |
| field:"okta::outcome.reason" kind:eq value:"VERIFICATION_ERROR" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
actor_alternateId_s | eq | [] | excludes:actor_alternateId_s field:"actor_alternateId_s" value:"[]" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
City | summarize |
Country | summarize |
FailedLoginCount | summarize |
LastSeen | summarize |
actor_alternateId_s | summarize |
client_ipAddress_s | summarize |
AccountName | extend |
AccountUPNSuffix | extend |