Fixing the "Rare RDP Connections" Analytic Rule
Ghost Post Settings
Alright class.
Fifth lesson in this series. We have fixed "MFA Rejected by User", "Privileged Role Assigned Outside PIM", "New User Assigned to Privileged Role", and "Attempt to Bypass Conditional Access Rule". Today we take apart "Rare RDP Connections".
When I first put this together, I kept the original rule's data source, the Windows Security Event log, as the primary and left Defender for Endpoint as an optional swap. On reflection that was the wrong call. That table has a coverage flaw that quietly breaks the whole detection, and the better source was there all along. So this is not a fix-up. It is a full rebuild on DeviceLogonEvents.
So if you ingest Security Events but not Defender DeviceLogonEvents, feel free to drop this one. It is not for you.
The Silent Coverage Problem
Here is why Security Events had to go.
The original reads SecurityEvent and WindowsEvent, the Windows Security log carried by the legacy MMA and the modern AMA. On paper that is the broad-compatibility choice and it needs no Defender licensing. In practice it only sees a host if that host forwards event 4624 with logon auditing on and the data collection rule scoped to include it. Most estates do not collect Security Events from everywhere. I would bet you do not have AMA on anywhere near all of your devices. It is expensive, so it gets scoped to domain controllers and a tier of important servers, and the user fleet and half the on-prem boxes stay behind the "we never got around to finishing that Azure Arc project" excuse.
The moment that happens, a rare-RDP rule on Security Events is blind on every host outside the collection scope, and it never tells you. It does not error. It does not warn. It simply never fires on the machines you are not watching, and a detection that fails by silent omission is the most dangerous kind, because you believe you have coverage you do not.
DeviceLogonEvents does not have this problem. If a device is onboarded to Defender for Endpoint, its logons are in the table, assuming you ticked the box in the XDR data connector.

