Query Details
// Rule : Azure - Mass Resource Creation Burst by New Identity (Cryptojacking / Persistence)
// Severity: High
// Tactics : Impact, Persistence
// MITRE : T1496, T1136
// Freq : PT2H Period: PT4H
//==========================================================================================
let LookupWindow = 2h;
let CreationThreshold = 15;
let ResourceTypeThreshold = 3;
let KnownIaCPatterns = dynamic(["terraform", "bicep", "pipeline", "github", "pulumi", "devops", "arm-deployment", "provisioner", "deployer"]);
let AzurePlatformIPs = dynamic(["168.63.", "169.254."]);
// Resource WRITE operations we care about (creation/provisioning)
let CreationOpKeywords = dynamic([
"MICROSOFT.COMPUTE/VIRTUALMACHINES/WRITE",
"MICROSOFT.COMPUTE/VIRTUALMACHINESCALESETS/WRITE",
"MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE",
"MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/WRITE",
"MICROSOFT.WEB/SITES/WRITE",
"MICROSOFT.CONTAINERINSTANCE/CONTAINERGROUPS/WRITE",
"MICROSOFT.CONTAINERSERVICE/MANAGEDCLUSTERS/WRITE",
"MICROSOFT.KEYVAULT/VAULTS/WRITE",
"MICROSOFT.NETWORK/VIRTUALNETWORKS/WRITE",
"MICROSOFT.COMPUTE/DISKS/WRITE",
"MICROSOFT.DATABRICKS/WORKSPACES/WRITE",
"MICROSOFT.LOGIC/WORKFLOWS/WRITE"
]);
// 14-day baseline: identities with established resource creation history
let KnownCreators = AzureActivity
| where TimeGenerated between (ago(14d) .. ago(3h))
| where OperationNameValue has_any (CreationOpKeywords)
| where ActivityStatusValue =~ "Success"
| distinct Caller;
// Current window: creation bursts by UNKNOWN identities
let CreationBursts = AzureActivity
| where TimeGenerated > ago(3h)
| where ActivityStatusValue =~ "Success"
| where OperationNameValue has_any (CreationOpKeywords)
| where not(tolower(Caller) has_any (KnownIaCPatterns))
| where isnotempty(CallerIpAddress)
| where not(CallerIpAddress has_any (AzurePlatformIPs))
| where Caller !in (KnownCreators)
| summarize
CreationCount = count(),
DistinctResourceTypes = dcount(tostring(split(OperationNameValue, "/")[1])),
CreatedResources = make_set(ResourceId, 20),
Operations = make_set(OperationNameValue, 10),
ResourceGroups = make_set(ResourceGroup, 5),
SubscriptionIds = make_set(SubscriptionId, 5),
CallerIP = any(CallerIpAddress),
SourceIPs = make_set(CallerIpAddress, 5),
FirstCreate = min(TimeGenerated),
LastCreate = max(TimeGenerated)
by Caller, bin(TimeGenerated, LookupWindow)
| where CreationCount >= CreationThreshold
| where DistinctResourceTypes >= ResourceTypeThreshold;
// Enrich: correlate with user sign-ins from same IP
let UserSigninContext = SigninLogs
| where TimeGenerated > ago(4h)
| where isnotempty(IPAddress)
| project
SigninTime = TimeGenerated,
UserPrincipalName,
SigninIP = IPAddress,
UserDisplayName,
RiskLevelAggregated,
RiskState,
ConditionalAccessStatus,
Location,
AppDisplayName,
IsInteractive
| extend IsRisky = RiskLevelAggregated in ("high", "medium") or RiskState =~ "atRisk";
// Enrich: correlate with SP sign-ins from same IP
let SPSigninContext = AADServicePrincipalSignInLogs
| where TimeGenerated > ago(4h)
| where isnotempty(IPAddress)
| project
SPSigninTime = TimeGenerated,
ServicePrincipalName,
ServicePrincipalId,
SPSigninIP = IPAddress,
SPConditionalAccessStatus = ConditionalAccessStatus,
SPResultType = ResultType;
CreationBursts
| join kind=leftouter (
UserSigninContext
| summarize
UserSigninCount = count(),
SigninLocations = make_set(Location, 3),
SigninApps = make_set(AppDisplayName, 3),
MaxRisk = max(RiskLevelAggregated),
HasRiskySignin = max(toint(IsRisky)),
UserNames = make_set(UserPrincipalName, 3),
FirstSignin = min(SigninTime)
by SigninIP
) on $left.CallerIP == $right.SigninIP
| join kind=leftouter (
SPSigninContext
| summarize
SPSigninCount = count(),
SigninSPs = make_set(ServicePrincipalName, 3),
CABypassCount = countif(SPConditionalAccessStatus has_any ("failure", "notApplied"))
by SPSigninIP
) on $left.CallerIP == $right.SPSigninIP
| extend
HasSigninCorroboration = isnotempty(UserSigninCount) or isnotempty(SPSigninCount),
SigninRiskLevel = coalesce(MaxRisk, "unknown"),
AccountName = tostring(split(Caller, "@")[0]),
AccountUPNSuffix = tostring(split(Caller, "@")[1])
| project-away SigninIP, SPSigninIPThis query is designed to detect suspicious activity in Azure, specifically focusing on the mass creation of resources by new or unknown identities, which could indicate cryptojacking or persistence tactics by attackers. Here's a simplified breakdown:
Setup and Thresholds:
Known Patterns and IPs:
Resource Creation Monitoring:
Detecting Bursts:
User and Service Principal Sign-in Context:
Output:
This query helps security teams identify and investigate potential unauthorized or malicious resource creation activities in Azure environments.

David Alonso
Released: March 12, 2026
Tables
Keywords
Operators