AnalysisType: rule
Filename: azure_resource_lock_deleted.py
RuleID: "Azure.MonitorActivity.Authorization.ResourceLockDeleted"
DisplayName: "Azure Resource Lock Deleted"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: High
Description: >
Detects when Azure resource locks are deleted. Storm-0501 and other ransomware operators
delete resource locks before destroying storage accounts and backups, as locks prevent
deletion even by administrators. This is a critical pre-ransomware indicator.
Reports:
MITRE ATT&CK:
- TA0005:T1562 # Defense Evasion: Impair Defenses
- TA0005:T1562.001 # Defense Evasion: Disable or Modify Tools
- TA0040:T1490 # Impact: Inhibit System Recovery
Reference: https://www.microsoft.com/en-us/security/blog/2025/08/27/storm-0501s-evolving-techniques-lead-to-cloud-based-ransomware/
Tags:
- Defense Evasion
- Impair Defenses
- Inhibit System Recovery
- Ransomware
- Storm-0501
Runbook: |
1. Query Azure Monitor Activity logs for all lock deletions by the callerIpAddress and caller identity in the 24 hours before and after the alert to calculate the total number of locks removed
2. Search for subsequent destructive operations (storage account deletions, snapshot deletions, immutability policy deletions, blob deletions) by the same caller in the 6 hours after lock deletion to identify ransomware attack pattern
3. Check if the callerIpAddress or caller identity has performed lock deletions in the past 90 days to establish if this is routine maintenance or anomalous activity
4. Review Azure.Audit logs for authentication events from the callerIpAddress in the 48 hours before the lock deletion to identify signs of credential compromise (unusual locations, MFA changes, privilege escalations)
5. Search for other alerts triggered by the same callerIpAddress or caller identity in the past 7 days to assess if this is part of a broader attack campaign
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Tests:
- Name: Resource Lock Deleted Successfully
ExpectedResult: true
Log:
{
"time": "2025-01-27T14:23:00.0000000Z",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/critical-data-rg/providers/Microsoft.Storage/storageAccounts/criticaldata001/providers/Microsoft.Authorization/locks/DoNotDelete",
"operationName": "MICROSOFT.AUTHORIZATION/LOCKS/DELETE",
"operationVersion": "2017-04-01",
"category": "Administrative",
"resultType": "Success",
"resultSignature": "200",
"callerIpAddress": "203.0.113.42",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Informational",
"location": "eastus",
"tenantId": "87654321-4321-4321-4321-111111111111",
"identity": {
"claims": {
"name": "compromised-admin@company.com",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn": "compromised-admin@company.com"
}
}
}
- Name: Subscription Level Lock Deleted
ExpectedResult: true
Log:
{
"time": "2025-01-27T15:45:00.0000000Z",
"resourceId": "/subscriptions/87654321-4321-4321-4321-987654321cba/providers/Microsoft.Authorization/locks/SubscriptionDoNotDelete",
"operationName": "microsoft.authorization/locks/delete",
"operationVersion": "2017-04-01",
"category": "Administrative",
"resultType": "Succeeded",
"callerIpAddress": "198.51.100.50",
"correlationId": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
"level": "Information",
"location": "global",
"tenantId": "11111111-1111-1111-1111-111111111111"
}
- Name: Resource Group Lock Deleted
ExpectedResult: true
Log:
{
"time": "2025-01-27T16:10:00.0000000Z",
"resourceId": "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/production-rg/providers/Microsoft.Authorization/locks/ReadOnlyLock",
"operationName": "Microsoft.Authorization/locks/delete",
"category": "Administrative",
"resultType": "Success",
"callerIpAddress": "192.0.2.100",
"correlationId": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"tenantId": "22222222-2222-2222-2222-222222222222"
}
- Name: Failed Lock Deletion
ExpectedResult: false
Log:
{
"time": "2025-01-27T17:00:00.0000000Z",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/protected-rg/providers/Microsoft.Authorization/locks/CriticalLock",
"operationName": "MICROSOFT.AUTHORIZATION/LOCKS/DELETE",
"category": "Administrative",
"resultType": "Failed",
"resultSignature": "403",
"callerIpAddress": "198.18.0.50",
"correlationId": "d4e5f6a7-b8c9-0123-def0-456789012345",
"tenantId": "33333333-3333-3333-3333-333333333333"
}
- Name: Different Operation - Lock Created
ExpectedResult: false
Log:
{
"time": "2025-01-27T18:00:00.0000000Z",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/data-rg/providers/Microsoft.Authorization/locks/NewLock",
"operationName": "MICROSOFT.AUTHORIZATION/LOCKS/WRITE",
"category": "Administrative",
"resultType": "Success",
"callerIpAddress": "203.0.113.10",
"correlationId": "e5f6a7b8-c9d0-1234-ef01-567890123456",
"tenantId": "44444444-4444-4444-4444-444444444444"
}
# ------ paired body: azure_resource_lock_deleted.py ------
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
LOCK_DELETE = "MICROSOFT.AUTHORIZATION/LOCKS/DELETE"
def rule(event):
return event.get("operationName", "").upper() == LOCK_DELETE and azure_activity_success(event)
def title(event):
resource_id = event.get("resourceId", "")
lock_name = extract_resource_name_from_id(resource_id, "locks", default="<UNKNOWN_LOCK>")
return f"Azure resource lock [{lock_name}] deleted"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
lock_name = extract_resource_name_from_id(resource_id, "locks", default="")
if lock_name:
context["lock_name"] = lock_name
# Extract the resource that was protected by the lock
resource_group = extract_resource_name_from_id(resource_id, "resourceGroups", default="")
if resource_group:
context["resource_group"] = resource_group
# Check if this is a subscription-level or resource-level lock
if "/subscriptions/" in resource_id and "/resourceGroups/" not in resource_id:
context["lock_scope"] = "subscription"
elif "/resourceGroups/" in resource_id:
context["lock_scope"] = "resource_group_or_resource"
return context