Detection rules › Kusto

Dataverse - Terminated employee exfiltration over email

Status
available
Severity
high
Time window
14d
Group by
AccountName, SenderAddress, UPNSuffix, UserId
Source
github.com/Azure/Azure-Sentinel

This query identifies Dataverse exfiltration via email by terminated employees.

MITRE ATT&CK coverage

Rule body kusto

id: de039242-47e0-43fa-84d7-b6be24305349
kind: Scheduled
name: Dataverse - Terminated employee exfiltration over email
description: This query identifies Dataverse exfiltration via email by terminated
  employees.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - EmailEvents
  - connectorId: AzureActiveDirectoryIdentityProtection
    dataTypes:
      - SecurityAlert
  - connectorId: IdentityInfo
    dataTypes:
      - IdentityInfo
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1639
  - T1567
query: |
  // Note this detection relies upon the user's UPN matching their email address.
  // UEBA can provide more accurate data if enabled.
  let query_frequency = 1h;
  let allowed_destination_smtp_domains = dynamic([
  // Specify a list of recipient domains to exclude from alerting.
  // Example:
  // "microsoft.com", "contoso.com"
      ]);
  let exfiltration_alert_users = SecurityAlert
      | where Tactics has 'Exfiltration' and Entities has_all ('account', '32780')
      | mv-expand DataverseEntities = todynamic(Entities)
      | where DataverseEntities.AppId == 32780
      | extend InstanceUrl = tostring(DataverseEntities.InstanceName)
      | mv-expand AccountEntities = todynamic(Entities)
      | where AccountEntities.Type == 'account'
      | extend
          AccountName = tostring(AccountEntities.Name),
          UPNSuffix = tostring(AccountEntities.UPNSuffix)
      | summarize InstanceUrls = make_set(InstanceUrl, 100) by AccountName, UPNSuffix
      | extend UserId = tolower(strcat(AccountName, "@", UPNSuffix));
  exfiltration_alert_users
  | join kind=inner (
      MSBizAppsTerminatedEmployees
      | project UserId = tolower(UserPrincipalName), NotificationDate
      | where startofday(NotificationDate) <= startofday(now()))
      // Uncomment the below KQL if UEBA is available to gain more accurate
      // email address data:
      // | join kind=leftouter (_ASIM_IdentityInfo) on $left.UserId == $right.Username
      // | extend UserId = iif(UserId == UserMailAddress or isempty(UserMailAddress), UserId, UserMailAddress))
      on UserId
  | join kind=inner (
      EmailEvents
      | where TimeGenerated >= ago (query_frequency)
      | where EmailDirection == "Outbound" and AttachmentCount > 0
      | extend RecipientDomain = tolower(split(RecipientEmailAddress, '@')[1])
      | where RecipientDomain !in (allowed_destination_smtp_domains)
      | summarize
          RecipientAddresses = make_set(RecipientEmailAddress, 1000),
          Subject = make_set(Subject, 1000)
          by SenderAddress = tolower(SenderMailFromAddress), SenderIPv4)
      on $left.UserId == $right.SenderAddress
  | mv-expand InstanceUrl = InstanceUrls to typeof(string)
  | extend
      CloudAppId = int(32780),
      AccountName = tostring(split(UserId, "@")[0]),
      UPNSuffix = tostring(split(UserId, "@")[1])
  | project
      UserId,
      InstanceUrl,
      SenderIPv4,
      RecipientAddresses,
      Subject,
      AccountName,
      UPNSuffix,
      CloudAppId
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SenderIPv4
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: CloudAppId
      - identifier: InstanceName
        columnName: InstanceUrl
alertDetailsOverride:
  alertDisplayNameFormat: Email attachment sent externally by terminated user following
    Dataverse exfiltration alerts
  alertDescriptionFormat: 'Departing or terminated user {{UserId}} was found to send
    email to external domains not on the allowed list: {{RecipientAddresses}}'
version: 3.2.0

Stages and Predicates

Parameters

let query_frequency = 1h;

Let binding: allowed_destination_smtp_domains

let allowed_destination_smtp_domains = dynamic([
    ]);

The stages below define let exfiltration_alert_users (the rule's main pipeline source).

Stage 1: source

SecurityAlert

Stage 2: where

| where Tactics has 'Exfiltration' and Entities has_all ('account', '32780')

Stage 3: mv-expand

| mv-expand DataverseEntities = todynamic(Entities)

Stage 4: where

| where DataverseEntities.AppId == 32780

Stage 5: extend

| extend InstanceUrl = tostring(DataverseEntities.InstanceName)

Stage 6: mv-expand

| mv-expand AccountEntities = todynamic(Entities)

Stage 7: where

| where AccountEntities.Type == 'account'

Stage 8: extend

| extend
        AccountName = tostring(AccountEntities.Name),
        UPNSuffix = tostring(AccountEntities.UPNSuffix)

Stage 9: summarize

| summarize InstanceUrls = make_set(InstanceUrl, 100) by AccountName, UPNSuffix

Stage 10: extend

| extend UserId = tolower(strcat(AccountName, "@", UPNSuffix))

The stages below run on exfiltration_alert_users (the outer pipeline).

Stage 11: join

exfiltration_alert_users
| join kind=inner (
    MSBizAppsTerminatedEmployees
    | project UserId = tolower(UserPrincipalName), NotificationDate
    | where startofday(NotificationDate) <= startofday(now()))
    on UserId

Stage 12: join

| join kind=inner (
    EmailEvents
    | where TimeGenerated >= ago (query_frequency)
    | where EmailDirection == "Outbound" and AttachmentCount > 0
    | extend RecipientDomain = tolower(split(RecipientEmailAddress, '@')[1])
    | where RecipientDomain !in (allowed_destination_smtp_domains)
    | summarize
        RecipientAddresses = make_set(RecipientEmailAddress, 1000),
        Subject = make_set(Subject, 1000)
        by SenderAddress = tolower(SenderMailFromAddress), SenderIPv4)
    on $left.UserId == $right.SenderAddress

Stage 13: mv-expand

| mv-expand InstanceUrl = InstanceUrls to typeof(string)

Stage 14: extend

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

Stage 15: project

| project
    UserId,
    InstanceUrl,
    SenderIPv4,
    RecipientAddresses,
    Subject,
    AccountName,
    UPNSuffix,
    CloudAppId

Exclusions

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

FieldKindExcluded values
RecipientDomaineq[]

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
AppIdeq
  • 32780 transforms: cased
AttachmentCountgt
  • 0 transforms: cased
EmailDirectioneq
  • Outbound transforms: cased
Entitiesmatch
  • 32780
  • account
Tacticsmatch
  • Exfiltration transforms: term corpus 2 (kusto 2)
Typeeq
  • account transforms: cased corpus 13 (kusto 13)

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
CloudAppIdproject
InstanceUrlproject
RecipientAddressesproject
SenderIPv4project
Subjectproject
UPNSuffixproject
UserIdproject