Detection rules › Kusto

Dataverse - Suspicious use of Web API

Status
available
Severity
medium
Time window
14d
Group by
AppDisplayName, AppId, ClientIp, IPAddress, InstanceUrl, UserId, UserPrincipalName
Source
github.com/Azure/Azure-Sentinel

Identifies sign-in across multiple Dataverse environments, breaching a predefined threshold, originating from a user with IP address that was used to sign-into the well known Microsoft Entra app registration.

MITRE ATT&CK coverage

Telemetry coverage

Rule body

id: 8a6ecba2-ccfe-4c8c-b086-fa3e6ff7fa86
kind: Scheduled
name: Dataverse - Suspicious use of Web API
description: Identifies sign-in across multiple Dataverse environments, breaching
  a predefined threshold, originating from a user with IP address that was used to
  sign-into the well known Microsoft Entra app registration.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Dataverse
    dataTypes:
      - DataverseActivity
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
  - Exfiltration
  - Reconnaissance
  - Discovery
relevantTechniques:
  - T1106
  - T1567
  - T1595
  - T1526
  - T1580
query: |
  let query_frequency = 1h;
  let query_lookback = 24h;
  // AppID of the multi-tenant Dynamics 365 Example Client Application
  let well_known_app_id = "51f81489-12ee-4a9e-aaae-a2591f45987d";
  let environment_count_threshold = 10;
  SigninLogs
  | where TimeGenerated >= ago(query_lookback)
  // Comment out the line below to monitor activity from all Azure AD apps
  | where AppId == well_known_app_id
  | where ResourceIdentity == '00000007-0000-0000-c000-000000000000'
  | summarize FirstSeen = min(TimeGenerated) by AppId, UserPrincipalName, IPAddress, AppDisplayName
  | join kind=inner (
      DataverseActivity
      | where TimeGenerated >= ago(query_frequency)
      | where Message == "UserSignIn")
      on $left.UserPrincipalName == $right.UserId, $left.IPAddress == $right.ClientIp
  | where TimeGenerated between (FirstSeen .. (FirstSeen + 2h))
  | summarize InstanceCount = dcount(InstanceUrl, 4), FirstSeen = min(FirstSeen) by UserId, ClientIp, InstanceUrl, AppDisplayName, AppId
  | where InstanceCount > environment_count_threshold
  | extend
      CloudAppId = int(32780),
      AccountName = tostring(split(UserId, '@')[0]),
      UPNSuffix = tostring(split(UserId, '@')[1])
  | project
      FirstSeen,
      UserId,
      ClientIp,
      AppDisplayName,
      AppId,
      InstanceUrl,
      CloudAppId,
      AccountName,
      UPNSuffix
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: CloudAppId
      - identifier: InstanceName
        columnName: InstanceUrl
alertDetailsOverride:
  alertDisplayNameFormat: Dataverse - Suspicious Web API sign-in activity
  alertDescriptionFormat: '{{UserId}} sign-in activity generated in {{InstanceUrl}}.
    The app used was a well known multi-tenant app not owned or registered by the
    organization.'
version: 3.2.0

Stages and Predicates

Parameters

let query_frequency = 1h;
let query_lookback = 24h;
let well_known_app_id = "51f81489-12ee-4a9e-aaae-a2591f45987d";
let environment_count_threshold = 10;

Stage 1: source

SigninLogs

Stage 2: where

| where TimeGenerated >= ago(query_lookback)

Stage 3: where

| where AppId == well_known_app_id

Stage 4: where

| where ResourceIdentity == '00000007-0000-0000-c000-000000000000'

Stage 5: summarize

| summarize FirstSeen = min(TimeGenerated) by AppId, UserPrincipalName, IPAddress, AppDisplayName

Stage 6: join

| join kind=inner (
    DataverseActivity
    | where TimeGenerated >= ago(query_frequency)
    | where Message == "UserSignIn")
    on $left.UserPrincipalName == $right.UserId, $left.IPAddress == $right.ClientIp

Stage 7: where TimeGenerated - FirstSeen >= 0s AND TimeGenerated - FirstSeen <= 2h

| where TimeGenerated between (FirstSeen .. (FirstSeen + 2h))

Stage 8: summarize

| summarize InstanceCount = dcount(InstanceUrl, 4), FirstSeen = min(FirstSeen) by UserId, ClientIp, InstanceUrl, AppDisplayName, AppId

Stage 9: where

| where InstanceCount > environment_count_threshold

Stage 10: extend

| extend
    CloudAppId = int(32780),
    AccountName = tostring(split(UserId, '@')[0]),
    UPNSuffix = tostring(split(UserId, '@')[1])

Stage 11: project

| project
    FirstSeen,
    UserId,
    ClientIp,
    AppDisplayName,
    AppId,
    InstanceUrl,
    CloudAppId,
    AccountName,
    UPNSuffix

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
AppDisplayNameproject
AppIdproject
ClientIpproject
CloudAppIdproject
FirstSeenproject
InstanceUrlproject
UPNSuffixproject
UserIdproject