Detection rules › Kusto
Dataverse - User bulk retrieval outside normal activity
Identifies users retrieving significantly more records from Dataverse than they have previously in the past 2 weeks.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Exfiltration | T1048 Exfiltration Over Alternative Protocol |
Rule body kusto
id: 08cb7ffc-59c6-4e7d-88e0-327371c9431b
kind: Scheduled
name: Dataverse - User bulk retrieval outside normal activity
description: Identifies users retrieving significantly more records from Dataverse
than they have previously in the past 2 weeks.
severity: Low
status: Available
requiredDataConnectors:
- connectorId: Dataverse
dataTypes:
- DataverseActivity
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Exfiltration
relevantTechniques:
- T1048
query: |
let baseline_time = 14d;
let detection_time = 1d;
DataverseActivity
| where TimeGenerated between(ago(baseline_time) .. ago(detection_time - 1d))
| where Message == "RetrieveMultiple"
| extend numQueryCount = todouble(QueryResults)
| extend QueryCount = iif(QueryResults contains ",", todouble(countof(tostring(QueryResults), ',') + 1), numQueryCount)
| extend QueryCount = iif(isnotempty(QueryCount), QueryCount, double(1))
| summarize sum(QueryCount) by UserId
| extend HistoricalBaseline = sum_QueryCount
| join kind=inner (
DataverseActivity
| where TimeGenerated > ago(detection_time)
| where Message == "RetrieveMultiple"
| extend numQueryCount = todouble(QueryResults)
| extend QueryCount = iif(QueryResults contains ",", todouble(countof(tostring(QueryResults), ',') + 1), numQueryCount)
| extend QueryCount = iif(isnotempty(QueryCount), QueryCount, double(1))
| summarize sum(QueryCount) by UserId
| extend CurrentExportRate = sum_QueryCount)
on UserId
| where CurrentExportRate > HistoricalBaseline
| project UserId, HistoricalBaseline, CurrentExportRate
| join kind=inner(
DataverseActivity
| where TimeGenerated > ago(detection_time)
| where Message == "RetrieveMultiple"
| extend numQueryCount = todouble(QueryResults)
| extend QueryCount = iif(QueryResults contains ",", todouble(countof(tostring(QueryResults), ',') + 1), numQueryCount)
| extend QueryCount = iif(isnotempty(QueryCount), QueryCount, double(1)))
on UserId
| summarize
QuerySizes = make_set(QueryCount),
MostRecentQuery = max(TimeGenerated),
IPs = make_set(ClientIp),
UserAgents = make_set(UserAgent),
Entities = make_set(EntityName),
Queries = make_set(Query)
by UserId, InstanceUrl, HistoricalBaseline, CurrentExportRate
| extend
AccountName = tostring(split(UserId, '@')[0]),
UPNSuffix = tostring(split(UserId, '@')[1]),
CloudAppId = int(32780)
| project
MostRecentQuery,
UserId,
IPs,
UserAgents,
InstanceUrl,
Queries,
QuerySizes,
Entities,
HistoricalBaseline,
CurrentExportRate,
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
alertDetailsOverride:
alertDisplayNameFormat: Dataverse - Bulk record retrieval outside of normal activity
alertDescriptionFormat: '{{UserId}} exported {{CurrentExportRate}} records, far
beyond the historical baseline of {{{HistoricalBaseline}}.'
customDetails: {}
version: 3.2.0
Stages and Predicates
Parameters
let baseline_time = 14d;
let detection_time = 1d;
Stage 1: source
DataverseActivity
Stage 2: where
| where TimeGenerated between(ago(baseline_time) .. ago(detection_time - 1d))
Stage 3: where
| where Message == "RetrieveMultiple"
Stage 4: extend (3 consecutive steps)
| extend numQueryCount = todouble(QueryResults)
| extend QueryCount = iif(QueryResults contains ",", todouble(countof(tostring(QueryResults), ',') + 1), numQueryCount)
| extend QueryCount = iif(isnotempty(QueryCount), QueryCount, double(1))
Stage 5: summarize
| summarize sum(QueryCount) by UserId
Stage 6: extend
| extend HistoricalBaseline = sum_QueryCount
Stage 7: join
| join kind=inner (
DataverseActivity
| where TimeGenerated > ago(detection_time)
| where Message == "RetrieveMultiple"
| extend numQueryCount = todouble(QueryResults)
| extend QueryCount = iif(QueryResults contains ",", todouble(countof(tostring(QueryResults), ',') + 1), numQueryCount)
| extend QueryCount = iif(isnotempty(QueryCount), QueryCount, double(1))
| summarize sum(QueryCount) by UserId
| extend CurrentExportRate = sum_QueryCount)
on UserId
Stage 8: where
| where CurrentExportRate > HistoricalBaseline
Stage 9: project
| project UserId, HistoricalBaseline, CurrentExportRate
Stage 10: join
| join kind=inner(
DataverseActivity
| where TimeGenerated > ago(detection_time)
| where Message == "RetrieveMultiple"
| extend numQueryCount = todouble(QueryResults)
| extend QueryCount = iif(QueryResults contains ",", todouble(countof(tostring(QueryResults), ',') + 1), numQueryCount)
| extend QueryCount = iif(isnotempty(QueryCount), QueryCount, double(1)))
on UserId
Stage 11: summarize
| summarize
QuerySizes = make_set(QueryCount),
MostRecentQuery = max(TimeGenerated),
IPs = make_set(ClientIp),
UserAgents = make_set(UserAgent),
Entities = make_set(EntityName),
Queries = make_set(Query)
by UserId, InstanceUrl, HistoricalBaseline, CurrentExportRate
Stage 12: extend
| extend
AccountName = tostring(split(UserId, '@')[0]),
UPNSuffix = tostring(split(UserId, '@')[1]),
CloudAppId = int(32780)
Stage 13: project
| project
MostRecentQuery,
UserId,
IPs,
UserAgents,
InstanceUrl,
Queries,
QuerySizes,
Entities,
HistoricalBaseline,
CurrentExportRate,
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 |
CloudAppId | project |
CurrentExportRate | project |
Entities | project |
HistoricalBaseline | project |
IPs | project |
InstanceUrl | project |
MostRecentQuery | project |
Queries | project |
QuerySizes | project |
UPNSuffix | project |
UserAgents | project |
UserId | project |