Detection rules › Kusto
Dataverse - Mass export of records to Excel
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
| Tactic | Techniques |
|---|---|
| Exfiltration | T1567 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
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.
| Field | Kind | Values |
|---|---|---|
CurrentExportRate | gt |
|
Message | eq |
|
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.
| Field | Source |
|---|---|
AccountName | project |
BaselineThreshold | project |
ClientIp | project |
CloudAppId | project |
CurrentExportRate | project |
Details | project |
FirstEvent | project |
InstanceUrl | project |
SampleRecordIds | project |
UPNSuffix | project |
UserId | project |