Detection rules › Kusto
Dataverse - New user agent type that was not used before
Identifies users accessing Dataverse from a User Agent that has not been seen in any Dataverse instance in the last 14 days.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | T1078 Valid Accounts |
| Stealth | T1036 Masquerading, T1078 Valid Accounts |
| Initial Access | T0819 Exploit Public-Facing Application, T0866 Exploitation of Remote Services |
| Lateral Movement | T0866 Exploitation of Remote Services |
Rule body kusto
id: 34a5d79b-8f9a-420c-aa64-7f4d262ac29a
kind: Scheduled
name: Dataverse - New user agent type that was not used before
description: Identifies users accessing Dataverse from a User Agent that has not been
seen in any Dataverse instance in the last 14 days.
severity: Low
status: Available
requiredDataConnectors:
- connectorId: Dataverse
dataTypes:
- DataverseActivity
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- DefenseEvasion
relevantTechniques:
- T1078
- T0866
- T0819
- T1036
query: |
let query_lookback = 14d;
let query_frequency = 1h;
let known_useragents = dynamic([
// Enter known user agents to exclude.
// example:
// "Agent1", "Agent2", "Agent3"
]);
DataverseActivity
| where TimeGenerated between(ago(query_lookback) .. ago(query_frequency))
| where isnotempty(UserAgent)
| summarize by UserAgent
| join kind = rightanti (DataverseActivity
| where TimeGenerated > ago(query_frequency)
| where not (UserId has_any ("@onmicrosoft.com", "@microsoft.com", "Unknown"))
| where isnotempty(UserAgent)
| where UserAgent !in~ (known_useragents)
| where UserAgent !hasprefix "azure-logic-apps" and UserAgent !hasprefix "PowerApps")
on UserAgent
// Exclude user agents with a render agent to reduce noise.
| join kind = leftanti(
DataverseActivity
| where TimeGenerated > ago(query_frequency)
| where UserAgent has_any ("Gecko", "WebKit", "Presto", "Trident", "EdgeHTML", "Blink"))
on UserAgent
| summarize
FirstSeen = min(TimeGenerated),
LatestIP = arg_max(ClientIp, TimeGenerated)
by UserAgent, UserId, InstanceUrl
| extend
AccountName = tostring(split(UserId, '@')[0]),
UPNSuffix = tostring(split(UserId, '@')[1]),
CloudAppId = int(32780)
| project
FirstSeen,
UserId,
UserAgent,
LatestIP,
InstanceUrl,
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: LatestIP
alertDetailsOverride:
alertDisplayNameFormat: 'Dataverse - new user agent detected in {{{InstanceUrl}} '
alertDescriptionFormat: |
{{UserId}} with new agent not seen previously in the Dataverse activity log.
Agent: {{UserAgent}}
Latest IP: {{LatestIP}}
version: 3.2.0
Stages and Predicates
Parameters
let query_lookback = 14d;
let query_frequency = 1h;
Let binding: known_useragents
let known_useragents = dynamic([
]);
Stage 1: source
DataverseActivity
Stage 2: where
| where TimeGenerated between(ago(query_lookback) .. ago(query_frequency))
Stage 3: where
| where isnotempty(UserAgent)
Stage 4: summarize
| summarize by UserAgent
Stage 5: join (negated)
| join kind = rightanti (DataverseActivity
| where TimeGenerated > ago(query_frequency)
| where not (UserId has_any ("@onmicrosoft.com", "@microsoft.com", "Unknown"))
| where isnotempty(UserAgent)
| where UserAgent !in~ (known_useragents)
| where UserAgent !hasprefix "azure-logic-apps" and UserAgent !hasprefix "PowerApps")
on UserAgent
Stage 6: join (negated)
| join kind = leftanti(
DataverseActivity
| where TimeGenerated > ago(query_frequency)
| where UserAgent has_any ("Gecko", "WebKit", "Presto", "Trident", "EdgeHTML", "Blink"))
on UserAgent
Stage 7: summarize
| summarize
FirstSeen = min(TimeGenerated),
LatestIP = arg_max(ClientIp, TimeGenerated)
by UserAgent, UserId, InstanceUrl
Stage 8: extend
| extend
AccountName = tostring(split(UserId, '@')[0]),
UPNSuffix = tostring(split(UserId, '@')[1]),
CloudAppId = int(32780)
Stage 9: project
| project
FirstSeen,
UserId,
UserAgent,
LatestIP,
InstanceUrl,
CloudAppId,
AccountName,
UPNSuffix
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
UserAgent | is_not_null | |
UserAgent | match | Gecko, WebKit, Presto, Trident, EdgeHTML, Blink |
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.
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 |
FirstSeen | project |
InstanceUrl | project |
LatestIP | project |
UPNSuffix | project |
UserAgent | project |
UserId | project |