Detection rules › Kusto
Dataverse - Login from IP not in the allow list
Identifies logons from IPv4 addresses not matching IPv4 subnets maintained on an allow list. This analytics rule uses the NetworkAddresses watchlist template.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Microsoft 365 | any: Dataverse and model-driven apps audit (catch-all) |
Rule body
id: 81c693fe-f6c4-4352-bc10-3526f6e22637
kind: Scheduled
name: Dataverse - Login from IP not in the allow list
description: Identifies logons from IPv4 addresses not matching IPv4 subnets maintained
on an allow list. This analytics rule uses the NetworkAddresses watchlist template.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Dataverse
dataTypes:
- DataverseActivity
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
- T1190
- T1133
query: |
// Use static IP address or CIDR list specified in the
// NetworkAddresses watchlist template with tag "AllowDataverse"
let allowed_networks = MSBizAppsNetworkAddresses()
| where Tags has "AllowDataverse"
| summarize by IPSubnet;
let query_frequency = 1h;
let watchlist_entries_count = toscalar (allowed_networks
| summarize count());
let dataverse_signin_activity = materialize(
DataverseActivity
| where watchlist_entries_count > 0
| where TimeGenerated >= ago (query_frequency)
| where Message == "UserSignIn" and isnotempty(ClientIp)
| summarize FirstEvent = arg_min(TimeGenerated, *) by UserId, ClientIp, InstanceUrl
);
let authorized_ip_addresses = dataverse_signin_activity
| evaluate ipv4_lookup(allowed_networks, ClientIp, IPSubnet);
dataverse_signin_activity
| join kind=leftanti(authorized_ip_addresses) on ClientIp
| extend
CloudAppId = int(32780),
AccountName = tostring(split(UserId, '@')[0]),
UPNSuffix = tostring(split(UserId, '@')[1])
| project
FirstEvent,
UserId,
ClientIp,
InstanceUrl,
CloudAppId,
AccountName,
UPNSuffix
eventGroupingSettings:
aggregationKind: AlertPerResult
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ClientIp
- entityType: CloudApplication
fieldMappings:
- identifier: AppId
columnName: CloudAppId
- identifier: InstanceName
columnName: InstanceUrl
alertDetailsOverride:
alertDisplayNameFormat: 'Dataverse - Login from IP not on the allow list in {{InstanceUrl}} '
alertDescriptionFormat: Sign-in activity detected in {{InstanceUrl}} from an IP
{{ClientIp}} not on the allow list.
version: 3.2.0
Stages and Predicates
Parameters
let query_frequency = 1h;
let dataverse_signin_activity is inlined into the numbered stages below.
Let binding: allowed_networks
let allowed_networks = MSBizAppsNetworkAddresses()
| where Tags has "AllowDataverse"
| summarize by IPSubnet;
Let binding: watchlist_entries_count
let watchlist_entries_count = toscalar (allowed_networks
| summarize count());
Let binding: authorized_ip_addresses
let authorized_ip_addresses = dataverse_signin_activity
| evaluate ipv4_lookup(allowed_networks, ClientIp, IPSubnet);
Stages 1 to 5 define let dataverse_signin_activity (the rule's main pipeline source); stages 6 to 8 run on it.
Stage 1: source
DataverseActivity
Stage 2: where
| where watchlist_entries_count > 0
Stage 3: where
| where TimeGenerated >= ago (query_frequency)
Stage 4: where
| where Message == "UserSignIn" and isnotempty(ClientIp)
Stage 5: summarize
| summarize FirstEvent = arg_min(TimeGenerated, *) by UserId, ClientIp, InstanceUrl
Stage 6: join (negated)
dataverse_signin_activity
| join kind=leftanti(authorized_ip_addresses) on ClientIp
Stage 7: extend
| extend
CloudAppId = int(32780),
AccountName = tostring(split(UserId, '@')[0]),
UPNSuffix = tostring(split(UserId, '@')[1])
Stage 8: project
| project
FirstEvent,
UserId,
ClientIp,
InstanceUrl,
CloudAppId,
AccountName,
UPNSuffix
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
ClientIp | is_not_null | excludes:ClientIp | |
Message | eq | UserSignIn | excludes:Message field:"Message" value:"UserSignIn" |
watchlist_entries_count | gt | 0 | excludes:watchlist_entries_count field:"watchlist_entries_count" value:"0" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ClientIp | is_not_null | field:"ClientIp" kind:is_not_null | |
Message | eq |
| field:"Message" kind:eq value:"UserSignIn" |
watchlist_entries_count | gt |
| field:"watchlist_entries_count" kind:gt value:"0" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
ClientIp | project |
CloudAppId | project |
FirstEvent | project |
InstanceUrl | project |
UPNSuffix | project |
UserId | project |