Detection rules › Kusto

Dataverse - Mass export of records to Excel

Status
available
Severity
low
Time window
14d
Group by
HistoricalUserId, InstanceUrl, UserId
Source
github.com/Azure/Azure-Sentinel

Identifies users exporting a large amount of records from Dynamics 365 to Excel, significantly more records exported than any other recent activity by that user. Large exports from users with no recent activity are identified using a predefined threshold.

MITRE ATT&CK coverage

TacticTechniques
ExfiltrationT1567 Exfiltration Over Web Service

Rule body kusto

id: 57000f0d-ff5d-4166-94b6-aa5fb62b16ec
kind: Scheduled
name: Dataverse - Mass export of records to Excel
description: Identifies users exporting a large amount of records from Dynamics 365
  to Excel, significantly more records exported than any other recent activity by
  that user. Large exports from users with no recent activity are identified using
  a predefined threshold.
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: Dataverse
    dataTypes:
      - DataverseActivity
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1567
query: |
  // Set a mass export threshold for users who have no historical activity.
  let mass_export_threshold = 10000;
  let query_lookback = 14d;
  let query_frequency = 1h;
  let export_activity = DataverseActivity
      | where Message == "ExportToExcel"
      | extend QueryCount = iif(QueryResults has ",", todouble(countof(tostring(QueryResults), ',') + 1), double(1));
  let current_activity = export_activity
      | where TimeGenerated > ago(query_frequency)
      | extend RecordId = split(QueryResults, ",")
      | summarize
          FirstEvent = min(TimeGenerated),
          CurrentExportRate = sum(QueryCount),
          SampleRecordIds = make_set(RecordId, 1000)
          by UserId, InstanceUrl;
  let historical_activity = export_activity
      | where TimeGenerated between(ago(query_lookback) .. ago(query_frequency))
      | summarize HistoricalBaseline = sum(QueryCount) by HistoricalUserId = UserId, InstanceUrl;
  current_activity
  | join kind=leftouter(historical_activity) on $left.UserId == $right.HistoricalUserId, InstanceUrl
  | extend BaselineThreshold = iif(isnotnull(HistoricalBaseline), HistoricalBaseline, todouble(mass_export_threshold))
  | where CurrentExportRate > BaselineThreshold
  | join kind=inner(export_activity
      | where TimeGenerated > ago(query_frequency)
      | summarize EntityCount = sum(QueryCount) by UserId, ClientIp, InstanceUrl, EntityName
      | extend Details = bag_pack("EntityName", EntityName, "EntityCount", EntityCount)
      | summarize Details = make_set(Details, 100) by UserId, ClientIp, InstanceUrl)
      on UserId, InstanceUrl
  | extend
      CloudAppId = int(32780),
      AccountName = tostring(split(UserId, '@')[0]),
      UPNSuffix = tostring(split(UserId, '@')[1])
  | project
      FirstEvent,
      UserId,
      ClientIp,
      BaselineThreshold,
      CurrentExportRate,
      Details,
      SampleRecordIds,
      InstanceUrl,
      AccountName,
      UPNSuffix,
      CloudAppId
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: CloudAppId
      - identifier: InstanceName
        columnName: InstanceUrl
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
alertDetailsOverride:
  alertDisplayNameFormat: 'Dataverse - mass export to Excel activity in {{{InstanceUrl}} '
  alertDescriptionFormat: User {{UserId}} exported {{{CurrentExportRate}} records
    using the ExportToExcel function in Dataverse.
version: 3.2.0

Stages and Predicates

Parameters

let mass_export_threshold = 10000;
let query_lookback = 14d;
let query_frequency = 1h;

Let binding: historical_activity

let historical_activity = export_activity
    | where TimeGenerated between(ago(query_lookback) .. ago(query_frequency))
    | summarize HistoricalBaseline = sum(QueryCount) by HistoricalUserId = UserId, InstanceUrl;

Derived from query_lookback, query_frequency, export_activity.

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

Stage 1: source

DataverseActivity

Stage 2: where

| where Message == "ExportToExcel"

Stage 3: extend

| extend QueryCount = iif(QueryResults has ",", todouble(countof(tostring(QueryResults), ',') + 1), double(1))

Stage 4: where

| where TimeGenerated > ago(query_frequency)

Stage 5: extend

| extend RecordId = split(QueryResults, ",")

Stage 6: summarize

| summarize
        FirstEvent = min(TimeGenerated),
        CurrentExportRate = sum(QueryCount),
        SampleRecordIds = make_set(RecordId, 1000)
        by UserId, InstanceUrl
Threshold
gt BaselineThreshold

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

Stage 7: join

current_activity
| join kind=leftouter(historical_activity) on $left.UserId == $right.HistoricalUserId, InstanceUrl

Stage 8: extend

| extend BaselineThreshold = iif(isnotnull(HistoricalBaseline), HistoricalBaseline, todouble(mass_export_threshold))

Stage 9: where

| where CurrentExportRate > BaselineThreshold

Stage 10: join

| join kind=inner(export_activity
    | where TimeGenerated > ago(query_frequency)
    | summarize EntityCount = sum(QueryCount) by UserId, ClientIp, InstanceUrl, EntityName
    | extend Details = bag_pack("EntityName", EntityName, "EntityCount", EntityCount)
    | summarize Details = make_set(Details, 100) by UserId, ClientIp, InstanceUrl)
    on UserId, InstanceUrl

Stage 11: extend

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

Stage 12: project

| project
    FirstEvent,
    UserId,
    ClientIp,
    BaselineThreshold,
    CurrentExportRate,
    Details,
    SampleRecordIds,
    InstanceUrl,
    AccountName,
    UPNSuffix,
    CloudAppId

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
CurrentExportRategt
  • BaselineThreshold transforms: cased
Messageeq
  • ExportToExcel 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
BaselineThresholdproject
ClientIpproject
CloudAppIdproject
CurrentExportRateproject
Detailsproject
FirstEventproject
InstanceUrlproject
SampleRecordIdsproject
UPNSuffixproject
UserIdproject