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

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 an attacker. Here's a simplified breakdown:

  1. Time Frame and Thresholds:

    • The query looks at activities within the last 3 hours and compares them against a 14-day baseline.
    • It flags cases where an unknown identity creates at least 15 resources of 3 or more different types within a 2-hour window.
  2. Known Patterns and IPs:

    • The query filters out known infrastructure-as-code patterns (like Terraform, Bicep) and Azure platform IPs to focus on potentially malicious activities.
  3. Resource Creation Monitoring:

    • It tracks specific resource creation operations (e.g., virtual machines, storage accounts) and identifies new identities that aren't in the baseline of known creators.
  4. Correlating Sign-ins:

    • It enriches the data by correlating resource creation activities 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 activities.
  5. Output:

    • The query outputs details about the suspicious activity, including the number of resources created, types of resources, associated IP addresses, and any related sign-in information.
    • It also provides risk levels and whether there is corroborating sign-in evidence.

Overall, this query helps identify potential security threats by detecting unusual patterns of resource creation by new identities, which could indicate unauthorized or malicious activities in an Azure environment.

Details

David Alonso profile picture

David Alonso

Released: July 27, 2026

Tables

AzureActivitySigninLogsAADServicePrincipalSignInLogs

Keywords

AzureActivityResourceCreationIdentityCryptojackingPersistenceImpactMITREUserSigninServicePrincipalRiskLocationAppDisplayNameConditionalAccessStatus

Operators

letbetweenagohas_any=~distinctwherenottolowerisnotemptysummarizecountdcounttostringsplitmake_setanyminmaxbybinjoinkindleftouterprojectextendincoalesceproject-away

MITRE Techniques

Actions

GitHub