Query Details

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, SPSigninIP

Explanation

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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 profile picture

David Alonso

Released: March 12, 2026

Tables

AzureActivitySigninLogsAADServicePrincipalSignInLogs

Keywords

AzureActivityCallerResourceOperationNameTimeGeneratedCallerIPAddressUserSigninLogsServicePrincipalSignInLogsRiskLevelAggregatedRiskStateConditionalAccessStatusLocationAppDisplayNameServicePrincipalNameServicePrincipalIdResultTypeSubscriptionIdResourceGroupResourceId

Operators

letbetweenago..has=~summarizecountbybinavgdcountdistinct>has_anytolowerisnotemptynot!inmake_setanyminmaxprojectextendinjoinkind=leftoutercountifcoalescetostringsplitproject-away

Actions