RULE 11 Mass Resource Deletion
Query
// Rule : Azure - Mass Deletion of Critical Resources (NSGs/VMs/Storage/VNETs/Key Vaults)
// Severity: High
// Tactics : Impact, DefenseEvasion
// MITRE : T1485, T1562
// Freq : PT1H Period: PT3H
//==========================================================================================
let LookupWindow = 1h;
let DeleteThreshold = 10;
let ResourceTypeThreshold = 2;
let KnownIaCPatterns = dynamic(["terraform", "bicep", "pipeline", "github", "pulumi", "devops", "arm-deployment", "cleanup", "deprovisioner"]);
let AzurePlatformIPs = dynamic(["168.63.", "169.254."]);
// Identities with established high-volume deletion patterns (14-day baseline)
let KnownBulkDeleters = AzureActivity
| where TimeGenerated between (ago(14d) .. ago(2h))
| where OperationNameValue has "DELETE"
| where ActivityStatusValue =~ "Success"
| summarize DailyDeletes = count() by Caller, Day = bin(TimeGenerated, 1d)
| summarize AvgDailyDeletes = avg(DailyDeletes), DaysActive = dcount(Day) by Caller
| where AvgDailyDeletes >= 10 and DaysActive >= 5
| distinct Caller;
// Critical resource DELETE operations (last 2h to capture full 1h windows)
let CriticalDeletes = AzureActivity
| where TimeGenerated > ago(2h)
| where ActivityStatusValue =~ "Success"
| where OperationNameValue has "DELETE"
| where OperationNameValue has_any (
"MICROSOFT.NETWORK/NETWORKSECURITYGROUPS",
"MICROSOFT.COMPUTE/VIRTUALMACHINES",
"MICROSOFT.STORAGE/STORAGEACCOUNTS",
"MICROSOFT.NETWORK/VIRTUALNETWORKS",
"MICROSOFT.NETWORK/SUBNETS",
"MICROSOFT.COMPUTE/DISKS",
"MICROSOFT.KEYVAULT/VAULTS",
"MICROSOFT.NETWORK/APPLICATIONGATEWAYS",
"MICROSOFT.NETWORK/LOADBALANCERS"
)
| where not(tolower(Caller) has_any (KnownIaCPatterns))
| where isnotempty(CallerIpAddress)
| where not(CallerIpAddress has_any (AzurePlatformIPs))
| where Caller !in (KnownBulkDeleters);
// Aggregate per caller/IP in 1-hour tumbling windows
let DeleteBursts = CriticalDeletes
| summarize
DeleteCount = count(),
DistinctResourceTypes = dcount(tostring(split(OperationNameValue, "/")[1])),
DeletedResources = 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),
FirstDelete = min(TimeGenerated),
LastDelete = max(TimeGenerated)
by Caller, bin(TimeGenerated, LookupWindow)
| where DeleteCount >= DeleteThreshold
| where DistinctResourceTypes >= ResourceTypeThreshold;
// Enrich: correlate with SigninLogs for suspicious human sign-ins
let UserSigninContext = SigninLogs
| where TimeGenerated > ago(3h)
| where isnotempty(IPAddress)
| project
SigninTime = TimeGenerated,
UserPrincipalName,
SigninIP = IPAddress,
UserDisplayName,
RiskLevelAggregated,
RiskState,
ConditionalAccessStatus,
Location,
AppDisplayName
| extend IsRisky = RiskLevelAggregated in ("high", "medium") or RiskState =~ "atRisk";
// Enrich: correlate with SP sign-ins
let SPSigninContext = AADServicePrincipalSignInLogs
| where TimeGenerated > ago(3h)
| where isnotempty(IPAddress)
| project
SPSigninTime = TimeGenerated,
ServicePrincipalName,
ServicePrincipalId,
SPSigninIP = IPAddress,
SPConditionalAccessStatus = ConditionalAccessStatus,
SPResultType = ResultType;
DeleteBursts
| 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)
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, SPSigninIPExplanation
This query is designed to detect suspicious mass deletions of critical Azure resources, such as Network Security Groups, Virtual Machines, Storage Accounts, Virtual Networks, and Key Vaults. Here's a simplified breakdown of what the query does:
-
Setup Parameters:
- It defines a 1-hour window for monitoring deletions and sets thresholds for the number of deletions (10) and distinct resource types (2) to trigger an alert.
- It lists known patterns related to Infrastructure as Code (IaC) and Azure platform IPs to exclude them from suspicious activity.
-
Identify Known Bulk Deleters:
- It identifies users with a history of high-volume deletions over the past 14 days, excluding them from being flagged as suspicious.
-
Detect Critical Deletions:
- It looks for successful deletion operations of critical resources in the last 2 hours, excluding known IaC patterns, Azure platform IPs, and known bulk deleters.
-
Aggregate Deletion Activity:
- It aggregates deletion activities by caller and IP address in 1-hour windows, checking if the number of deletions and resource types meet the defined thresholds.
-
Correlate with Sign-in Logs:
- It enriches the data by correlating with user and service principal sign-in logs from the last 3 hours to identify any suspicious sign-ins, such as those with high or medium risk levels.
-
Join and Extend Data:
- It joins the deletion data with sign-in data to see if there's corroborating sign-in activity, assesses the risk level of sign-ins, and extracts account details.
-
Output:
- The query outputs potential security incidents where there are bursts of deletions that are not explained by known patterns or users, and where there might be risky sign-in activity associated with the deletions.
In summary, this query helps identify potentially malicious activities involving the mass deletion of critical resources in Azure by analyzing deletion patterns and correlating them with sign-in activities.
Details

David Alonso
Released: March 12, 2026
Tables
AzureActivitySigninLogsAADServicePrincipalSignInLogs
Keywords
AzureActivityCallerResourceOperationNameTimeGeneratedIPAddressUserSigninLogsServicePrincipalSignInRiskLevelAggregatedStateConditionalAccessStatusLocationAppDisplayIdResultTypeSubscriptionGroup
Operators
letbetweenago..has=~summarizecountbybinavgdcountdistinct>has_anytolowerisnotemptynot!inmake_setanyminmaxprojectextendinjoinkind=leftoutercountifcoalescetostringsplitproject-away