Detection rules › Kusto

StealthTalk - Login outside work zone

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

Identifies a single StealthTalk authentication originating from a country or city that does not match the user's assigned (expected) geographic zone. Each individual mismatch is treated as an incident - there is no aggregation threshold, since a single login from an unexpected country is high-confidence evidence of a credential issue. An incident fires when LoginCountry differs from AssignedCountry OR LoginCity differs from AssignedCity. Source IPv4, raw event ID, and both the observed and expected geo-locations are surfaced as entities and custom details for the SOC analyst.

MITRE ATT&CK coverage

TacticTechniques
Initial Access
Stealth

Rule body

id: a7c3e9b1-4f5d-4e2a-9b8c-1d2e3f4a5b6c
name: StealthTalk - Login outside work zone
description: |
  Identifies a single StealthTalk authentication originating from a country or city that does
  not match the user's assigned (expected) geographic zone. Each individual mismatch is
  treated as an incident - there is no aggregation threshold, since a single login from an
  unexpected country is high-confidence evidence of a credential issue.

  An incident fires when LoginCountry differs from AssignedCountry OR LoginCity differs
  from AssignedCity. Source IPv4, raw event ID, and both the observed and expected
  geo-locations are surfaced as entities and custom details for the SOC analyst.
severity: High
requiredDataConnectors:
  - connectorId: StealthTalkAnomalousAuth
    dataTypes:
      - StealthTalkAnomalousAuth_CL
queryFrequency: 15m
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
  - DefenseEvasion
  - CredentialAccess
relevantTechniques:
  - T1078
query: |
  let LookbackPeriod = 1h;
  StealthTalkAnomalousAuth_CL
  | where TimeGenerated >= ago(LookbackPeriod)
  | where EventType == "GeoAnomalyLogin"
  | where LoginCountry != AssignedCountry
       or LoginCity    != AssignedCity
  | extend
      CountryMismatch = LoginCountry != AssignedCountry,
      CityMismatch    = LoginCity    != AssignedCity,
      AlertName       = "LoginOutsideWorkZone",
      AlertDetails    = strcat(
          "User ", UserId,
          " logged in from ", LoginCity, " (", LoginCountry, ")",
          " - assigned zone: ", AssignedCity, " (", AssignedCountry, ").",
          " Source IP: ", IpAddress, ".",
          " Country mismatch: ", tostring(LoginCountry != AssignedCountry), ".",
          " City mismatch: ", tostring(LoginCity != AssignedCity), "."
      )
  | project
      TimeGenerated, UserId, DeviceId,
      LoginCountry, LoginCity, AssignedCountry, AssignedCity,
      CountryMismatch, CityMismatch, IpAddress,
      AppVersion, RawEventId, AlertName, AlertDetails
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: UserId
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DeviceId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IpAddress
customDetails:
  LoginCountry: LoginCountry
  LoginCity: LoginCity
  AssignedCountry: AssignedCountry
  AssignedCity: AssignedCity
  CountryMismatch: CountryMismatch
  CityMismatch: CityMismatch
  AppVersion: AppVersion
  EventReference: RawEventId
alertDetailsOverride:
  alertDisplayNameFormat: 'StealthTalk: Login Outside Work Zone - {{UserId}} from {{LoginCity}} ({{LoginCountry}})'
  alertDescriptionFormat: '{{AlertDetails}}'
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: Selected
    groupByEntities:
      - Account
suppressionEnabled: false
suppressionDuration: 1h
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let LookbackPeriod = 1h;

Stage 1: source

StealthTalkAnomalousAuth_CL

Stage 2: where

| where TimeGenerated >= ago(LookbackPeriod)

Stage 3: where

| where EventType == "GeoAnomalyLogin"

Stage 4: where

| where LoginCountry != AssignedCountry
     or LoginCity    != AssignedCity

Stage 5: extend

| extend
    CountryMismatch = LoginCountry != AssignedCountry,
    CityMismatch    = LoginCity    != AssignedCity,
    AlertName       = "LoginOutsideWorkZone",
    AlertDetails    = strcat(
        "User ", UserId,
        " logged in from ", LoginCity, " (", LoginCountry, ")",
        " - assigned zone: ", AssignedCity, " (", AssignedCountry, ").",
        " Source IP: ", IpAddress, ".",
        " Country mismatch: ", tostring(LoginCountry != AssignedCountry), ".",
        " City mismatch: ", tostring(LoginCity != AssignedCity), "."
    )

Stage 6: project

| project
    TimeGenerated, UserId, DeviceId,
    LoginCountry, LoginCity, AssignedCountry, AssignedCity,
    CountryMismatch, CityMismatch, IpAddress,
    AppVersion, RawEventId, AlertName, AlertDetails

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
AlertDetailsproject
AlertNameproject
AppVersionproject
AssignedCityproject
AssignedCountryproject
CityMismatchproject
CountryMismatchproject
DeviceIdproject
IpAddressproject
LoginCityproject
LoginCountryproject
RawEventIdproject
TimeGeneratedproject
UserIdproject