Detection rules › Kusto
New Device/Location sign-in along with critical operation
This query identifies users seen login from a new geo location/country and a new device, then correlates that sign-in with successful risky operations such as policy changes, MFA changes, API token actions etc. This can be an indication of an attacker gaining access to a user's credentials and then performing critical operations, which typically requires admin privileges. By detecting such patterns, organizations can quickly respond to potential security incidents and mitigate risks associated with unauthorized access and privilege escalation.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence | |
| Privilege Escalation |
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- Attempt to Create Okta API Token (Elastic)
- Attempt to Deactivate an Okta Policy (Elastic)
- Attempt to Deactivate an Okta Policy Rule (Elastic)
- Attempt to Delete an Okta Policy (Elastic)
- Attempt to Delete an Okta Policy Rule (Elastic)
- Attempt to Modify an Okta Network Zone (Elastic)
- Attempt to Modify an Okta Policy (Elastic)
- Attempt to Modify an Okta Policy Rule (Elastic)
Rule body
id: 41e843a8-92e7-444d-8d72-638f1145d1e1
name: New Device/Location sign-in along with critical operation
description: |
This query identifies users seen login from a new geo location/country and a new device, then correlates that sign-in with successful risky operations such as policy changes, MFA changes, API token actions etc. This can be an indication of an attacker gaining access to a user's credentials and then performing critical operations, which typically requires admin privileges. By detecting such patterns, organizations can quickly respond to potential security incidents and mitigate risks associated with unauthorized access and privilege escalation.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: OktaSSO
dataTypes:
- Okta_CL
- connectorId: OktaSSOv2
dataTypes:
- OktaSSO
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- Persistence
- PrivilegeEscalation
relevantTechniques:
- T1078
- T1556
- T1098
query: |
let timeframe = 1h;
let RiskyOperations = dynamic(["policy.rule.update","policy.rule.create","policy.rule.delete", "policy.rule.deactivate", "policy.lifecycle.update", "policy.rule.modify", "policy.lifecycle.create", "policy.lifecycle.delete", "policy.lifecycle.deactivate", "policy.lifecycle.modify", "network_zone.rule.disabled", "system.api_token.create", "system.api_token.revoke", "application.policy.sign_on.update", "application.policy.sign_on.rule.delete","user.mfa.factor.deactivate", "user.mfa.factor.reset_all", "system.mfa.factor.deactivate", "user.mfa.attempt_bypass"]);
let UserLoginNewCountryDevice = OktaSSO
| where eventType_s == "user.session.start"
| where outcome_result_s == "SUCCESS"
| extend debugContext_debugData_logOnlySecurityData_s = column_ifexists('debugContext_debugData_logOnlySecurityData_s', '{}')
| extend behaviors = parse_json(tostring(parse_json(debugContext_debugData_logOnlySecurityData_s).behaviors))
| where tostring(behaviors.["New Country"]) == "POSITIVE"
| where tostring(behaviors.["New Geo-Location"]) == "POSITIVE"
| where tostring(behaviors.["New Device"]) == "POSITIVE"
| summarize by timekey = bin(TimeGenerated, timeframe), actor_alternateId_s, actor_displayName_s, client_userAgent_os_s, client_userAgent_browser_s, client_device_s, client_userAgent_rawUserAgent_s, client_ipAddress_s, authenticationContext_externalSessionId_s, client_geographicalContext_country_s, client_geographicalContext_city_s, displayMessage_s, outcome_result_s, outcome_reason_s, column_ifexists('debugContext_debugData_logOnlySecurityData_s', ""), column_ifexists('debugContext_debugData_threatSuspected_s', ""), client_geographicalContext_geolocation_lat_d, client_geographicalContext_geolocation_lon_d
| extend Location = strcat(client_geographicalContext_city_s, "-", client_geographicalContext_country_s);
let RiskyOperationsObserved = OktaSSO
| where eventType_s in (RiskyOperations)
| where outcome_result_s == "SUCCESS"
| summarize by timekey = bin(TimeGenerated, timeframe), actor_alternateId_s, actor_displayName_s, client_userAgent_os_s, client_userAgent_browser_s, client_device_s, client_userAgent_rawUserAgent_s, client_ipAddress_s, authenticationContext_externalSessionId_s, client_geographicalContext_country_s, client_geographicalContext_city_s, displayMessage_s, outcome_result_s, outcome_reason_s, column_ifexists('debugContext_debugData_logOnlySecurityData_s', ""), column_ifexists('debugContext_debugData_threatSuspected_s', ""), client_geographicalContext_geolocation_lat_d, client_geographicalContext_geolocation_lon_d;
UserLoginNewCountryDevice
| join kind=inner (RiskyOperationsObserved) on timekey, actor_displayName_s, client_ipAddress_s
| project TimeGenerated = timekey, actor_alternateId_s, actor_displayName_s, client_userAgent_os_s, client_userAgent_browser_s, client_device_s, client_userAgent_rawUserAgent_s, client_ipAddress_s, authenticationContext_externalSessionId_s, client_geographicalContext_country_s, client_geographicalContext_city_s, displayMessage_s, outcome_result_s, outcome_reason_s, column_ifexists('debugContext_debugData_logOnlySecurityData_s', ""), column_ifexists('debugContext_debugData_threatSuspected_s', ""), client_geographicalContext_geolocation_lat_d, client_geographicalContext_geolocation_lon_d, Location
| 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:
ActorDisplayName: actor_displayName_s
ActorDevice: client_device_s
ActorOS: client_userAgent_os_s
ActorBrowser: client_userAgent_browser_s
UserAgent: client_userAgent_rawUserAgent_s
SessionId: authenticationContext_externalSessionId_s
Location: Location
alertDetailsOverride:
alertDisplayNameFormat: 'New Device/Location {{Location}} sign-in along with critical operation'
alertDescriptionFormat: |
This query identifies users seen login from new geo location/country {{Location}} as well as a new device and performing critical operations.
version: 1.1.3
kind: Scheduled
Stages and Predicates
Parameters
let timeframe = 1h;
let UserLoginNewCountryDevice is inlined into the numbered stages below.
Let binding: RiskyOperations
let RiskyOperations = dynamic(["policy.rule.update","policy.rule.create","policy.rule.delete", "policy.rule.deactivate", "policy.lifecycle.update", "policy.rule.modify", "policy.lifecycle.create", "policy.lifecycle.delete", "policy.lifecycle.deactivate", "policy.lifecycle.modify", "network_zone.rule.disabled", "system.api_token.create", "system.api_token.revoke", "application.policy.sign_on.update", "application.policy.sign_on.rule.delete","user.mfa.factor.deactivate", "user.mfa.factor.reset_all", "system.mfa.factor.deactivate", "user.mfa.attempt_bypass"]);
Let binding: RiskyOperationsObserved
let RiskyOperationsObserved = OktaSSO
| where eventType_s in (RiskyOperations)
| where outcome_result_s == "SUCCESS"
| summarize by timekey = bin(TimeGenerated, timeframe), actor_alternateId_s, actor_displayName_s, client_userAgent_os_s, client_userAgent_browser_s, client_device_s, client_userAgent_rawUserAgent_s, client_ipAddress_s, authenticationContext_externalSessionId_s, client_geographicalContext_country_s, client_geographicalContext_city_s, displayMessage_s, outcome_result_s, outcome_reason_s, column_ifexists('debugContext_debugData_logOnlySecurityData_s', ""), column_ifexists('debugContext_debugData_threatSuspected_s', ""), client_geographicalContext_geolocation_lat_d, client_geographicalContext_geolocation_lon_d;
Stages 1 to 10 define let UserLoginNewCountryDevice (the rule's main pipeline source); stages 11 to 13 run on it.
Stage 1: source
OktaSSO
Stage 2: where
| where eventType_s == "user.session.start"
Stage 3: where
| where outcome_result_s == "SUCCESS"
Stage 4: extend
| extend debugContext_debugData_logOnlySecurityData_s = column_ifexists('debugContext_debugData_logOnlySecurityData_s', '{}')
Stage 5: extend
| extend behaviors = parse_json(tostring(parse_json(debugContext_debugData_logOnlySecurityData_s).behaviors))
Stage 6: where
| where tostring(behaviors.["New Country"]) == "POSITIVE"
Stage 7: where
| where tostring(behaviors.["New Geo-Location"]) == "POSITIVE"
Stage 8: where
| where tostring(behaviors.["New Device"]) == "POSITIVE"
Stage 9: summarize
| summarize by timekey = bin(TimeGenerated, timeframe), actor_alternateId_s, actor_displayName_s, client_userAgent_os_s, client_userAgent_browser_s, client_device_s, client_userAgent_rawUserAgent_s, client_ipAddress_s, authenticationContext_externalSessionId_s, client_geographicalContext_country_s, client_geographicalContext_city_s, displayMessage_s, outcome_result_s, outcome_reason_s, column_ifexists('debugContext_debugData_logOnlySecurityData_s', ""), column_ifexists('debugContext_debugData_threatSuspected_s', ""), client_geographicalContext_geolocation_lat_d, client_geographicalContext_geolocation_lon_d
Stage 10: extend
| extend Location = strcat(client_geographicalContext_city_s, "-", client_geographicalContext_country_s)
Stage 11: join
UserLoginNewCountryDevice
| join kind=inner (RiskyOperationsObserved) on timekey, actor_displayName_s, client_ipAddress_s
Stage 12: project
| project TimeGenerated = timekey, actor_alternateId_s, actor_displayName_s, client_userAgent_os_s, client_userAgent_browser_s, client_device_s, client_userAgent_rawUserAgent_s, client_ipAddress_s, authenticationContext_externalSessionId_s, client_geographicalContext_country_s, client_geographicalContext_city_s, displayMessage_s, outcome_result_s, outcome_reason_s, column_ifexists('debugContext_debugData_logOnlySecurityData_s', ""), column_ifexists('debugContext_debugData_threatSuspected_s', ""), client_geographicalContext_geolocation_lat_d, client_geographicalContext_geolocation_lon_d, Location
Stage 13: extend
| extend AccountName = tostring(split(actor_alternateId_s, "@")[0]), AccountUPNSuffix = tostring(split(actor_alternateId_s, "@")[1])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
New Country | eq |
| field:"New Country" kind:eq value:"POSITIVE" |
New Device | eq |
| field:"New Device" kind:eq value:"POSITIVE" |
New Geo-Location | eq |
| field:"New Geo-Location" kind:eq value:"POSITIVE" |
eventType_s | eq |
| field:"okta::eventType" kind:eq value:"user.session.start" |
eventType_s | in |
| field:"okta::eventType" kind:in |
outcome_result_s | eq |
| field:"okta::outcome.result" kind:eq value:"SUCCESS" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Location | project |
TimeGenerated | project |
actor_alternateId_s | project |
actor_displayName_s | project |
authenticationContext_externalSessionId_s | project |
client_device_s | project |
client_geographicalContext_city_s | project |
client_geographicalContext_country_s | project |
client_geographicalContext_geolocation_lat_d | project |
client_geographicalContext_geolocation_lon_d | project |
client_ipAddress_s | project |
client_userAgent_browser_s | project |
client_userAgent_os_s | project |
client_userAgent_rawUserAgent_s | project |
displayMessage_s | project |
outcome_reason_s | project |
outcome_result_s | project |
AccountName | extend |
AccountUPNSuffix | extend |