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 | T1078 Valid Accounts, T1133 External Remote Services, T1190 Exploit Public-Facing Application |
Rule body kusto
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 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());
Derived from allowed_networks.
Let binding: authorized_ip_addresses
let authorized_ip_addresses = dataverse_signin_activity
| evaluate ipv4_lookup(allowed_networks, ClientIp, IPSubnet);
Derived from allowed_networks, dataverse_signin_activity.
The stages below define let dataverse_signin_activity (the rule's main pipeline source).
Stage 1: source
DataverseActivity
Stage 2: where
| where watchlist_entries_count > 0
References watchlist_entries_count (defined above).
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
The stages below run on dataverse_signin_activity (the outer pipeline).
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
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
ClientIp | is_not_null | |
Message | eq | UserSignIn |
watchlist_entries_count | gt | 0 |
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 |
|---|---|---|
ClientIp | is_not_null | |
Message | eq |
|
watchlist_entries_count | gt |
|
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 |
ClientIp | project |
CloudAppId | project |
FirstEvent | project |
InstanceUrl | project |
UPNSuffix | project |
UserId | project |