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: P14D
//==========================================================================================

let LookupWindow = 1h;
// Tuned: raised deletion floor 10 -> 15 to filter routine environment teardown; kept type
// diversity at 2+ so single-resource-type cleanups (e.g., dev VM sweep) don't trigger.
let DeleteThreshold = 15;
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 potentially suspicious mass deletions of critical resources in Azure, 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 time window of 1 hour for analysis.
    • Sets thresholds for detecting suspicious activity: at least 15 deletions and at least 2 different types of resources must be involved to trigger an alert.
  2. Exclude Known Patterns:

    • It filters out deletions that match known infrastructure as code (IaC) patterns (e.g., Terraform, Bicep) and those initiated from Azure's own platform IPs.
    • It also excludes deletions by users who have a history of high-volume deletions over the past 14 days.
  3. Identify Critical Deletions:

    • It looks for successful deletion operations of critical resources within the last 2 hours.
    • It aggregates these deletions by caller and IP address in 1-hour windows, checking if they meet the defined thresholds.
  4. Correlate with Sign-in Data:

    • It enriches the deletion data by correlating it with user and service principal sign-in logs from the last 3 hours.
    • It checks for risky sign-ins (e.g., high or medium risk) and whether conditional access policies were bypassed.
  5. Output:

    • The query outputs potential incidents where there is a burst of deletions that do not match known safe patterns and may be associated with risky sign-ins.
    • It provides details such as the number of deletions, types of resources deleted, associated user or service principal sign-ins, and any risk levels identified.

Overall, this query helps in identifying and investigating potential security incidents involving unauthorized or suspicious mass deletions of critical Azure resources.

Details

David Alonso profile picture

David Alonso

Released: July 27, 2026

Tables

AzureActivitySigninLogsAADServicePrincipalSignInLogs

Keywords

AzureActivityResourcesSigninLogsServicePrincipal

Operators

letbetween..ago|wherehas=~summarizecountbybindistinct>has_anynottolowerisnotempty!indcounttostringsplitmake_setanyminmaxextendinprojectjoinkind=leftoutercountifcoalesceproject-away

MITRE Techniques

Actions

GitHub