Detection rules › Kusto

GitHub Signin Burst from Multiple Locations

Status
available
Severity
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

'This detection triggers when there is a Signin burst from multiple locations in GitHub (Entra ID SSO). This detection is based on configurable threshold which can be prone to false positives. To view the anomaly based equivalent of thie detection, please see here https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Entra ID/Analytic Rules/AnomalousUserAppSigninLocationIncrease-detection.yaml. '

MITRE ATT&CK coverage

TacticTechniques
Credential AccessT1110 Brute Force

Rule body kusto

id: d3980830-dd9d-40a5-911f-76b44dfdce16
name: GitHub Signin Burst from Multiple Locations
description: |
  'This detection triggers when there is a Signin burst from multiple locations in GitHub (Entra ID SSO).
   This detection is based on configurable threshold which can be prone to false positives. To view the anomaly based equivalent of thie detection, please see here https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft%20Entra%20ID/Analytic%20Rules/AnomalousUserAppSigninLocationIncrease-detection.yaml. '
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  let locationThreshold = 1;
  let aadFunc = (tableName:string){
  table(tableName)
  | where AppDisplayName =~ "GitHub.com"
  | where ResultType == 0
  | summarize CountOfLocations = dcount(Location), Locations = make_set(Location,100), BurstStartTime = min(TimeGenerated), BurstEndTime = max(TimeGenerated) by UserPrincipalName, Type
  | where CountOfLocations > locationThreshold
  | extend timestamp = BurstStartTime
  };
  let aadSignin = aadFunc("SigninLogs");
  let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
  union isfuzzy=true aadSignin, aadNonInt
  | extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
    - identifier: FullName
      columnName: UserPrincipalName
    - identifier: Name
      columnName: Name
    - identifier: UPNSuffix
      columnName: UPNSuffix
version: 1.0.3
kind: Scheduled

Stages and Predicates

Parameters

let locationThreshold = 1;
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");

Let binding: aadFunc

let aadFunc = (tableName:string){
table(tableName)
| where AppDisplayName =~ "GitHub.com"
| where ResultType == 0
| summarize CountOfLocations = dcount(Location), Locations = make_set(Location,100), BurstStartTime = min(TimeGenerated), BurstEndTime = max(TimeGenerated) by UserPrincipalName, Type
| where CountOfLocations > locationThreshold
| extend timestamp = BurstStartTime
};

Derived from locationThreshold.

union isfuzzy=true (2 sources)

Each leg below queries one source; the rule matches if any leg does. Sources: aadSignin, aadNonInt

Leg 1: aadSignin

Leg 2: aadNonInt

Applied to the combined result

| extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])

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.

FieldSource
Nameextend
UPNSuffixextend