Detection rules › Kusto
Dataverse - Suspicious use of TDS endpoint
Identifies Dataverse TDS (Tabular Data Stream) protocol based queries where the source user or IP address has recent security alerts and the TDS protocol has not been used previously in the target environment.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | T1190 Exploit Public-Facing Application |
| Exfiltration | T1048 Exfiltration Over Alternative Protocol |
Rule body kusto
id: d875af10-6bb9-4d6a-a6e4-78439a98bf4b
kind: Scheduled
name: Dataverse - Suspicious use of TDS endpoint
description: Identifies Dataverse TDS (Tabular Data Stream) protocol based queries
where the source user or IP address has recent security alerts and the TDS protocol
has not been used previously in the target environment.
severity: Low
status: Available
requiredDataConnectors:
- connectorId: Dataverse
dataTypes:
- DataverseActivity
- connectorId: AzureActiveDirectoryIdentityProtection
dataTypes:
- SecurityAlert
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Exfiltration
- InitialAccess
relevantTechniques:
- T1048
- T1190
query: |
let query_frequency = 1h;
let query_lookback = 14d;
DataverseActivity
| where TimeGenerated >= ago(query_frequency)
| where Message == 'ExecutePowerBISql'
| summarize FirstEvent = min(TimeGenerated) by UserId, ClientIp, InstanceUrl
| join kind=inner(
DataverseActivity
| where TimeGenerated >= ago(query_lookback)
| where Message == 'ExecutePowerBISql'
| summarize UniqueUsers = dcount(UserId, 4) by InstanceUrl)
on InstanceUrl
| where UniqueUsers == 1
| join kind=inner (
SecurityAlert
| where Entities has ('"Type":"ip"')
| project AlertName, SystemAlertId, Entities
| mv-expand todynamic(Entities)
| where Entities.Type == "ip"
| extend IPAddress = tostring(Entities.Address)
| summarize SystemAlerts = make_set(SystemAlertId, 100), Alerts = make_set(AlertName, 100) by IPAddress)
on $left.ClientIp == $right.IPAddress
| extend
CloudAppId = int(32780),
AccountName = tostring(split(UserId, '@')[0]),
UPNSuffix = tostring(split(UserId, '@')[1])
| join kind = inner (
SecurityAlert
| where Entities has ('Type":"account"')
| project AlertName, SystemAlertId, Entities
| mv-expand todynamic(Entities)
| where Entities.Type == "account"
| extend
UPNSuffix = tostring(Entities.UPNSuffix),
AccountName = tostring(Entities.Name)
| summarize SystemAlerts = make_set(SystemAlertId, 100), Alerts = make_set(AlertName, 100) by AccountName, UPNSuffix
| where isnotempty(AccountName) and isnotempty(UPNSuffix))
on AccountName, UPNSuffix
| summarize SystemAlerts = make_set(SystemAlerts, 100), Alerts = make_set(Alerts, 100) by FirstEvent, UserId, ClientIp, InstanceUrl, AccountName, UPNSuffix
| extend CloudAppId = int(32780)
| project
FirstEvent,
UserId,
ClientIp,
InstanceUrl,
Alerts,
SystemAlerts,
CloudAppId,
AccountName,
UPNSuffix
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 - Suspicious use of TDS endpoint in {{InstanceUrl}} '
alertDescriptionFormat: 'The TDS endpoint was used to query Dataverse instance {{InstanceUrl}}
. The use of this protocol was not seen previously and the following alerts were
associated with the caller: {{Alerts}}'
version: 3.2.0
Stages and Predicates
Parameters
let query_frequency = 1h;
let query_lookback = 14d;
Stage 1: source
DataverseActivity
Stage 2: where
| where TimeGenerated >= ago(query_frequency)
Stage 3: where
| where Message == 'ExecutePowerBISql'
Stage 4: summarize
| summarize FirstEvent = min(TimeGenerated) by UserId, ClientIp, InstanceUrl
Stage 5: join
| join kind=inner(
DataverseActivity
| where TimeGenerated >= ago(query_lookback)
| where Message == 'ExecutePowerBISql'
| summarize UniqueUsers = dcount(UserId, 4) by InstanceUrl)
on InstanceUrl
Stage 6: where
| where UniqueUsers == 1
Stage 7: join
| join kind=inner (
SecurityAlert
| where Entities has ('"Type":"ip"')
| project AlertName, SystemAlertId, Entities
| mv-expand todynamic(Entities)
| where Entities.Type == "ip"
| extend IPAddress = tostring(Entities.Address)
| summarize SystemAlerts = make_set(SystemAlertId, 100), Alerts = make_set(AlertName, 100) by IPAddress)
on $left.ClientIp == $right.IPAddress
Stage 8: extend
| extend
CloudAppId = int(32780),
AccountName = tostring(split(UserId, '@')[0]),
UPNSuffix = tostring(split(UserId, '@')[1])
Stage 9: join
| join kind = inner (
SecurityAlert
| where Entities has ('Type":"account"')
| project AlertName, SystemAlertId, Entities
| mv-expand todynamic(Entities)
| where Entities.Type == "account"
| extend
UPNSuffix = tostring(Entities.UPNSuffix),
AccountName = tostring(Entities.Name)
| summarize SystemAlerts = make_set(SystemAlertId, 100), Alerts = make_set(AlertName, 100) by AccountName, UPNSuffix
| where isnotempty(AccountName) and isnotempty(UPNSuffix))
on AccountName, UPNSuffix
Stage 10: summarize
| summarize SystemAlerts = make_set(SystemAlerts, 100), Alerts = make_set(Alerts, 100) by FirstEvent, UserId, ClientIp, InstanceUrl, AccountName, UPNSuffix
Stage 11: extend
| extend CloudAppId = int(32780)
Stage 12: project
| project
FirstEvent,
UserId,
ClientIp,
InstanceUrl,
Alerts,
SystemAlerts,
CloudAppId,
AccountName,
UPNSuffix
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 |
|---|---|---|
AccountName | is_not_null | |
Entities | match |
|
Message | eq |
|
Type | eq |
|
UPNSuffix | is_not_null | |
UniqueUsers | 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 |
Alerts | project |
ClientIp | project |
CloudAppId | project |
FirstEvent | project |
InstanceUrl | project |
SystemAlerts | project |
UPNSuffix | project |
UserId | project |