Detection rules › Kusto

Suspicious Resource deployment

Status
available
Severity
low
Time window
1d
Group by
Caller, CallerIpAddress, OperationNameValue
Source
github.com/Azure/Azure-Sentinel

Identifies when a rare Resource and ResourceGroup deployment occurs by a previously unseen caller.

MITRE ATT&CK coverage

TacticTechniques
ImpactT1496 Resource Hijacking

Event coverage

Rule body kusto

id: 9fb57e58-3ed8-4b89-afcf-c8e786508b1c
name: Suspicious Resource deployment
description: |
  'Identifies when a rare Resource and ResourceGroup deployment occurs by a previously unseen caller.'
severity: Low
status: Available 
requiredDataConnectors:
  - connectorId: AzureActivity
    dataTypes:
      - AzureActivity
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1496
query: |
  // Add or remove operation names below as per your requirements. For operations lists, please refer to https://learn.microsoft.com/en-us/Azure/role-based-access-control/resource-provider-operations#all
  let szOperationNames = dynamic(["Microsoft.Compute/virtualMachines/write", "Microsoft.Resources/deployments/write", "Microsoft.Resources/subscriptions/resourceGroups/write"]);
  let starttime = 14d;
  let endtime = 1d;
  let RareCaller = AzureActivity
  | where TimeGenerated between (ago(starttime) .. ago(endtime))
  | where OperationNameValue in~ (szOperationNames)
  | summarize count() by CallerIpAddress, Caller, OperationNameValue, bin(TimeGenerated,1d)
  // Returns all the records from the right side that don't have matches from the left.
  | join kind=rightantisemi (
  AzureActivity
  | where TimeGenerated > ago(endtime)
  | where OperationNameValue in~ (szOperationNames)
  | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), ActivityTimeStamp = make_set(TimeGenerated,100), ActivityStatusValue = make_set(ActivityStatusValue,100), CorrelationIds = make_set(CorrelationId,100), ResourceGroups = make_set(ResourceGroup,100), ResourceIds = make_set(_ResourceId,100), ActivityCountByCallerIPAddress = count()
  by CallerIpAddress, Caller, OperationNameValue) on CallerIpAddress, Caller, OperationNameValue;
  RareCaller
  | extend Name = iif(Caller has '@',tostring(split(Caller,'@',0)[0]),"")
  | extend UPNSuffix = iif(Caller has '@',tostring(split(Caller,'@',1)[0]),"")
  | extend AadUserId = iif(Caller !has '@',Caller,"")
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Caller
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: AadUserId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CallerIpAddress
version: 2.0.4
kind: Scheduled

Stages and Predicates

Parameters

let starttime = 14d;
let endtime = 1d;

Let binding: szOperationNames

let szOperationNames = dynamic(["Microsoft.Compute/virtualMachines/write", "Microsoft.Resources/deployments/write", "Microsoft.Resources/subscriptions/resourceGroups/write"]);

The stages below define let RareCaller (the rule's main pipeline source).

Stage 1: source

AzureActivity

Stage 2: where

| where TimeGenerated between (ago(starttime) .. ago(endtime))

Stage 3: where

| where OperationNameValue in~ (szOperationNames)

References szOperationNames (defined above).

Stage 4: summarize

| summarize count() by CallerIpAddress, Caller, OperationNameValue, bin(TimeGenerated,1d)

Stage 5: join (negated)

| join kind=rightantisemi (
AzureActivity
| where TimeGenerated > ago(endtime)
| where OperationNameValue in~ (szOperationNames)
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), ActivityTimeStamp = make_set(TimeGenerated,100), ActivityStatusValue = make_set(ActivityStatusValue,100), CorrelationIds = make_set(CorrelationId,100), ResourceGroups = make_set(ResourceGroup,100), ResourceIds = make_set(_ResourceId,100), ActivityCountByCallerIPAddress = count()
by CallerIpAddress, Caller, OperationNameValue) on CallerIpAddress, Caller, OperationNameValue

The stages below run on RareCaller (the outer pipeline).

Stage 6: extend (3 consecutive steps)

RareCaller
| extend Name = iif(Caller has '@',tostring(split(Caller,'@',0)[0]),"")
| extend UPNSuffix = iif(Caller has '@',tostring(split(Caller,'@',1)[0]),"")
| extend AadUserId = iif(Caller !has '@',Caller,"")

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
OperationNameValueinMicrosoft.Compute/virtualMachines/write, Microsoft.Resources/deployments/write, Microsoft.Resources/subscriptions/resourceGroups/write

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
OperationNameValuein
  • Microsoft.Compute/virtualMachines/write
  • Microsoft.Resources/deployments/write
  • Microsoft.Resources/subscriptions/resourceGroups/write

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
Callersummarize
CallerIpAddresssummarize
OperationNameValuesummarize
Nameextend
UPNSuffixextend
AadUserIdextend