Detection rules › Kusto

F&O - Unusual sign-in activity using single factor authentication

Status
available
Severity
low
Time window
14d
Group by
AppDisplayName, IPAddress, Location, UserPrincipalName
Source
github.com/Azure/Azure-Sentinel

Identifies sucessful sign-in events to Finance & Operations and Lifecycle Services using single factor/password authentication. Sign-in events from tenants not using MFA, coming from a Microsoft Entra trusted network location, or from geolocations seen previously in the last 14 days are excluded.

MITRE ATT&CK coverage

TacticTechniques
Initial AccessT1078 Valid Accounts
Credential AccessT1552 Unsecured Credentials

Rules detecting the same action

Other rules on this platform that filter on the same API call or operation.

Rule body kusto

id: 919e939f-95e2-4978-846e-13a721c89ea1
kind: Scheduled
name: F&O - Unusual sign-in activity using single factor authentication
description: Identifies sucessful sign-in events to Finance & Operations and Lifecycle
  Services using single factor/password authentication. Sign-in events from tenants
  not using MFA, coming from a Microsoft Entra trusted network location, or from geolocations
  seen previously in the last 14 days are excluded.
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
  - InitialAccess
relevantTechniques:
  - T1552
  - T1078
query: |
  // Dynamics Lifecycle services: 913c6de4-2a4a-4a61-a9ce-945d2b2ce2e0
  // Microsoft Dynamics ERP: 00000015-0000-0000-c000-000000000000
  let appid_list = dynamic(["913c6de4-2a4a-4a61-a9ce-945d2b2ce2e0", "00000015-0000-0000-c000-000000000000"]);
  let query_frequency = 1h;
  let query_lookback = 14d;
  let historical_sign_in_activity = SigninLogs
      | where TimeGenerated between (ago(query_lookback) .. ago(query_frequency));
  let historical_sign_in_locations = historical_sign_in_activity
      | summarize by Location;
  let multifactor_sign_in_count = toscalar(historical_sign_in_activity
      | where AppId in (appid_list) and ResultType == 0
      | where AuthenticationRequirement == "multiFactorAuthentication"
      | summarize count());
  SigninLogs
  | where TimeGenerated >= ago(query_frequency)
  | where AppId in (appid_list) and ResultType == 0
  | where multifactor_sign_in_count > 0
  | where Location !in (historical_sign_in_locations)
  | where NetworkLocationDetails !has "trustedNamedLocation"
  | summarize by UserPrincipalName, AppDisplayName, IPAddress, Location
  | extend
      CloudAppId = 32780,
      AccountName = tostring(split(UserPrincipalName, "@")[0]),
      UPNSuffix = tostring(split(UserPrincipalName, "@")[1])
  | project
      UserPrincipalName,
      AppDisplayName,
      IPAddress,
      Location,
      CloudAppId,
      AccountName,
      UPNSuffix
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: CloudAppId
alertDetailsOverride:
  alertDisplayNameFormat: Dynamics 365 F&O - Unusual sign-in without multi-factor
    authentication
  alertDescriptionFormat: Successful sign in by {{UserPrincipalName}} to {{AppDisplayName}}
    from location {{Location}} which has not been seen before in the last 14 days.
version: 3.2.0

Stages and Predicates

Parameters

let appid_list = dynamic(["913c6de4-2a4a-4a61-a9ce-945d2b2ce2e0", "00000015-0000-0000-c000-000000000000"]);
let query_frequency = 1h;
let query_lookback = 14d;

Let binding: historical_sign_in_activity

let historical_sign_in_activity = SigninLogs
    | where TimeGenerated between (ago(query_lookback) .. ago(query_frequency));

Derived from query_frequency, query_lookback.

Let binding: historical_sign_in_locations

let historical_sign_in_locations = historical_sign_in_activity
    | summarize by Location;

Derived from historical_sign_in_activity.

Let binding: multifactor_sign_in_count

let multifactor_sign_in_count = toscalar(historical_sign_in_activity
    | where AppId in (appid_list) and ResultType == 0
    | where AuthenticationRequirement == "multiFactorAuthentication"
    | summarize count());

Derived from appid_list, historical_sign_in_activity.

Stage 1: source

SigninLogs

Stage 2: where

| where TimeGenerated >= ago(query_frequency)

Stage 3: where

| where AppId in (appid_list) and ResultType == 0

Stage 4: where

| where multifactor_sign_in_count > 0

References multifactor_sign_in_count (defined above).

Stage 5: where

| where Location !in (historical_sign_in_locations)

References historical_sign_in_locations (defined above).

Stage 6: where

| where NetworkLocationDetails !has "trustedNamedLocation"

Stage 7: summarize

| summarize by UserPrincipalName, AppDisplayName, IPAddress, Location

Stage 8: extend

| extend
    CloudAppId = 32780,
    AccountName = tostring(split(UserPrincipalName, "@")[0]),
    UPNSuffix = tostring(split(UserPrincipalName, "@")[1])

Stage 9: project

| project
    UserPrincipalName,
    AppDisplayName,
    IPAddress,
    Location,
    CloudAppId,
    AccountName,
    UPNSuffix

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
Locationeqhistorical_sign_in_locations
NetworkLocationDetailsmatchtrustedNamedLocation

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.

FieldKindValues
AppIdin
  • 00000015-0000-0000-c000-000000000000 transforms: cased
  • 913c6de4-2a4a-4a61-a9ce-945d2b2ce2e0 transforms: cased
ResultTypeeq
  • 0 transforms: cased
multifactor_sign_in_countgt
  • 0 transforms: cased

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
AccountNameproject
AppDisplayNameproject
CloudAppIdproject
IPAddressproject
Locationproject
UPNSuffixproject
UserPrincipalNameproject