Detection rules › Kusto
Rare subscription-level operations in Azure
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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
ActivityCountByCallerIPAddress | ge |
| field:"ActivityCountByCallerIPAddress" kind:ge value:"5" |
ActivityStatusValue | eq |
| field:"ActivityStatusValue" kind:eq value:"Success" |
OperationNameValue | ends_with |
| field:"azure_ad::operation_name_value" kind:ends_with value:"listkeys/action" |
OperationNameValue | in |
| field:"azure_ad::operation_name_value" kind:in |
count_ | ge |
| field:"count_" kind:ge value:"5" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Caller | summarize |
CallerIpAddress | summarize |
OperationNameValue | summarize |
Name | extend |
UPNSuffix | extend |