RULE 19 Anonymizer Signin Priv Ops
Query
// Rule : Azure - Sign-in from Anonymizer / Tor / Anonymous IP Followed by Management Ops
// Severity: High
// Tactics : InitialAccess, DefenseEvasion
// MITRE : T1078.004
// Freq : PT30M Period: PT2H
//==========================================================================================
// AAD's own risk engine tags sign-ins with riskEventTypes containing "anonymizedIPAddress"
// or "unfamiliarFeatures". This rule fires when a *successful* sign-in with an anonymizer
// signal is followed within 1 hour by ANY Azure management-plane write/delete/action from
// the same IP — a strong indicator of attacker session takeover or password-spray success
// via Tor/VPN proxies. Correlates both SigninLogs (users) and AADServicePrincipalSignInLogs
// (workload identities).
//
// FP mitigations:
// * ResultType == 0 (successful auth only — noisy failed anonymizer attempts filtered out).
// * Requires at least 1 management-plane WRITE/DELETE/ACTION within 1h from the same IP.
// * Excludes IaC caller name patterns.
// * Excludes RFC1918 and Azure platform IPs (anonymizer IPs are always public).
let AnonymizerRiskTypes = dynamic(["anonymizedIPAddress", "anonymousIPAddress"]);
let PrivateOrPlatform = dynamic(["10.", "172.16.", "172.17.", "172.18.", "172.19.",
"172.20.", "172.21.", "172.22.", "172.23.", "172.24.", "172.25.", "172.26.",
"172.27.", "172.28.", "172.29.", "172.30.", "172.31.", "192.168.", "127.",
"169.254.", "168.63."]);
let ExcludedCallerPatterns = dynamic(["terraform", "bicep", "pipeline", "github",
"pulumi", "devops", "arm-deployment"]);
// --- Interactive user sign-ins from anonymizer IPs (successful) ---
let AnonUserSignins = SigninLogs
| where TimeGenerated > ago(2h)
| where ResultType == 0
| where isnotempty(IPAddress)
| where not(IPAddress has_any (PrivateOrPlatform))
// column_ifexists() keeps the rule deployable where RiskEventTypes_V2 / RiskEventTypes
// are not present in the workspace SigninLogs schema (schema drift across connectors).
| extend RiskTypesText = strcat(
tostring(column_ifexists("RiskEventTypes_V2", dynamic([]))), " ",
tostring(column_ifexists("RiskEventTypes", dynamic([]))))
| where RiskTypesText has_any (AnonymizerRiskTypes)
or RiskLevelAggregated in~ ("medium", "high")
| project SigninTime = TimeGenerated, UserPrincipalName, AnonIP = IPAddress,
UserDisplayName, AppDisplayName, Location, RiskLevelAggregated,
RiskEventTypes = RiskTypesText, ConditionalAccessStatus;
// --- Service principal sign-ins from anonymizer IPs (successful) ---
let AnonSPSignins = AADServicePrincipalSignInLogs
| where TimeGenerated > ago(2h)
| where ResultType == 0
| where isnotempty(IPAddress)
| where not(IPAddress has_any (PrivateOrPlatform))
// AADServicePrincipalSignInLogs carries no Identity-Protection risk columns in most
// workspaces; column_ifexists() keeps this branch valid (and inert) rather than failing
// semantic validation. It activates automatically if the columns are ever present.
| where strcat(
tostring(column_ifexists("RiskEventTypes_V2", dynamic([]))), " ",
tostring(column_ifexists("RiskEventTypes", dynamic([])))) has_any (AnonymizerRiskTypes)
| project SPSigninTime = TimeGenerated, ServicePrincipalName, ServicePrincipalId,
AnonIP = IPAddress, AppId, SPConditionalAccessStatus = ConditionalAccessStatus;
// --- Combined anonymizer sign-ins (one column per identity kind) ---
let AllAnonSignins = AnonUserSignins
| project AnonIP, SigninTime, Identity = UserPrincipalName, IdentityKind = "User",
Context = strcat("app=", AppDisplayName, "; loc=", tostring(Location),
"; risk=", tostring(RiskLevelAggregated))
| union (
AnonSPSignins
| project AnonIP, SigninTime = SPSigninTime, Identity = ServicePrincipalName,
IdentityKind = "ServicePrincipal",
Context = strcat("appId=", AppId, "; caStatus=", tostring(SPConditionalAccessStatus))
);
// --- Management-plane writes/deletes/actions from same IPs, within 1h AFTER sign-in ---
AzureActivity
| where TimeGenerated > ago(2h)
| where ActivityStatusValue =~ "Success"
| where OperationNameValue has_any ("WRITE", "DELETE", "ACTION")
| where not(OperationNameValue has_any ("READ", "LIST", "GET"))
| where not(tolower(Caller) has_any (ExcludedCallerPatterns))
| where isnotempty(CallerIpAddress)
| where not(CallerIpAddress has_any (PrivateOrPlatform))
| join kind=inner AllAnonSignins on $left.CallerIpAddress == $right.AnonIP
| where TimeGenerated >= SigninTime and TimeGenerated <= SigninTime + 1h
| summarize
MGMTOpCount = count(),
DistinctOps = dcount(OperationNameValue),
Operations = make_set(OperationNameValue, 10),
AffectedResources = make_set(ResourceId, 10),
SubscriptionIds = make_set(SubscriptionId, 5),
CallerIP = any(CallerIpAddress),
SigninIdentity = any(Identity),
IdentityKind = any(IdentityKind),
SigninContext = any(Context),
FirstMgmtOp = min(TimeGenerated),
LastMgmtOp = max(TimeGenerated),
SigninTime = any(SigninTime)
by Caller, AnonIP
// Even a single WRITE from an anonymizer IP is high-signal; require just >=1 op.
| where MGMTOpCount >= 1
| extend
AccountName = coalesce(tostring(split(Caller, "@")[0]), SigninIdentity),
AccountUPNSuffix = tostring(split(Caller, "@")[1])Explanation
This KQL (Kusto Query Language) query is designed to detect potential security threats in Azure by identifying suspicious sign-in activities followed by management operations. Here's a simple breakdown of what the query does:
-
Purpose: The query aims to identify successful sign-ins from anonymized IP addresses (such as those using Tor or VPNs) that are followed by management operations (like write, delete, or action) within one hour. This pattern is a strong indicator of a potential attacker session takeover or a successful password spray attack.
-
Severity and Tactics: The rule is marked with high severity and is associated with tactics like Initial Access and Defense Evasion, as per the MITRE ATT&CK framework (specifically T1078.004).
-
Data Sources: The query analyzes data from two main sources:
- SigninLogs: Logs of user sign-ins.
- AADServicePrincipalSignInLogs: Logs of service principal sign-ins.
-
Filtering Criteria:
- Only considers successful sign-ins (ResultType == 0).
- Excludes private or Azure platform IPs, focusing only on public anonymizer IPs.
- Looks for risk event types related to anonymized IP addresses or unfamiliar features.
- Excludes certain caller patterns related to infrastructure as code (IaC) tools like Terraform, Bicep, etc.
-
Correlation:
- It correlates sign-in events with management operations (from AzureActivity logs) that occur within one hour after the sign-in from the same IP address.
- Only considers operations that are not read, list, or get, focusing on more impactful actions like write or delete.
-
Output:
- Summarizes the number of management operations, distinct operations, affected resources, and other relevant details.
- Provides information about the caller, the IP address used, and the context of the sign-in.
-
Mitigation of False Positives:
- The query is designed to minimize false positives by excluding failed sign-in attempts and focusing on successful authentications followed by impactful operations.
Overall, this query helps security teams identify and investigate potentially malicious activities in Azure environments by flagging unusual sign-in patterns and subsequent management actions.
Details

David Alonso
Released: July 27, 2026
Tables
Keywords
Operators
MITRE Techniques