Query Details

RULE 12 Mass Resource Creation

Query

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

Explanation

This 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:

  1. Setup and Thresholds:

    • The query looks at a 2-hour window and sets thresholds for detecting suspicious activity: at least 15 resource creations and at least 3 different types of resources.
  2. Known Patterns and IPs:

    • It filters out known Infrastructure as Code (IaC) patterns and Azure platform IP addresses to focus on potentially malicious activity.
  3. Resource Creation Monitoring:

    • It identifies resource creation operations (like creating virtual machines or storage accounts) that occurred successfully in the last 3 hours by unknown identities (those not seen in the last 14 days).
  4. Detecting Bursts:

    • The query looks for bursts of resource creation by these unknown identities, ensuring they meet the set thresholds.
  5. User and Service Principal Sign-in Context:

    • It enriches the data by correlating with user and service principal sign-ins from the same IP address within the last 4 hours.
    • It checks for risky sign-ins, conditional access status, and whether the sign-ins are corroborated by other data.
  6. Output:

    • The final output includes details about the suspicious activity, such as the number of resources created, types of resources, associated IP addresses, and any related sign-in activity, along with risk assessments.

This query helps security teams identify and investigate potential unauthorized or malicious resource creation activities in Azure environments.

Details

David Alonso profile picture

David Alonso

Released: March 12, 2026

Tables

AzureActivitySigninLogsAADServicePrincipalSignInLogs

Keywords

AzureActivityResourceCreationIdentitySigninLogsServicePrincipal

Operators

letbetween..agohas_any=~distinct|>nottolowerisnotempty!insummarizecountdcounttostringsplitmake_setanyminmaxbybinwhereprojectextendinjoinkind=leftoutercountifcoalescetostringproject-away

Actions