Detection rules › Kusto
Service Principal Authentication Attempt from New Country
'Detects when there is a Service Principal login attempt from a country that has not seen a successful login in the previous 14 days. Threat actors may attempt to authenticate with credentials from compromised accounts - monitoring attempts from anomalous locations may help identify these attempts. Authentication attempts should be investigated to ensure the activity was legitimate and if there is other similar activity. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-failed-unusual-sign-ins'
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | T1078.004 Valid Accounts: Cloud Accounts |
Rules detecting the same action
Other rules on this platform that filter on the same API call or operation.
- Authentication Attempt from New Country (Kusto)
- Azure AD High Number Of Failed Authentications For User (Splunk)
- Azure AD High Number Of Failed Authentications From Ip (Splunk)
- Azure AD Multi-Source Failed Authentications Spike (Splunk)
- Azure AD Multiple Users Failing To Authenticate From Ip (Splunk)
- Azure AD Unusual Number of Failed Authentications From Ip (Splunk)
- Entra ID Sign-in Brute Force Attempted (Microsoft 365) (Elastic)
- Entra ID User Sign-in Brute Force Attempted (Elastic)
Rule body kusto
id: 1baaaf00-655f-4de9-8ff8-312e902cda71
name: Service Principal Authentication Attempt from New Country
description: |
'Detects when there is a Service Principal login attempt from a country that has not seen a successful login in the previous 14 days.
Threat actors may attempt to authenticate with credentials from compromised accounts - monitoring attempts from anomalous locations may help identify these attempts.
Authentication attempts should be investigated to ensure the activity was legitimate and if there is other similar activity.
Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-failed-unusual-sign-ins'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AADServicePrincipalSignInLogs
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078.004
tags:
- AADSecOpsGuide
query: |
let known_locations = (
AADServicePrincipalSignInLogs
| where TimeGenerated between(ago(14d)..ago(1d))
| where ResultType == 0
| summarize by Location);
AADServicePrincipalSignInLogs
| where TimeGenerated > ago(1d)
| where ResultType != 50126
| where Location !in (known_locations)
| extend City = tostring(parse_json(LocationDetails).city)
| extend State = tostring(parse_json(LocationDetails).state)
| extend Place = strcat(City, " - ", State)
| extend Result = strcat(tostring(ResultType), " - ", ResultDescription)
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), make_set(Result), make_set(IPAddress), make_set(Place) by ServicePrincipalName, Location
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: ServicePrincipalName
version: 1.0.1
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Pete Bryan
support:
tier: Community
categories:
domains: [ "Security - Others", "Identity" ]
Stages and Predicates
Let binding: known_locations
let known_locations = (
AADServicePrincipalSignInLogs
| where TimeGenerated between(ago(14d)..ago(1d))
| where ResultType == 0
| summarize by Location);
Stage 1: source
AADServicePrincipalSignInLogs
Stage 2: where
| where TimeGenerated > ago(1d)
Stage 3: where
| where ResultType != 50126
Stage 4: where
| where Location !in (known_locations)
References known_locations (defined above).
Stage 5: extend (4 consecutive steps)
| extend City = tostring(parse_json(LocationDetails).city)
| extend State = tostring(parse_json(LocationDetails).state)
| extend Place = strcat(City, " - ", State)
| extend Result = strcat(tostring(ResultType), " - ", ResultDescription)
Stage 6: summarize
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), make_set(Result), make_set(IPAddress), make_set(Place) by ServicePrincipalName, Location
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
Location | eq | known_locations |
Indicators
Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.
| Field | Kind | Values |
|---|---|---|
ResultType | ne |
|
Output fields
Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.
| Field | Source |
|---|---|
FirstSeen | summarize |
LastSeen | summarize |
Location | summarize |
ServicePrincipalName | summarize |