What the New Source Gives Us
Moving to DeviceLogonEvents is not just a cleaner pipe. The schema carries fields the Security Event log never had, and each one removes a hack.
RemoteIPType. Microsoft classifies the source as Public or Private for you. The Security Event approach needed ipv4_is_private, and the popular community version did it with octet prefix matching that does not even respect CIDR boundaries. Gone. External is now a column check: RemoteIPType == "Public".
RemoteDeviceName. The source hostname, and it changes the detection. A workstation keeps its name across every DHCP lease it is ever handed, so it is a stable key where an IP is not.
IsLocalAdmin. The table tells you whether the session landed with local administrator rights on the target. An interactive session arriving as a local admin is a materially worse outcome, and now it is a single boolean.
Rarity, Done Properly
The detection keeps the first-seen idea but gives it two stable dimensions instead of one brittle one.
NewRdpEdge is the destination relationship: this account has not opened an RDP session to this host inside the baseline window. It catches movement to somewhere new, internal or external. The important part is that it baselines on RDP only. The original baselined against every logon type, so constant type 3 network noise meant almost every account-to-host pair already looked known, and real first-time RDP was silenced.
NewSourceHost is the origin relationship: this account has not opened RDP from this source host before. Because the key is a hostname and not an address, it is low-noise. A genuinely new pivot origin stands out; a laptop on a new DHCP lease does not register, because its name did not change.
The firing condition combines those with the external case:
| where IsNewEdge or IsNewSourceHost or ExternalSource
A new destination fires. A new source host fires. And any RemoteInteractive logon from a public source fires, because in a managed estate inbound RDP from the internet to an endpoint should be rare to nonexistent. If that branch is loud, the rule has not produced false positives. It has found an exposure.
The Blind Spot You Should Know About
The honest trade: DeviceLogonEvents only covers devices onboarded to Defender for Endpoint. That is the inverse of the Security Event weakness, not its removal. A host you collect Security Events from but have not onboarded was visible to the old rule and is invisible to this one. If you have critical servers, appliances, or non-Windows systems off MDE, that is a gap, and the answer is to onboard them, not to fall back to the table with the silent-coverage problem.
One more: this is success-only. It keys on LogonSuccess and does not see the failed RemoteInteractive attempts that precede a brute force. Those live in the same table under ActionType LogonFailed and make a natural companion. RDP nesting, where an operator hops RDP to RDP across hosts, is a different problem with its own data source, and it gets its own post.
MITRE Mappings for the Updated Rule
Tactic: Lateral Movement, with Initial Access for the external-source case.
T1021.001 Remote Desktop Protocol. The precise mapping and the technique the rule exists to watch.
T1078.002 and T1078.003 Valid Accounts, Domain and Local. A valid credential performing the RDP is what the identity context and the new-source signal are built around.
T1133 External Remote Services. The external-source branch covers a valid account reaching in over RDP from a public address.
Rule Settings
Run every 60 minutes with a 60 minute query period. The original ran daily, which for lateral movement is far too slow. The baselines still reach back the full 14 days independently. DeviceLogonEvents can carry ingestion latency, so widen DetectionWindow if your lag runs over an hour. Medium severity, raised by the indicators. Alert per result. Group by Account and Host entities with a 6 hour lookback.
Entity mapping:
- AccountName to Account (Name), AccountDomain to Account (NTDomain)
- HostName to Host (HostName), HostNameDomain to Host (DnsDomain)
- RemoteIP to IP (Address)
- Optionally map RemoteDeviceName to a second Host entity for the source host
Custom details to surface in the incident: RiskIndicators, RemoteIPType, RemoteDeviceName, IsLocalAdmin, ConnectionCount, AccountUPN, IsAccountEnabled, RiskLevel.
KQL
// =====================================================================
// Rare RDP Connection - Defender for Endpoint (DeviceLogonEvents)
// =====================================================================
// Description : Detects RemoteInteractive (RDP) logons that are new by destination
// (account has not RDP'd to this host in 14 days), new by source host,
// or arriving from a public source, with local-admin and identity context.
// Rebuilt on DeviceLogonEvents to remove the Security Event silent-coverage
// problem and to use native source classification.
// Type : Detection
//
// Tables : DeviceLogonEvents, IdentityInfo
// Connectors : Microsoft Defender for Endpoint (DeviceLogonEvents),
// Microsoft Sentinel UEBA (IdentityInfo)
// License : Microsoft Defender for Endpoint P2 + Microsoft Sentinel;
// Microsoft Entra ID P2 recommended (UEBA / IdentityInfo enrichment)
//
// Tuning : - DetectionWindow - align to run frequency AND to DeviceLogonEvents ingestion latency
// - BaselineWindow - history depth for the new-edge / new-source-host baselines
// - ExcludedHostRegex - AVD / RDS pools, jump hosts, bastions where RDP is normal
// (default \b\B matches nothing; replace the pattern to enable, do NOT set it to "")
// - ExcludedAccounts - known admin / monitoring / break-glass accounts that RDP broadly
// - SensitiveRoles - directory roles that set the PrivilegedAccount indicator
// - Join IdentityInfo on AccountSid instead of name if your IdentityInfo carries the on-prem SID
//
// Known FPs : - Help desk / admin jump hosts that legitimately RDP to many servers - exclude host or account
// - AVD / RDS session-host pools where RemoteInteractive is the normal access path - ExcludedHostRegex
// - First RDP to a freshly built server during provisioning - baseline clears within 14 days
// - VPN remote workers arriving from a new public egress IP - triaged by identity context, not suppressed
//
// Author : Bartosz Wysocki | https://www.itprofessor.cloud
// Version : 2.2 | 2026-06-17
// =====================================================================
let DetectionWindow = 1h;
let BaselineWindow = 14d;
let IdentityLookback = 30d;
let ExcludedHostRegex = @"\b\B"; // never-matches default (excludes nothing); replace e.g. with "(?i)(AVD|RDS|JUMP|BASTION)"
let ExcludedAccounts = dynamic([]); // e.g. ["svc-monitoring", "breakglass"] matched on SAM name
let SensitiveRoles = dynamic([
"Global Administrator",
"Privileged Role Administrator",
"Privileged Authentication Administrator",
"Security Administrator",
"Exchange Administrator",
"SharePoint Administrator",
"User Administrator",
"Intune Administrator",
"Application Administrator",
"Hybrid Identity Administrator",
"Domain Admins",
"Enterprise Admins"
]);
// Normalize successful RDP logons from DeviceLogonEvents into one schema
let RdpLogons = (windowStart:datetime, windowEnd:datetime) {
DeviceLogonEvents
| where Timestamp between (windowStart .. windowEnd)
| where ActionType == "LogonSuccess"
| where LogonType == "RemoteInteractive"
| project
Timestamp,
DeviceId,
DeviceName = toupper(DeviceName),
Account = tolower(strcat(AccountDomain, "\\", AccountName)),
AccountName = tolower(AccountName),
AccountDomain,
AccountSid,
RemoteIP,
RemoteIPType,
RemoteDeviceName = toupper(RemoteDeviceName),
Protocol,
IsLocalAdmin
| extend AccountType = case(
AccountName endswith "$" or AccountSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine",
isempty(AccountSid), "Unknown",
"User")
};
// 14-day RDP history, materialized once, used to derive both baselines
let BaselineRdp = materialize(RdpLogons(ago(BaselineWindow), ago(DetectionWindow)));
let BaselineEdges = BaselineRdp | distinct Account, DeviceName | extend SeenEdge = true;
let BaselineSourceHosts = BaselineRdp
| where isnotempty(RemoteDeviceName)
| distinct Account, RemoteDeviceName
| extend SeenSourceHost = true;
// Identity context, keyed on the normalized SAM account name (swap to AccountSid if available)
let IdentityContext = IdentityInfo
| where TimeGenerated > ago(IdentityLookback)
| extend NormalizedAccountName = tolower(trim(" ", AccountName))
| summarize arg_max(TimeGenerated, AccountDisplayName, AccountUPN, IsAccountEnabled, UserType, AssignedRoles, GroupMembership, RiskLevel, RiskState) by NormalizedAccountName;
// Recent RDP in the detection window
let RecentRdp = RdpLogons(ago(DetectionWindow), now())
| summarize
StartTime = min(Timestamp),
EndTime = max(Timestamp),
ConnectionCount = count()
by Account, AccountName, AccountDomain, AccountSid, AccountType, DeviceId, DeviceName,
RemoteIP, RemoteIPType, RemoteDeviceName, Protocol, IsLocalAdmin;
RecentRdp
| join kind=leftouter BaselineEdges on Account, DeviceName
| join kind=leftouter BaselineSourceHosts on Account, RemoteDeviceName
| extend IsNewEdge = isnull(SeenEdge)
| extend IsNewSourceHost = isnotempty(RemoteDeviceName) and isnull(SeenSourceHost)
| extend ExternalSource = RemoteIPType =~ "Public"
| where IsNewEdge or IsNewSourceHost or ExternalSource
| extend HostName = toupper(tostring(split(DeviceName, ".")[0]))
| where not(DeviceName matches regex ExcludedHostRegex)
| where array_length(ExcludedAccounts) == 0 or AccountName !in~ (ExcludedAccounts)
| join kind=leftouter IdentityContext on $left.AccountName == $right.NormalizedAccountName
| extend DomainIndex = toint(indexof(DeviceName, "."))
| extend HostNameDomain = iff(DomainIndex != -1, substring(DeviceName, DomainIndex + 1), DeviceName)
| extend Risk_1 = iff(IsNewEdge, "NewRdpEdge", "")
| extend Risk_2 = iff(IsNewSourceHost, "NewSourceHost", "")
| extend Risk_3 = iff(ExternalSource, "ExternalSourceIP", "")
| extend Risk_4 = iff(IsLocalAdmin == true, "LocalAdminLogon", "")
| extend Risk_5 = iff(tostring(AssignedRoles) has_any (SensitiveRoles), "PrivilegedAccount", "")
| extend Risk_6 = iff(IsAccountEnabled == false or UserType =~ "Guest", "DisabledOrGuestAccount", "")
| extend Risk_7 = iff(AccountType == "Machine", "MachineAccountRDP", "")
| extend RiskIndicators = trim(@"\s\|\s*$", strcat(
iff(isnotempty(Risk_1), strcat(Risk_1, " | "), ""),
iff(isnotempty(Risk_2), strcat(Risk_2, " | "), ""),
iff(isnotempty(Risk_3), strcat(Risk_3, " | "), ""),
iff(isnotempty(Risk_4), strcat(Risk_4, " | "), ""),
iff(isnotempty(Risk_5), strcat(Risk_5, " | "), ""),
iff(isnotempty(Risk_6), strcat(Risk_6, " | "), ""),
iff(isnotempty(Risk_7), strcat(Risk_7, " | "), "")
))
| project
StartTime, EndTime, RiskIndicators,
Account, AccountName, AccountDomain, AccountType, AccountSid, AccountUPN, AccountDisplayName,
DeviceId, DeviceName, HostName, HostNameDomain,
RemoteIP, RemoteIPType, RemoteDeviceName,
IsNewEdge, IsNewSourceHost, ExternalSource,
IsLocalAdmin, Protocol, ConnectionCount,
IsAccountEnabled, UserType, AssignedRoles, GroupMembership, RiskLevel, RiskState
| sort by StartTime descYou can also download this as an analytic rule and import it directly to Sentinel.
Follow my repo - GitHub
What You Should Do Next
- Confirm your onboarding coverage first. List the devices you consider critical and check they are onboarded to Defender for Endpoint. Anywhere they are not, this rule sees nothing.
- Run both baselines manually over the last 14 days before deploying. The edge baseline shows how many distinct account-to-host RDP relationships exist; the source-host baseline shows your normal origins. If either is enormous, your RDP hygiene is the finding.
- Populate ExcludedHostRegex and ExcludedAccounts up front. AVD and RDS pools, jump hosts, and bastions generate constant RemoteInteractive activity and will otherwise dominate the output. That is tuning, not weakening the detection.
- Build the LogonFailed companion (if you want additional audit type of reporting) This rule catches the successful session; the failed-then-succeeded brute-force arc lives in the same table under ActionType LogonFailed.
Class dismissed