Detection rules › Kusto

Rare subscription-level operations in Azure

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

This query looks for a few sensitive subscription-level events based on Azure Activity Logs. For example, this monitors for the operation name 'Create or Update Snapshot', which is used for creating backups but could be misused by attackers to dump hashes or extract sensitive information from the disk.

MITRE ATT&CK coverage

TacticTechniques
Persistence
Credential Access

Telemetry coverage

Rule body

id: 23de46ea-c425-4a77-b456-511ae4855d69
name: Rare subscription-level operations in Azure
description: |
  'This query looks for a few sensitive subscription-level events based on Azure Activity Logs. For example, this monitors for the operation name 'Create or Update Snapshot', which is used for creating backups but could be misused by attackers to dump hashes or extract sensitive information from the disk.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: AzureActivity
    dataTypes:
      - AzureActivity
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
  - Persistence
relevantTechniques:
  - T1003
  - T1098
query: |
  let starttime = 14d;
  let endtime = 1d;
  // The number of operations above which an IP address is considered an unusual source of role assignment operations
  let alertOperationThreshold = 5;
  // 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 SensitiveOperationList =  dynamic(["microsoft.compute/snapshots/write", "microsoft.network/networksecuritygroups/write", "microsoft.storage/storageaccounts/listkeys/action"]);
  let SensitiveActivity = AzureActivity
  | where OperationNameValue in~ (SensitiveOperationList) or OperationNameValue hassuffix "listkeys/action"
  | where ActivityStatusValue =~ "Success";
  SensitiveActivity
  | where TimeGenerated between (ago(starttime) .. ago(endtime))
  | summarize count() by CallerIpAddress, Caller, OperationNameValue, bin(TimeGenerated,1d)
  | where count_ >= alertOperationThreshold
  // Returns all the records from the right side that don't have matches from the left
  | join kind = rightanti (
  SensitiveActivity
  | where TimeGenerated >= ago(endtime)
  | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), ActivityTimeStamp = make_list(TimeGenerated), ActivityStatusValue = make_list(ActivityStatusValue), CorrelationIds = make_list(CorrelationId), ResourceGroups = make_list(ResourceGroup), ResourceIds = make_list(_ResourceId), ActivityCountByCallerIPAddress = count()
  by CallerIpAddress, Caller, OperationNameValue
  | where ActivityCountByCallerIPAddress >= alertOperationThreshold
  ) on CallerIpAddress, Caller, OperationNameValue
  | extend Name = tostring(split(Caller,'@',0)[0]), UPNSuffix = tostring(split(Caller,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Caller
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CallerIpAddress
version: 2.0.3
kind: Scheduled

Stages and Predicates

Parameters

let starttime = 14d;
let endtime = 1d;
let alertOperationThreshold = 5;
let SensitiveOperationList = dynamic(["microsoft.compute/snapshots/write", "microsoft.network/networksecuritygroups/write", "microsoft.storage/storageaccounts/listkeys/action"]);

let SensitiveActivity is inlined into the numbered stages below.

Stages 1 to 3 define let SensitiveActivity (the rule's main pipeline source); stages 4 to 8 run on it.

Stage 1: source

AzureActivity

Stage 2: where

| where OperationNameValue in~ (SensitiveOperationList) or OperationNameValue hassuffix "listkeys/action"

Stage 3: where

| where ActivityStatusValue =~ "Success"

Stage 4: where

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

Stage 5: summarize

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

Stage 6: where

| where count_ >= alertOperationThreshold

Stage 7: join

| join kind = rightanti (
SensitiveActivity
| where TimeGenerated >= ago(endtime)
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), ActivityTimeStamp = make_list(TimeGenerated), ActivityStatusValue = make_list(ActivityStatusValue), CorrelationIds = make_list(CorrelationId), ResourceGroups = make_list(ResourceGroup), ResourceIds = make_list(_ResourceId), ActivityCountByCallerIPAddress = count()
by CallerIpAddress, Caller, OperationNameValue
| where ActivityCountByCallerIPAddress >= alertOperationThreshold
) on CallerIpAddress, Caller, OperationNameValue

Stage 8: extend

| extend Name = tostring(split(Caller,'@',0)[0]), UPNSuffix = tostring(split(Caller,'@',1)[0])

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
Callersummarize
CallerIpAddresssummarize
OperationNameValuesummarize
Nameextend
UPNSuffixextend