Detection rules › Kusto
Dataverse - New user agent type that was not used with Office 365
Identifies users accessing Dynamics with a User Agent that has not been seen in any Office 365 workloads in the last 14 days.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | T1133 External Remote Services, T1190 Exploit Public-Facing Application |
Rule body kusto
id: 094b3c0a-1f63-42f7-9535-c8c7b7198328
kind: Scheduled
name: Dataverse - New user agent type that was not used with Office 365
description: Identifies users accessing Dynamics with a User Agent that has not been
seen in any Office 365 workloads in the last 14 days.
severity: Low
status: Available
requiredDataConnectors:
- connectorId: Dataverse
dataTypes:
- DataverseActivity
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
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 > 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"
| join kind = leftanti (
OfficeActivity
| where TimeGenerated between(ago(query_lookback) .. ago(query_frequency))
| where isnotempty(UserAgent)
| summarize by UserAgent)
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: CloudApplication
fieldMappings:
- identifier: AppId
columnName: CloudAppId
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: 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 > ago(query_frequency)
Stage 3: where
| where not (UserId has_any ("@onmicrosoft.com", "@microsoft.com", "Unknown"))
Stage 4: where
| where isnotempty(UserAgent)
Stage 5: where
| where UserAgent !in~ (known_useragents)
References known_useragents (defined above).
Stage 6: where
| where UserAgent !hasprefix "azure-logic-apps" and UserAgent !hasprefix "PowerApps"
Stage 7: join (negated)
| join kind = leftanti (
OfficeActivity
| where TimeGenerated between(ago(query_lookback) .. ago(query_frequency))
| where isnotempty(UserAgent)
| summarize by UserAgent)
on UserAgent
Stage 8: join (negated)
| join kind = leftanti(
DataverseActivity
| where TimeGenerated > ago(query_frequency)
| where UserAgent has_any ("Gecko", "WebKit", "Presto", "Trident", "EdgeHTML", "Blink"))
on UserAgent
Stage 9: summarize
| summarize
FirstSeen = min(TimeGenerated),
LatestIP = arg_max(ClientIp, TimeGenerated)
by UserAgent, UserId, InstanceUrl
Stage 10: extend
| extend
AccountName = tostring(split(UserId, '@')[0]),
UPNSuffix = tostring(split(UserId, '@')[1]),
CloudAppId = int(32780)
Stage 11: project
| project
FirstSeen,
UserId,
UserAgent,
LatestIP,
InstanceUrl,
CloudAppId,
AccountName,
UPNSuffix
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
UserId | match | @onmicrosoft.com, @microsoft.com, Unknown |
UserAgent | eq | [] |
UserAgent | starts_with | PowerApps |
UserAgent | starts_with | azure-logic-apps |
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.
| Field | Kind | Values |
|---|---|---|
UserAgent | is_not_null |
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 |