Detection rules › Panther
Panther rules: azure
Azure Action Groups Deleted
#Detects when Azure action groups are deleted, which could disable alert notifications to security teams to prevent incident response.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | Activity Log event Microsoft.Insights/ActionGroups/Delete: Delete an action group |
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
ACTION_GROUPS_DELETE = "MICROSOFT.INSIGHTS/ACTIONGROUPS/DELETE"
def rule(event):
return event.get(
"operationName", ""
).upper() == ACTION_GROUPS_DELETE and azure_activity_success(event)
def title(event):
resource_id = event.get("resourceId", "")
action_group = extract_resource_name_from_id(
resource_id, "actionGroups", default="<UNKNOWN_ACTION_GROUP>"
)
return f"Azure Action Group deleted [{action_group}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
action_group_name = extract_resource_name_from_id(resource_id, "actionGroups", default="")
if action_group_name:
context["action_group_name"] = action_group_name
resource_group = extract_resource_name_from_id(resource_id, "resourceGroups", default="")
if resource_group:
context["resource_group"] = resource_group
return context
Rule specification
AnalysisType: rule
Filename: azure_action_groups_deleted.py
RuleID: "Azure.MonitorActivity.ActionGroups.Deleted"
DisplayName: "Azure Action Groups Deleted"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Info
Description: >
Detects when Azure action groups are deleted, which could disable alert notifications to security teams to prevent incident response.
Reports:
MITRE ATT&CK:
- TA0005:T1562.008 # Defense Evasion: Impair Defenses - Disable Cloud Logs
Tags:
- Defense Evasion
- Impair Defenses
- Disable Cloud Logs
Runbook: |
1. Query Azure Monitor Activity logs for all action group and notification operations by the callerIpAddress in the 24 hours before and after this alert to identify if multiple notification channels are being deleted
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure alert rule deletions or monitoring infrastructure changes from the same user or IP in the past 7 days to assess if this is part of a defense evasion campaign
Reference: https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/action-groups
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.INSIGHTS/ACTIONGROUPS/DELETEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.INSIGHTS/ACTIONGROUPS/DELETE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all action group and notification operations by the callerIpAddress in the 24 hours before and after this alert to identify if multiple notification channels are being deleted
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure alert rule deletions or monitoring infrastructure changes from the same user or IP in the past 7 days to assess if this is part of a defense evasion campaign
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"location": "eastus",
"operationName": "Microsoft.Insights/actionGroups/delete",
"operationVersion": "2019-06-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.Insights/actionGroups/securityteam",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Advisor Security Recommendation Available
#Detects when Azure Advisor generates a new security recommendation for a resource. Azure Advisor analyzes your resource configurations and usage telemetry to recommend solutions that can help improve security, cost effectiveness, performance, reliability, and operational excellence.
Detection logic
from panther_azureactivity_helpers import azure_activity_alert_context
ADVISOR_RECOMMENDATION_OPERATION = "MICROSOFT.ADVISOR/RECOMMENDATIONS/AVAILABLE/ACTION"
RECOMMENDATION_CATEGORY = "Recommendation"
SECURITY_CATEGORY = "Security"
def rule(event):
return all(
[
event.get("operationName", "").upper() == ADVISOR_RECOMMENDATION_OPERATION,
event.get("category", "") == RECOMMENDATION_CATEGORY,
event.deep_get("properties", "recommendationCategory") == SECURITY_CATEGORY,
]
)
def title(event):
resource_id = event.get("resourceId", "<UNKNOWN_RESOURCE>")
return f"Azure Advisor Security Recommendation Available for [{resource_id}]"
def alert_context(event):
context = azure_activity_alert_context(event)
context["recommendation_name"] = event.deep_get(
"properties", "recommendationName", default=None
)
context["recommendation_impact"] = event.deep_get(
"properties", "recommendationImpact", default=None
)
context["recommendation_category"] = event.deep_get(
"properties", "recommendationCategory", default=None
)
context["recommendation_type"] = event.deep_get(
"properties", "recommendationType", default=None
)
context["recommendation_link"] = event.deep_get(
"properties", "recommendationResourceLink", default=None
)
context["result_description"] = event.get("resultDescription", None)
return context
Rule specification
AnalysisType: rule
Filename: azure_advisor_security_recommendation.py
RuleID: "Azure.MonitorActivity.Advisor.SecurityRecommendation"
DisplayName: "Azure Advisor Security Recommendation Available"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Info
Status: Experimental
Description: >
Detects when Azure Advisor generates a new security recommendation for a resource.
Azure Advisor analyzes your resource configurations and usage telemetry to recommend solutions
that can help improve security, cost effectiveness, performance, reliability, and operational excellence.
Runbook: |
1. Query Azure Monitor Activity logs for all operations on the resourceId in the 48 hours before the alert to identify recent configuration changes
2. Check if this recommendationType has been triggered for other resources in the tenantId in the past 30 days to identify patterns
3. Review the recommendationImpact and recommendationName to assess priority and determine if immediate remediation is required
Reference: https://learn.microsoft.com/en-us/azure/advisor/advisor-overview
SummaryAttributes:
- resourceId
- properties.recommendationName
- properties.recommendationImpact
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.ADVISOR/RECOMMENDATIONS/AVAILABLE/ACTIONcategoryisRecommendationproperties.recommendationCategoryisSecurity
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
category | eq |
| field:"category" kind:eq value:"Recommendation" |
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.ADVISOR/RECOMMENDATIONS/AVAILABLE/ACTION" |
properties.recommendationCategory | eq |
| field:"properties.recommendationCategory" kind:eq value:"Security" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
resourceId |
Response runbook
1. Query Azure Monitor Activity logs for all operations on the resourceId in the 48 hours before the alert to identify recent configuration changes
2. Check if this recommendationType has been triggered for other resources in the tenantId in the past 30 days to identify patterns
3. Review the recommendationImpact and recommendationName to assess priority and determine if immediate remediation is required
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "0.0.0.0",
"category": "Recommendation",
"correlationId": "00000000-0000-0000-0000-111111111111",
"identity": {
"claims": {
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "Microsoft.Advisor"
}
},
"level": "Informational",
"location": "global",
"operationName": "Microsoft.Advisor/recommendations/available/action",
"operationVersion": "2017-03-31",
"properties": {
"recommendationCategory": "Security",
"recommendationImpact": "Medium",
"recommendationName": "Storage accounts should restrict network access using virtual network rules",
"recommendationResourceLink": "https://portal.azure.com/#blade/Microsoft_Azure_Expert/RecommendationListBlade/source/ActivityLog",
"recommendationSchemaVersion": "1.0",
"recommendationType": "00000000-0000-0000-0000-111111111111"
},
"resourceId": "/SUBSCRIPTIONS/00000000-0000-0000-0000-111111111111/RESOURCEGROUPS/EXAMPLE-RG/PROVIDERS/MICROSOFT.STORAGE/STORAGEACCOUNTS/EXAMPLESTORAGE",
"resultDescription": "A new recommendation is available.",
"resultSignature": "Succeeded",
"resultType": "Active",
"tenantId": "00000000-0000-0000-0000-111111111111",
"time": "2025-12-22T08:46:29.678707300Z"
}
Azure Alert Rules Deleted
#Detects when Azure alert rules are deleted. Deleting alert rules disables security notifications and is a common defense evasion technique.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
ALERT_RULES_DELETE = "MICROSOFT.INSIGHTS/ALERTRULES/DELETE"
METRIC_ALERTS_DELETE = "MICROSOFT.INSIGHTS/METRICALERTS/DELETE"
def rule(event):
operation = event.get("operationName", "").upper()
return operation in [ALERT_RULES_DELETE, METRIC_ALERTS_DELETE] and azure_activity_success(event)
def title(event):
alert_rule = event.get("resourceId", "<UNKNOWN_ALERT>")
return f"Azure Alert Rule deleted [{alert_rule}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
operation = event.get("operationName", "").upper()
# Determine resource type based on operation
if operation == METRIC_ALERTS_DELETE:
alert_name = extract_resource_name_from_id(resource_id, "metricalerts", default="")
if alert_name:
context["alert_rule_name"] = alert_name
context["alert_type"] = "metric"
else:
alert_name = extract_resource_name_from_id(resource_id, "alertrules", default="")
if alert_name:
context["alert_rule_name"] = alert_name
context["alert_type"] = "classic"
resource_group = extract_resource_name_from_id(resource_id, "resourceGroups", default="")
if resource_group:
context["resource_group"] = resource_group
return context
Rule specification
AnalysisType: rule
Filename: azure_alert_rules_deleted.py
RuleID: "Azure.MonitorActivity.AlertRules.Deleted"
DisplayName: "Azure Alert Rules Deleted"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Medium
Description: >
Detects when Azure alert rules are deleted. Deleting alert rules disables security notifications and is a common defense evasion technique.
Reports:
MITRE ATT&CK:
- TA0005:T1562.008 # Defense Evasion: Impair Defenses - Disable Cloud Logs
Tags:
- Defense Evasion
- Impair Defenses
- Disable Cloud Logs
Runbook: |
1. Query Azure Monitor Activity logs for all alert rule and notification operations by the callerIpAddress in the 24 hours before and after this alert to identify if multiple alerts are being deleted
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure monitoring or security configuration changes from the same user or IP in the past 7 days to assess if this is part of a defense evasion campaign
Reference: https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-manage-alert-rules
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameis one ofMICROSOFT.INSIGHTS/ALERTRULES/DELETE,MICROSOFT.INSIGHTS/METRICALERTS/DELETEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | in |
| field:"operationName" kind:in |
resultType | in |
| field:"resultType" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
resourceId |
Response runbook
1. Query Azure Monitor Activity logs for all alert rule and notification operations by the callerIpAddress in the 24 hours before and after this alert to identify if multiple alerts are being deleted
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure monitoring or security configuration changes from the same user or IP in the past 7 days to assess if this is part of a defense evasion campaign
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"location": "eastus",
"operationName": "Microsoft.Insights/alertRules/delete",
"operationVersion": "2016-03-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.Insights/alertRules/myalert",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Alert Suppression Rule Created or Modified
#Detects when an Azure Security Center alert suppression rule is created or modified. Alert suppression rules allow filtering of specific security alerts to reduce noise, but adversaries may abuse this feature to silence alerts related to their malicious activities. While legitimate use cases exist (suppressing known false positives), new suppression rules should be reviewed to ensure they don't hide indicators of compromise.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
ALERT_SUPPRESSION_WRITE_OPERATION = "MICROSOFT.SECURITY/ALERTSSUPPRESSIONRULES/WRITE"
def rule(event):
return all(
[
event.get("operationName", "").upper() == ALERT_SUPPRESSION_WRITE_OPERATION,
azure_activity_success(event),
]
)
def title(event):
resource_id = event.get("resourceId", "<UNKNOWN_RESOURCE>")
rule_name = extract_resource_name_from_id(
resource_id, "alertsSuppressionRules", default="<UNKNOWN_RULE_NAME>"
)
return f"Azure Alert Suppression Rule Created or Modified: [{rule_name}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
rule_name = extract_resource_name_from_id(resource_id, "alertsSuppressionRules", default="")
if rule_name:
context["suppression_rule_name"] = rule_name
return context
Rule specification
AnalysisType: rule
Filename: azure_alert_suppression_rule_created.py
RuleID: "Azure.MonitorActivity.Security.AlertSuppressionRuleCreated"
DisplayName: "Azure Alert Suppression Rule Created or Modified"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Low
Description: >
Detects when an Azure Security Center alert suppression rule is created or modified.
Alert suppression rules allow filtering of specific security alerts to reduce noise,
but adversaries may abuse this feature to silence alerts related to their malicious
activities. While legitimate use cases exist (suppressing known false positives), new
suppression rules should be reviewed to ensure they don't hide indicators of compromise.
Reports:
MITRE ATT&CK:
- TA0005:T1562 # Defense Evasion: Impair Defenses
Tags:
- Defense Evasion
- Impair Defenses
Runbook: |
1. Query Azure Monitor Activity logs for all security control operations (alert suppression rules, alert rule deletions, diagnostic settings deletions) by the callerIpAddress in the 24 hours before and after the alert
2. Find all alert suppression rule creations in the past 6 hours to determine if multiple alerts are being suppressed to hide malicious activity
3. Check if the callerIpAddress has created alert suppression rules in the past 90 days to establish if this is typical security operations activity
Reference: https://learn.microsoft.com/en-us/azure/defender-for-cloud/alerts-suppression-rules
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.SECURITY/ALERTSSUPPRESSIONRULES/WRITEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.SECURITY/ALERTSSUPPRESSIONRULES/WRITE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all security control operations (alert suppression rules, alert rule deletions, diagnostic settings deletions) by the callerIpAddress in the 24 hours before and after the alert
2. Find all alert suppression rule creations in the past 6 hours to determine if multiple alerts are being suppressed to hide malicious activity
3. Check if the callerIpAddress has created alert suppression rules in the past 90 days to establish if this is typical security operations activity
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Informational",
"location": "global",
"operationName": "MICROSOFT.SECURITY/ALERTSSUPPRESSIONRULES/WRITE",
"operationVersion": "2019-01-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/providers/Microsoft.Security/alertsSuppressionRules/SuppressFalsePositives",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-22T10:30:00.0000000Z"
}
Azure Authentication Methods Policy OIDC Discovery URL Changed
#Detects modifications to the OIDC discovery URL in Azure Entra ID's Authentication Methods Policy. This technique enables attackers to federate the tenant with attacker-controlled identity providers, bypassing multi-factor authentication and enabling unauthorized access through bring-your-own IdP methods.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Detection logic
def rule(event):
operation_name = event.get("operationName", "")
if "authentication methods policy update" not in operation_name.lower():
return False
old_values = event.deep_walk(
"properties", "targetResources", "modifiedProperties", "oldValue", default=[]
)
new_values = event.deep_walk(
"properties", "targetResources", "modifiedProperties", "newValue", default=[]
)
# Ensure we have lists
if not isinstance(old_values, list):
old_values = [old_values] if old_values else []
if not isinstance(new_values, list):
new_values = [new_values] if new_values else []
if len(old_values) != len(new_values):
# Lists have different lengths; check all values to be safe
for value in old_values + new_values:
if isinstance(value, str) and "discoveryUrl" in value:
return True
return False
for old_value, new_value in zip(old_values, new_values):
if (
isinstance(old_value, str)
and isinstance(new_value, str)
and "discoveryUrl" in old_value
and "discoveryUrl" in new_value
):
if old_value != new_value:
return True
return False
def title(event):
actor = event.deep_get(
"properties", "initiatedBy", "user", "userPrincipalName", default="<UNKNOWN_ACTOR>"
)
return f"Authentication Methods Policy OIDC Discovery URL Changed by [{actor}]"
def alert_context(event):
context = {}
context["operation_name"] = event.get("operationName", "<NO_OPERATION>")
context["activity_display_name"] = event.deep_get(
"properties", "activityDisplayName", default="<NO_ACTIVITY>"
)
context["category"] = event.get("category", "<NO_CATEGORY>")
context["initiator_user_id"] = event.deep_get(
"properties", "initiatedBy", "user", "id", default="<NO_USER_ID>"
)
context["initiator_display_name"] = event.deep_get(
"properties", "initiatedBy", "user", "displayName", default="<NO_DISPLAY_NAME>"
)
context["initiator_ip"] = event.deep_get(
"properties", "initiatedBy", "user", "ipAddress", default="<NO_IP>"
)
# Extract OIDC discovery URL changes
old_values = event.deep_walk(
"properties", "targetResources", "modifiedProperties", "oldValue", default=[]
)
new_values = event.deep_walk(
"properties", "targetResources", "modifiedProperties", "newValue", default=[]
)
if not isinstance(old_values, list):
old_values = [old_values] if old_values else []
if not isinstance(new_values, list):
new_values = [new_values] if new_values else []
for old_value, new_value in zip(old_values, new_values):
if (isinstance(old_value, str) and "discoveryUrl" in old_value) or (
isinstance(new_value, str) and "discoveryUrl" in new_value
):
context["old_discovery_url"] = old_value
context["new_discovery_url"] = new_value
break
return context
Rule specification
AnalysisType: rule
Filename: azure_auth_methods_policy_oidc_change.py
RuleID: "Azure.Audit.OIDC.Changed"
DisplayName: "Azure Authentication Methods Policy OIDC Discovery URL Changed"
Enabled: true
LogTypes:
- Azure.Audit
Severity: High
Description: >
Detects modifications to the OIDC discovery URL in Azure Entra ID's Authentication Methods Policy. This technique enables attackers to federate the tenant with attacker-controlled identity providers, bypassing multi-factor authentication and enabling unauthorized access through bring-your-own IdP methods.
Tags:
- Persistence
- Modify Authentication Process
Reports:
MITRE ATT&CK:
- TA0003:T1556
- TA0003:T1556.009
Runbook: |
1. Query Azure.Audit logs for all authentication policy changes by properties:initiatedBy:user:userPrincipalName in the 7 days before and after this change to determine if this modification was part of a broader attack campaign
2. Immediately verify the new OIDC discovery URL with your identity team to confirm it points to a legitimate identity provider owned by your organization and not an attacker-controlled domain
3. Query Azure.Audit and sign-in logs for all authentication events in the 24 hours after the policy change to identify any suspicious token issuance or unauthorized access attempts, and if the modification was unauthorized, immediately revert the authentication methods policy, revoke all active sessions, and review all API access and resource modifications that occurred during the compromise window
Reference: https://github.com/elastic/detection-rules/blob/main/rules/integrations/azure/persistence_entra_id_oidc_discovery_url_change.toml
SummaryAttributes:
- properties:initiatedBy:user:userPrincipalName
- properties:targetResources:displayName
- properties:initiatedBy:user:ipAddress
Stages and Predicates
Fires on Azure.Audit events when the condition below holds.
Condition
operationNamecontainsauthentication methods policy update
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | contains |
| field:"operationName" kind:contains value:"authentication methods policy update" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
userPrincipalName | properties.initiatedBy.user.userPrincipalName |
Response runbook
1. Query Azure.Audit logs for all authentication policy changes by properties:initiatedBy:user:userPrincipalName in the 7 days before and after this change to determine if this modification was part of a broader attack campaign
2. Immediately verify the new OIDC discovery URL with your identity team to confirm it points to a legitimate identity provider owned by your organization and not an attacker-controlled domain
3. Query Azure.Audit and sign-in logs for all authentication events in the 24 hours after the policy change to identify any suspicious token issuance or unauthorized access attempts, and if the modification was unauthorized, immediately revert the authentication methods policy, revoke all active sessions, and review all API access and resource modifications that occurred during the compromise window
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"Level": "4",
"callerIpAddress": "1.2.3.4",
"category": "Policy",
"correlationId": "policy-update-001",
"durationMs": 0,
"operationName": "Authentication Methods Policy Update",
"operationVersion": "1.0",
"p_event_time": "2025-01-15 14:20:35.456",
"p_log_type": "Azure.Audit",
"properties": {
"activityDateTime": "2025-01-15T14:20:35.4567890Z",
"activityDisplayName": "Update authentication methods policy",
"initiatedBy": {
"user": {
"displayName": "Compromised Global Admin",
"id": "admin-compromised-001",
"ipAddress": "1.2.3.4",
"userPrincipalName": "denethor@lotr.com"
}
},
"loggedByService": "Core Directory",
"operationName": "Authentication Methods Policy Update",
"operationType": "Update",
"result": "success",
"targetResources": [
{
"displayName": "Authentication Methods Policy",
"id": "policy-auth-methods-123",
"modifiedProperties": [
{
"displayName": "OpenIdConnectConfiguration",
"newValue": "{\"discoveryUrl\":\"https://attacker-idp.evil.com/.well-known/openid-configuration\"}",
"oldValue": "{\"discoveryUrl\":\"https://login.microsoftonline.com/tenant-abc/.well-known/openid-configuration\"}"
}
],
"type": "Policy"
}
]
},
"resourceId": "/tenants/tenant-abc/providers/Microsoft.aadiam",
"resultSignature": "None",
"tenantId": "tenant-abc",
"time": "2025-01-15 14:20:35.456"
}
Azure Automation Account Created
#Detects when an Azure Automation account is created. Azure Automation accounts can be used to automate management tasks and orchestrate actions across systems. Adversaries may create Automation accounts to maintain persistence in their target's environment by leveraging managed identities and runbooks to execute code with elevated privileges.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Stealth |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
AUTOMATION_ACCOUNT_WRITE = "MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WRITE"
def rule(event):
return event.get(
"operationName", ""
).upper() == AUTOMATION_ACCOUNT_WRITE and azure_activity_success(event)
def title(event):
resource_id = event.get("resourceId", "<UNKNOWN_RESOURCE>")
account_name = extract_resource_name_from_id(
resource_id, "automationAccounts", default="<UNKNOWN_ACCOUNT>"
)
return f"Azure Automation Account Created: [{account_name}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
account_name = extract_resource_name_from_id(resource_id, "automationAccounts", default="")
if account_name:
context["automation_account_name"] = account_name
return context
Rule specification
AnalysisType: rule
Filename: azure_automation_account_created.py
RuleID: "Azure.MonitorActivity.Automation.AccountCreated"
DisplayName: "Azure Automation Account Created"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Info
Description: >
Detects when an Azure Automation account is created. Azure Automation accounts can be used to
automate management tasks and orchestrate actions across systems. Adversaries may create
Automation accounts to maintain persistence in their target's environment by leveraging managed
identities and runbooks to execute code with elevated privileges.
Reports:
MITRE ATT&CK:
- TA0003:T1078 # Persistence: Valid Accounts
- TA0005:T1078 # Defense Evasion: Valid Accounts
Tags:
- Persistence
- Defense Evasion
- Valid Accounts
Runbook: |
1. Query Azure Monitor Activity logs for all automation-related operations (runbook creation, webhook creation, job executions) for the newly created automation account in the 6 hours after the account was created
2. Find all automation account creations by the callerIpAddress in the past 24 hours to identify if multiple accounts are being created
3. Check if the callerIpAddress has created automation accounts in the past 90 days to establish if this is typical infrastructure deployment behavior
Reference: https://github.com/elastic/detection-rules/blob/main/rules/integrations/azure/persistence_automation_account_created.toml
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WRITEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WRITE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all automation-related operations (runbook creation, webhook creation, job executions) for the newly created automation account in the 6 hours after the account was created
2. Find all automation account creations by the callerIpAddress in the past 24 hours to identify if multiple accounts are being created
3. Check if the callerIpAddress has created automation accounts in the past 90 days to establish if this is typical infrastructure deployment behavior
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Informational",
"location": "eastus",
"operationName": "MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WRITE",
"operationVersion": "2021-06-22",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/automation-rg/providers/Microsoft.Automation/automationAccounts/malicious-automation",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-22T10:30:00.0000000Z"
}
Azure Automation Runbook Created or Modified
#Detects when an Azure Automation runbook is created, modified, or published. Runbooks contain executable code that can automate tasks within Azure environments. Adversaries may abuse runbooks for persistence, privilege escalation, or execution of malicious scripts. This rule monitors draft creation, runbook modifications, and publishing activities that could indicate unauthorized automation code deployment.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Persistence |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
RUNBOOK_OPERATIONS = [
"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/DRAFT/WRITE",
"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/WRITE",
"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/PUBLISH/ACTION",
]
def rule(event):
return event.get("operationName", "").upper() in RUNBOOK_OPERATIONS and azure_activity_success(
event
)
def title(event):
resource_id = event.get("resourceId", "<UNKNOWN_RESOURCE_ID>")
runbook_name = extract_resource_name_from_id(
resource_id, "runbooks", default="<UNKNOWN_RUNBOOK_NAME>"
)
operation = event.get("operationName", "").upper()
action = "Modified"
if "PUBLISH" in operation:
action = "Published"
elif "DRAFT" in operation:
action = "Draft Created"
return f"Azure Automation Runbook {action}: [{runbook_name}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
runbook_name = extract_resource_name_from_id(resource_id, "runbooks", default="")
if runbook_name:
context["runbook_name"] = runbook_name
automation_account_name = extract_resource_name_from_id(
resource_id, "automationAccounts", default=""
)
if automation_account_name:
context["automation_account_name"] = automation_account_name
return context
Rule specification
AnalysisType: rule
Filename: azure_automation_runbook_created_or_modified.py
RuleID: "Azure.MonitorActivity.Automation.RunbookCreatedOrModified"
DisplayName: "Azure Automation Runbook Created or Modified"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Info
Description: >
Detects when an Azure Automation runbook is created, modified, or published. Runbooks contain
executable code that can automate tasks within Azure environments. Adversaries may abuse runbooks
for persistence, privilege escalation, or execution of malicious scripts. This rule monitors draft
creation, runbook modifications, and publishing activities that could indicate unauthorized automation
code deployment.
Reports:
MITRE ATT&CK:
- TA0003:T1078.004 # Persistence: Cloud Accounts
- TA0002:T1059 # Execution: Command and Scripting Interpreter
Tags:
- AZT302
- AZT302.1
- AZT302.2
- AZT302.3
- AZT601
- AZT601.5
- AZT602
- AZT605.2
- Persistence
- Execution
- Cloud Accounts
- Command and Scripting Interpreter
Runbook: |
1. Query Azure Monitor Activity logs for all operations by the callerIpAddress in the 24 hours before and after the alert to establish activity patterns
2. Check if the caller has created or modified automation runbooks in the past 90 days to determine if this is typical behavior
3. Search for other automation-related activities (webhook creation, automation account creation, runbook job executions) from the same caller in the 6 hours around the alert
Reference: https://github.com/elastic/detection-rules/blob/main/rules/integrations/azure/execution_azure_automation_runbook_created_or_modified.toml
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameis one ofMICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/DRAFT/WRITE,MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/WRITE,MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/PUBLISH/ACTIONresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | in |
| field:"operationName" kind:in |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all operations by the callerIpAddress in the 24 hours before and after the alert to establish activity patterns
2. Check if the caller has created or modified automation runbooks in the past 90 days to determine if this is typical behavior
3. Search for other automation-related activities (webhook creation, automation account creation, runbook job executions) from the same caller in the 6 hours around the alert
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Informational",
"location": "eastus",
"operationName": "MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/DRAFT/WRITE",
"operationVersion": "2021-06-22",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/automation-rg/providers/Microsoft.Automation/automationAccounts/MyAutomationAccount/runbooks/MaliciousRunbook",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-22T10:30:00.0000000Z"
}
Azure Automation Runbook Deleted
#Detects when an Azure Automation runbook is deleted. Adversaries may delete runbooks to cover their tracks after using them for malicious purposes, to disrupt automated security responses, or to eliminate forensic evidence. Legitimate runbook deletions should be rare and controlled through change management processes.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Stealth |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
RUNBOOK_DELETE_OPERATION = "MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/DELETE"
def rule(event):
return event.get(
"operationName", ""
).upper() == RUNBOOK_DELETE_OPERATION and azure_activity_success(event)
def title(event):
resource_id = event.get("resourceId", "<UNKNOWN_RESOURCE>")
runbook_name = extract_resource_name_from_id(
resource_id, "runbooks", default="<UNKNOWN_RUNBOOK_NAME>"
)
return f"Azure Automation Runbook Deleted: [{runbook_name}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
runbook_name = extract_resource_name_from_id(resource_id, "runbooks", default="")
if runbook_name:
context["runbook_name"] = runbook_name
automation_account_name = extract_resource_name_from_id(
resource_id, "automationAccounts", default=""
)
if automation_account_name:
context["automation_account_name"] = automation_account_name
return context
Rule specification
AnalysisType: rule
Filename: azure_automation_runbook_deleted.py
RuleID: "Azure.MonitorActivity.Automation.RunbookDeleted"
DisplayName: "Azure Automation Runbook Deleted"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Info
Description: >
Detects when an Azure Automation runbook is deleted. Adversaries may delete runbooks to cover
their tracks after using them for malicious purposes, to disrupt automated security responses,
or to eliminate forensic evidence. Legitimate runbook deletions should be rare and controlled
through change management processes.
Reports:
MITRE ATT&CK:
- TA0005:T1070 # Defense Evasion: Indicator Removal
Tags:
- Defense Evasion
- Indicator Removal
Runbook: |
1. Query Azure Monitor Activity logs for all runbook operations (create, modify, delete) by the callerIpAddress in the 24 hours before the deletion to identify if the runbook was recently created by the same actor
2. Find all runbook deletions in the past 6 hours to determine if this is part of a larger cleanup operation
3. Search for other defense evasion activities (diagnostic settings deletions, alert rule deletions, event hub deletions) from the same caller in the 24 hours around the alert
Reference: https://github.com/elastic/detection-rules/blob/main/rules/integrations/azure/defense_evasion_azure_automation_runbook_deleted.toml
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/DELETEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/DELETE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all runbook operations (create, modify, delete) by the callerIpAddress in the 24 hours before the deletion to identify if the runbook was recently created by the same actor
2. Find all runbook deletions in the past 6 hours to determine if this is part of a larger cleanup operation
3. Search for other defense evasion activities (diagnostic settings deletions, alert rule deletions, event hub deletions) from the same caller in the 24 hours around the alert
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Informational",
"location": "eastus",
"operationName": "MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/DELETE",
"operationVersion": "2021-06-22",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/automation-rg/providers/Microsoft.Automation/automationAccounts/MyAutomationAccount/runbooks/SuspiciousRunbook",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-22T10:30:00.0000000Z"
}
Azure Automation Schedule Created or Modified
#Detects when an Azure Automation schedule is created or modified. Schedules define when and how often automation runbooks execute. Adversaries may create or modify schedules to establish persistence by executing malicious runbooks at regular intervals or specific times. This technique allows attackers to maintain access and execute commands without requiring direct interaction.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
SCHEDULE_OPERATIONS = [
"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/SCHEDULES/WRITE",
]
def rule(event):
return event.get("operationName", "").upper() in SCHEDULE_OPERATIONS and azure_activity_success(
event
)
def title(event):
resource_id = event.get("resourceId", "<UNKNOWN_RESOURCE>")
caller = event.get("callerIpAddress", "<UNKNOWN_CALLER>")
schedule_name = extract_resource_name_from_id(
resource_id, "schedules", default="<UNKNOWN_SCHEDULE_NAME>"
)
return f"Azure Automation Schedule Created or Modified: [{schedule_name}] by [{caller}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
schedule_name = extract_resource_name_from_id(resource_id, "schedules", default="")
if schedule_name:
context["schedule_name"] = schedule_name
automation_account_name = extract_resource_name_from_id(
resource_id, "automationAccounts", default=""
)
if automation_account_name:
context["automation_account_name"] = automation_account_name
return context
Rule specification
AnalysisType: rule
Filename: azure_automation_schedule_created.py
RuleID: "Azure.MonitorActivity.Automation.ScheduleCreated"
DisplayName: "Azure Automation Schedule Created or Modified"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Info
Description: >
Detects when an Azure Automation schedule is created or modified. Schedules define when and how often
automation runbooks execute. Adversaries may create or modify schedules to establish persistence by
executing malicious runbooks at regular intervals or specific times. This technique allows attackers
to maintain access and execute commands without requiring direct interaction.
Reports:
MITRE ATT&CK:
- TA0003:T1053.005 # Persistence: Scheduled Task/Job - Scheduled Task
- TA0004:T1068 # Privilege Escalation: Exploitation for Privilege Escalation
Tags:
- AZT505
- Persistence
- Privilege Escalation
- Scheduled Task
- Exploitation for Privilege Escalation
Runbook: |
1. Find all automation account operations by the callerIpAddress in the 24 hours before and after this alert to identify if runbooks were created or modified alongside this schedule
2. Query for runbook job execution events from the automation account in the 6 hours after the schedule creation to determine if the schedule has already triggered
3. Check if the callerIpAddress is associated with known cloud providers, VPNs, or corporate network ranges used by authorized DevOps personnel
Reference: https://microsoft.github.io/Azure-Threat-Research-Matrix/Persistence/AZT505/AZT505-1
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameis one ofMICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/SCHEDULES/WRITEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | in |
| field:"operationName" kind:in value:"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/SCHEDULES/WRITE" |
resultType | in |
| field:"resultType" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
callerIpAddress |
Response runbook
1. Find all automation account operations by the callerIpAddress in the 24 hours before and after this alert to identify if runbooks were created or modified alongside this schedule
2. Query for runbook job execution events from the automation account in the 6 hours after the schedule creation to determine if the schedule has already triggered
3. Check if the callerIpAddress is associated with known cloud providers, VPNs, or corporate network ranges used by authorized DevOps personnel
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Informational",
"location": "eastus",
"operationName": "MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/SCHEDULES/WRITE",
"operationVersion": "2021-06-22",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/automation-rg/providers/Microsoft.Automation/automationAccounts/MyAutomationAccount/schedules/DailyMaintenanceSchedule",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-23T10:30:00.0000000Z"
}
Azure Automation Webhook Created
#Detects when an Azure Automation webhook is created. A webhook uses a custom URL passed to Azure Automation along with a data payload specific to the runbook. Adversaries may exploit this capability to trigger runbooks containing malicious code for persistence or to execute unauthorized actions in the environment.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Resource Development | |
| Persistence |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
AUTOMATION_WEBHOOK_OPERATIONS = [
"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WEBHOOKS/ACTION",
"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WEBHOOKS/WRITE",
]
def rule(event):
return event.get(
"operationName", ""
).upper() in AUTOMATION_WEBHOOK_OPERATIONS and azure_activity_success(event)
def title(event):
resource_id = event.get("resourceId", "<UNKNOWN_RESOURCE>")
webhook_name = extract_resource_name_from_id(
resource_id, "webhooks", default="<UNKNOWN_WEBHOOK_NAME>"
)
return f"Azure Automation Webhook Created: [{webhook_name}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
webhook_name = extract_resource_name_from_id(resource_id, "webhooks", default="")
if webhook_name:
context["webhook_name"] = webhook_name
automation_account_name = extract_resource_name_from_id(
resource_id, "automationAccounts", default=""
)
if automation_account_name:
context["automation_account_name"] = automation_account_name
return context
Rule specification
AnalysisType: rule
Filename: azure_automation_webhook_created.py
RuleID: "Azure.MonitorActivity.Automation.WebhookCreated"
DisplayName: "Azure Automation Webhook Created"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Info
Description: >
Detects when an Azure Automation webhook is created. A webhook uses a custom URL passed to
Azure Automation along with a data payload specific to the runbook. Adversaries may exploit
this capability to trigger runbooks containing malicious code for persistence or to execute
unauthorized actions in the environment.
Reports:
MITRE ATT&CK:
- TA0003:T1546 # Persistence: Event Triggered Execution
- TA0042:T1608 # Resource Development: Stage Capabilities
Tags:
- AZT502
- AZT503
- AZT503.3
- Persistence
- Resource Development
- Event Triggered Execution
- Stage Capabilities
Runbook: |
1. Query Azure Monitor Activity logs for all automation account operations (account creation, runbook creation, webhook creation) by the callerIpAddress in the 24 hours before and after the alert to identify a sequence of persistence activities
2. Find all webhook creations in the past 6 hours to determine if multiple persistence mechanisms are being established
3. Check if the callerIpAddress has created webhooks in the past 90 days to determine if this is typical automation workflow deployment
Reference: https://github.com/elastic/detection-rules/blob/main/rules/integrations/azure/persistence_automation_webhook_created.toml
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameis one ofMICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WEBHOOKS/ACTION,MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WEBHOOKS/WRITEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | in |
| field:"operationName" kind:in |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all automation account operations (account creation, runbook creation, webhook creation) by the callerIpAddress in the 24 hours before and after the alert to identify a sequence of persistence activities
2. Find all webhook creations in the past 6 hours to determine if multiple persistence mechanisms are being established
3. Check if the callerIpAddress has created webhooks in the past 90 days to determine if this is typical automation workflow deployment
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Informational",
"location": "eastus",
"operationName": "MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WEBHOOKS/ACTION",
"operationVersion": "2021-06-22",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/automation-rg/providers/Microsoft.Automation/automationAccounts/my-automation/webhooks/malicious-webhook",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-22T10:30:00.0000000Z"
}
Azure Device Code Authentication with Broker Client
#Detects device code authentication using the Microsoft Broker Client application, which may indicate Primary Refresh Token (PRT) abuse. Device code flow allows adversaries to trick users into entering codes on attacker-controlled applications. When combined with Microsoft Broker Client (app ID 29d9ed98-a469-4536-ade2-f981bc1d605e), this may indicate PRT theft or replay attacks that bypass MFA and Conditional Access policies.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Stealth | |
| Lateral Movement |
Telemetry coverage
Detection logic
from panther_azuresignin_helpers import (
azure_signin_alert_context,
azure_signin_success,
is_sign_in_event,
)
# Microsoft Broker Client Application ID
BROKER_CLIENT_APP_ID = "29d9ed98-a469-4536-ade2-f981bc1d605e"
def rule(event):
if not is_sign_in_event(event) or not azure_signin_success(event):
return False
auth_protocol = event.deep_get("properties", "authenticationProtocol", default="").lower()
if auth_protocol != "devicecode":
return False
app_id = event.deep_walk(
"properties", "conditionalAccessAudiences", "applicationId", default=""
)
# deep_walk returns a list if multiple values found, or a string if one value
if isinstance(app_id, list):
return BROKER_CLIENT_APP_ID in app_id
return BROKER_CLIENT_APP_ID == app_id
def title(event):
user_principal_name = event.deep_get(
"properties", "userPrincipalName", default="<UNKNOWN_USER>"
)
source_ip = event.deep_get("properties", "ipAddress", default="<UNKNOWN_IP>")
return (
f"Device Code Authentication with Broker Client: User [{user_principal_name}] "
f"from IP [{source_ip}]"
)
def alert_context(event):
context = azure_signin_alert_context(event)
return context
Rule specification
AnalysisType: rule
Filename: azure_device_code_broker_client.py
RuleID: "Azure.Audit.DeviceCodeBrokerClient"
DisplayName: "Azure Device Code Authentication with Broker Client"
Enabled: true
LogTypes:
- Azure.Audit
Severity: Medium
DedupPeriodMinutes: 60
Description: >
Detects device code authentication using the Microsoft Broker Client application, which may indicate
Primary Refresh Token (PRT) abuse. Device code flow allows adversaries to trick users into entering
codes on attacker-controlled applications. When combined with Microsoft Broker Client (app ID
29d9ed98-a469-4536-ade2-f981bc1d605e), this may indicate PRT theft or replay attacks that bypass MFA
and Conditional Access policies.
Tags:
- Initial Access
- Phishing
- Use Alternate Authentication Material
- Valid Accounts
Reports:
MITRE ATT&CK:
- TA0001:T1566
- TA0001:T1566.002
- TA0005:T1550
- TA0005:T1550.001
- TA0008:T1078
- TA0008:T1078.004
Runbook: |
1. Query Azure.Audit logs for all device code authentication attempts by properties:userPrincipalName in the 24 hours before and after this event to identify the scope of potential phishing campaign or PRT abuse affecting this user
2. Check properties:deviceDetail to verify if the device is registered and compliant in Azure AD, and review whether the user recognizes this device and the sign-in activity as legitimate
3. Search for other users in the organization with similar device code authentication patterns using the broker client application ID to determine if this is an isolated incident or part of a broader attack campaign targeting multiple accounts
Reference: https://github.com/elastic/detection-rules/blob/main/rules/integrations/azure/initial_access_entra_id_device_code_auth_with_broker_client.toml
SummaryAttributes:
- properties:userPrincipalName
- callerIpAddress
- properties:deviceDetail:deviceId
- properties:appDisplayName
Stages and Predicates
Fires on Azure.Audit events when all of the conditions below hold.
Condition
operationNameisSign-in activityresultSignatureisSUCCESSproperties.authenticationProtocolisdevicecodeany of:
properties.conditionalAccessAudiences.applicationIdcontains29d9ed98-a469-4536-ade2-f981bc1d605eproperties.conditionalAccessAudiences.applicationIdis29d9ed98-a469-4536-ade2-f981bc1d605e
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
operationName | ne | Sign-in activity | excludes:operationName field:"operationName" value:"Sign-in activity" |
resultSignature | ne | SUCCESS | excludes:resultSignature field:"resultSignature" value:"SUCCESS" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
properties.authenticationProtocol | eq |
| field:"properties.authenticationProtocol" kind:eq value:"devicecode" |
properties.conditionalAccessAudiences.applicationId | contains |
| field:"properties.conditionalAccessAudiences.applicationId" kind:contains value:"29d9ed98-a469-4536-ade2-f981bc1d605e" |
properties.conditionalAccessAudiences.applicationId | eq |
| field:"properties.conditionalAccessAudiences.applicationId" kind:eq value:"29d9ed98-a469-4536-ade2-f981bc1d605e" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
userPrincipalName | properties.userPrincipalName |
ipAddress | properties.ipAddress |
Response runbook
1. Query Azure.Audit logs for all device code authentication attempts by properties:userPrincipalName in the 24 hours before and after this event to identify the scope of potential phishing campaign or PRT abuse affecting this user
2. Check properties:deviceDetail to verify if the device is registered and compliant in Azure AD, and review whether the user recognizes this device and the sign-in activity as legitimate
3. Search for other users in the organization with similar device code authentication patterns using the broker client application ID to determine if this is an isolated incident or part of a broader attack campaign targeting multiple accounts
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"Level": "4",
"callerIpAddress": "2.2.2.2",
"category": "SignInLogs",
"correlationId": "device-code-001",
"durationMs": 0,
"operationName": "Sign-in activity",
"operationVersion": "1.0",
"properties": {
"appDisplayName": "Microsoft Authentication Broker",
"appId": "29d9ed98-a469-4536-ade2-f981bc1d605e",
"authenticationProtocol": "deviceCode",
"conditionalAccessAudiences": [
{
"applicationId": "29d9ed98-a469-4536-ade2-f981bc1d605e"
},
{
"applicationId": "00000003-0000-0000-c000-111111111111"
}
],
"deviceDetail": {
"browser": "Chrome 120",
"deviceId": "device-attacker-456",
"operatingSystem": "Windows 10"
},
"ipAddress": "2.2.2.2",
"isInteractive": true,
"resourceDisplayName": "Microsoft Graph",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/1.1.1.1",
"userId": "user-victim-123",
"userPrincipalName": "aragorn@lotr.com"
},
"resourceId": "/tenants/tenant-123/providers/Microsoft.aadiam",
"resultSignature": "SUCCESS",
"resultType": "0",
"tenantId": "tenant-123",
"time": "2025-01-15 09:30:45.123"
}
Azure Diagnostic Settings Deleted
#Detects when Azure diagnostic settings are deleted. Deleting diagnostic settings disables logging and monitoring, which is a common technique to hide malicious activity.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | Activity Log event Microsoft.Insights/DiagnosticSettings/Delete: Delete a resource diagnostic setting |
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
DIAGNOSTIC_SETTINGS_DELETE = "MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE"
def rule(event):
return event.get(
"operationName", ""
).upper() == DIAGNOSTIC_SETTINGS_DELETE and azure_activity_success(event)
def title(event):
resource_id = event.get("resourceId", "")
resource = extract_resource_name_from_id(
resource_id, "diagnosticSettings", default="<UNKNOWN_RESOURCE>"
)
return f"Azure Diagnostic Settings deleted on [{resource}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
diagnostic_setting_name = extract_resource_name_from_id(
resource_id, "diagnosticSettings", default=""
)
if diagnostic_setting_name:
context["diagnostic_setting_name"] = diagnostic_setting_name
return context
Rule specification
AnalysisType: rule
Filename: azure_diagnostic_settings_deleted.py
RuleID: "Azure.MonitorActivity.DiagnosticSettings.Deleted"
DisplayName: "Azure Diagnostic Settings Deleted"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Medium
Description: >
Detects when Azure diagnostic settings are deleted. Deleting diagnostic settings disables logging and monitoring, which is a common technique to hide malicious activity.
Reports:
MITRE ATT&CK:
- TA0005:T1562.008 # Defense Evasion: Impair Defenses - Disable Cloud Logs
Tags:
- Defense Evasion
- Impair Defenses
- Disable Cloud Logs
Runbook: |
1. Query Azure Monitor Activity logs for all diagnostic settings and monitoring operations by the callerIpAddress in the 24 hours before and after this alert to identify if multiple resources are being de-monitored
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure resource modifications or suspicious activities from the same user or IP in the past 7 days to assess if this is part of a defense evasion campaign
Reference: https://docs.datadoghq.com/security/default_rules/apn-0ib-a6f/
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all diagnostic settings and monitoring operations by the callerIpAddress in the 24 hours before and after this alert to identify if multiple resources are being de-monitored
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure resource modifications or suspicious activities from the same user or IP in the past 7 days to assess if this is part of a defense evasion campaign
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"location": "eastus",
"operationName": "Microsoft.Insights/diagnosticSettings/delete",
"operationVersion": "2021-05-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.Storage/storageAccounts/mystorageaccount/providers/Microsoft.Insights/diagnosticSettings/mydiagnostics",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Disk Deleted
#Detects when an Azure managed disk is deleted. Unauthorized disk deletion can indicate ransomware activity where attackers destroy data or delete backup disks to prevent recovery. This may also indicate legitimate cleanup operations.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | Activity Log event Microsoft.Compute/disks/delete: Deletes the Disk |
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
DISK_DELETE = "MICROSOFT.COMPUTE/DISKS/DELETE"
def rule(event):
return event.get("operationName", "").upper() == DISK_DELETE and azure_activity_success(event)
def title(event):
resource_id = event.get("resourceId", "")
disk = extract_resource_name_from_id(resource_id, "disks", default="<UNKNOWN_DISK>")
return f"Azure disk deleted [{disk}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
disk_name = extract_resource_name_from_id(resource_id, "disks", default="")
if disk_name:
context["disk_name"] = disk_name
resource_group = extract_resource_name_from_id(resource_id, "resourceGroups", default="")
if resource_group:
context["resource_group"] = resource_group
return context
Rule specification
AnalysisType: rule
Filename: azure_disk_deleted.py
RuleID: "Azure.MonitorActivity.Compute.DiskDeleted"
DisplayName: "Azure Disk Deleted"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Info
Status: Experimental
Description: >
Detects when an Azure managed disk is deleted.
Unauthorized disk deletion can indicate ransomware activity where attackers destroy data or delete backup disks to prevent recovery.
This may also indicate legitimate cleanup operations.
Reports:
MITRE ATT&CK:
- TA0040:T1485 # Impact: Data Destruction
- TA0040:T1490 # Impact: Inhibit System Recovery
Tags:
- Impact
- Data Destruction
- Inhibit System Recovery
- Ransomware
Runbook: |
1. Query Azure Monitor Activity logs for all disk operations by the callerIpAddress in the 24 hours before and after the alert to identify if multiple disks are being deleted in sequence
2. Find all compute resource deletion events from the same caller in the past 7 days to assess if this is part of a broader data destruction pattern
3. Check if the source IP matches known VPN ranges or corporate network addresses associated with authorized administrators
Reference: https://learn.microsoft.com/en-us/rest/api/compute/disks/delete?view=rest-compute-2025-04-01#:~:text=Deletes%20a%20disk.,version=2025-01-02
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.COMPUTE/DISKS/DELETEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.COMPUTE/DISKS/DELETE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all disk operations by the callerIpAddress in the 24 hours before and after the alert to identify if multiple disks are being deleted in sequence
2. Find all compute resource deletion events from the same caller in the past 7 days to assess if this is part of a broader data destruction pattern
3. Check if the source IP matches known VPN ranges or corporate network addresses associated with authorized administrators
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"location": "eastus",
"operationName": "Microsoft.Compute/disks/delete",
"operationVersion": "2021-04-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.Compute/disks/mydisk",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Domain Federation Settings Modified
#Detects modifications to domain federation settings in Microsoft Entra ID, including changes to federation trust configurations and OIDC discovery endpoints. Adversaries who compromise administrative accounts may modify these settings to federate the tenant with attacker-controlled identity providers, enabling unauthorized access and MFA bypass. This technique allows attackers to establish persistent access by redirecting authentication to malicious infrastructure.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Detection logic
def rule(event):
operation_name = event.get("operationName", "")
# Branch 1: For "Set federation settings on domain" any change is suspicious
if "set federation settings on domain" in operation_name.lower():
return True
# Branch 2: For "Set domain authentication" check if LiveType property changed to Federated
if "set domain authentication" in operation_name.lower():
display_names = event.deep_walk(
"properties", "targetResources", "modifiedProperties", "displayName", default=[]
)
new_values = event.deep_walk(
"properties", "targetResources", "modifiedProperties", "newValue", default=[]
)
# Ensure we have lists (deep_walk returns single value if only one result)
if not isinstance(display_names, list):
display_names = [display_names] if display_names else []
if not isinstance(new_values, list):
new_values = [new_values] if new_values else []
if len(display_names) != len(new_values):
# Lists have different lengths; check all values with consistent approach
if "LiveType" in display_names and any(
isinstance(val, str) and "Federated" in val for val in new_values
):
return True
return False
# Check if the same property has displayName="LiveType" AND newValue contains "Federated"
for display_name, new_value in zip(display_names, new_values):
if (
display_name == "LiveType"
and isinstance(new_value, str)
and "Federated" in new_value
):
return True
return False
def title(event):
actor = event.deep_get(
"properties", "initiatedBy", "user", "userPrincipalName", default="<UNKNOWN_ACTOR>"
)
return f"Domain Federation Trust Settings Modified by [{actor}] "
def alert_context(event):
context = {}
# Add federation-specific context
context["operation_name"] = event.get("operationName", "<NO_OPERATION>")
context["activity_display_name"] = event.deep_get(
"properties", "activityDisplayName", default="<NO_ACTIVITY>"
)
context["category"] = event.get("category", "<NO_CATEGORY>")
# Add initiator details
context["initiator_user_id"] = event.deep_get(
"properties", "initiatedBy", "user", "id", default="<NO_USER_ID>"
)
context["initiator_display_name"] = event.deep_get(
"properties", "initiatedBy", "user", "displayName", default="<NO_DISPLAY_NAME>"
)
context["initiator_ip"] = event.deep_get(
"properties", "initiatedBy", "user", "ipAddress", default="<NO_IP>"
)
return context
Rule specification
AnalysisType: rule
Filename: azure_domain_trust_settings_modified.py
RuleID: "Azure.Audit.DomainSettingsModified"
DisplayName: "Azure Domain Federation Settings Modified"
Enabled: true
LogTypes:
- Azure.Audit
Severity: Medium
Description: >
Detects modifications to domain federation settings in Microsoft Entra ID, including changes to
federation trust configurations and OIDC discovery endpoints. Adversaries who compromise administrative
accounts may modify these settings to federate the tenant with attacker-controlled identity providers,
enabling unauthorized access and MFA bypass. This technique allows attackers to establish persistent
access by redirecting authentication to malicious infrastructure.
Tags:
- Persistence
- Modify Authentication Process
Reports:
MITRE ATT&CK:
- TA0003:T1556
- TA0003:T1556.006
Runbook: |
1. Query Azure.Audit logs for all federation-related operations by properties:initiatedBy:user:userPrincipalName in the 7 days before and after this change to determine if this modification was part of a broader attack campaign involving administrative credential compromise or privilege escalation
2. Verify with your identity team and the initiating administrator whether this federation settings change was authorized through proper change management procedures, and review the new OIDC discovery endpoint URL to confirm it points to a legitimate identity provider owned by your organization and not an attacker-controlled domain
3. Query Azure.Audit logs and sign-in logs for all authentication events using the affected domain in the 24 hours after the federation change to identify any suspicious token issuance or unauthorized access attempts, and if the modification was unauthorized, immediately revert the federation settings, revoke all active sessions for affected users, and review all API access and resource modifications that occurred during the compromise window
Reference: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft%20Entra%20ID/Analytic%20Rules/ADFSDomainTrustMods.yaml
SummaryAttributes:
- properties:initiatedBy:user:userPrincipalName
- properties:targetResources:displayName
- properties:initiatedBy:user:ipAddress
Stages and Predicates
Fires on Azure.Audit events when any of the conditions below holds.
Condition
any of:
operationNamecontainsset federation settings on domainall of:
operationNamecontainsset domain authenticationproperties.targetResources.modifiedProperties.displayNamecontainsLiveType
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | contains |
| field:"operationName" kind:contains |
properties.targetResources.modifiedProperties.displayName | contains |
| field:"properties.targetResources.modifiedProperties.displayName" kind:contains value:"LiveType" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
userPrincipalName | properties.initiatedBy.user.userPrincipalName |
Response runbook
1. Query Azure.Audit logs for all federation-related operations by properties:initiatedBy:user:userPrincipalName in the 7 days before and after this change to determine if this modification was part of a broader attack campaign involving administrative credential compromise or privilege escalation
2. Verify with your identity team and the initiating administrator whether this federation settings change was authorized through proper change management procedures, and review the new OIDC discovery endpoint URL to confirm it points to a legitimate identity provider owned by your organization and not an attacker-controlled domain
3. Query Azure.Audit logs and sign-in logs for all authentication events using the affected domain in the 24 hours after the federation change to identify any suspicious token issuance or unauthorized access attempts, and if the modification was unauthorized, immediately revert the federation settings, revoke all active sessions for affected users, and review all API access and resource modifications that occurred during the compromise window
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"Level": "4",
"callerIpAddress": "2.2.2.2",
"category": "DirectoryManagement",
"correlationId": "federation-change-001",
"durationMs": 0,
"operationName": "Set federation settings on domain",
"operationVersion": "1.0",
"p_event_time": "2025-01-15 09:30:25.123",
"p_log_type": "Azure.Audit",
"properties": {
"activityDateTime": "2025-01-15T09:30:25.1234567Z",
"activityDisplayName": "Set federation settings on domain",
"initiatedBy": {
"user": {
"displayName": "Compromised Admin",
"id": "admin-attacker-123",
"ipAddress": "2.2.2.2",
"userPrincipalName": "frodo@lotr.com"
}
},
"loggedByService": "Core Directory",
"operationName": "Set federation settings on domain",
"operationType": "Update",
"result": "success"
},
"resourceId": "/tenants/tenant-123/providers/Microsoft.aadiam",
"resultSignature": "None",
"tenantId": "tenant-123",
"time": "2025-01-15 09:30:25.123"
}
Azure Event Hub Deleted
#Detects when an Azure Event Hub is deleted. Event Hubs are critical event processing services that ingest and process large volumes of data for log collection, SIEM ingestion, and real-time analytics. Adversaries may delete Event Hubs to evade detection by disrupting data flows and erasing evidence of their malicious activities. Deletion of Event Hubs used for security logging can blind security teams to ongoing attacks.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
EVENT_HUB_DELETE_OPERATION = "MICROSOFT.EVENTHUB/NAMESPACES/EVENTHUBS/DELETE"
def rule(event):
return all(
[
event.get("operationName", "").upper() == EVENT_HUB_DELETE_OPERATION,
azure_activity_success(event),
]
)
def title(event):
resource_id = event.get("resourceId", "")
eventhub = extract_resource_name_from_id(resource_id, "eventhubs", default="<UNKNOWN_EVENTHUB>")
return f"Azure Event Hub Deleted: [{eventhub}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
eventhub_name = extract_resource_name_from_id(resource_id, "eventhubs", default="")
if eventhub_name:
context["eventhub_name"] = eventhub_name
namespace_name = extract_resource_name_from_id(resource_id, "namespaces", default="")
if namespace_name:
context["namespace_name"] = namespace_name
resource_group = extract_resource_name_from_id(resource_id, "resourceGroups", default="")
if resource_group:
context["resource_group"] = resource_group
return context
Rule specification
AnalysisType: rule
Filename: azure_event_hub_deleted.py
RuleID: "Azure.MonitorActivity.EventHub.Deleted"
DisplayName: "Azure Event Hub Deleted"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: High
Description: >
Detects when an Azure Event Hub is deleted. Event Hubs are critical event processing services
that ingest and process large volumes of data for log collection, SIEM ingestion, and real-time
analytics. Adversaries may delete Event Hubs to evade detection by disrupting data flows and
erasing evidence of their malicious activities. Deletion of Event Hubs used for security logging
can blind security teams to ongoing attacks.
Reports:
MITRE ATT&CK:
- TA0005:T1562.008 # Defense Evasion: Impair Defenses - Disable or Modify Cloud Logs
Tags:
- Defense Evasion
- Impair Defenses
- Disable Cloud Logs
Runbook: |
1. Query Azure Monitor Activity logs for all logging infrastructure operations (event hub deletions, diagnostic settings deletions, log analytics workspace deletions) by the callerIpAddress in the 24 hours before and after the alert
2. Find all event hub deletions in the past 6 hours to determine if this is part of a coordinated attack on security logging infrastructure
3. Check if the callerIpAddress has deleted event hubs in the past 90 days to establish if this is typical infrastructure management activity
Reference: https://learn.microsoft.com/en-us/azure/event-hubs/event-hubs-about
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.EVENTHUB/NAMESPACES/EVENTHUBS/DELETEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.EVENTHUB/NAMESPACES/EVENTHUBS/DELETE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all logging infrastructure operations (event hub deletions, diagnostic settings deletions, log analytics workspace deletions) by the callerIpAddress in the 24 hours before and after the alert
2. Find all event hub deletions in the past 6 hours to determine if this is part of a coordinated attack on security logging infrastructure
3. Check if the callerIpAddress has deleted event hubs in the past 90 days to establish if this is typical infrastructure management activity
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Informational",
"location": "eastus",
"operationName": "MICROSOFT.EVENTHUB/NAMESPACES/EVENTHUBS/DELETE",
"operationVersion": "2021-11-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/logging-rg/providers/Microsoft.EventHub/namespaces/security-logs-hub/eventhubs/siem-events",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-22T10:30:00.0000000Z"
}
Azure Excessive Account Lockouts
#Detects high volumes of failed Microsoft Entra ID sign-in attempts resulting in account lockouts, indicating potential brute-force credential attacks such as password spraying, password guessing, or credential stuffing. When adversaries repeatedly attempt authentication with incorrect credentials, Entra ID Smart Lockout policies trigger account lockouts (error code 50053).
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- Account Lockout (Sigma)
- Entra ID Excessive Account Lockouts Detected (Elastic)
Detection logic
from panther_azuresignin_helpers import azure_signin_alert_context
MICROSOFT_ASNS = {
"8075", # Microsoft Corporation
"8068", # Microsoft Corporation
"8069", # Microsoft Corporation
"8070", # Microsoft Corporation
"12076", # Microsoft Azure
}
def rule(event):
error_code = event.deep_get("properties", "status", "errorCode", default=None)
asn = event.deep_get("properties", "autonomousSystemNumber", default="")
user_principal_name = event.deep_get("properties", "userPrincipalName", default="")
# Exclude Microsoft ASN
return all([error_code == 50053, asn not in MICROSOFT_ASNS, user_principal_name])
def title(event):
user_principal_name = event.deep_get(
"properties", "userPrincipalName", default="<UNKNOWN_USER>"
)
return f"Excessive Account Lockouts Detected for [{user_principal_name}]"
def alert_context(event):
context = azure_signin_alert_context(event)
# Add lockout-specific context
context["error_code"] = event.deep_get("properties", "status", "errorCode", default=None)
context["failure_reason"] = event.deep_get(
"properties", "status", "failureReason", default="<NO_REASON>"
)
context["user_agent"] = event.deep_get("properties", "userAgent", default="<NO_USER_AGENT>")
context["app_display_name"] = event.deep_get("properties", "appDisplayName", default="<NO_APP>")
context["location_city"] = event.deep_get("properties", "location", "city", default="<NO_CITY>")
context["location_country"] = event.deep_get(
"properties", "location", "countryOrRegion", default="<NO_COUNTRY>"
)
context["asn"] = event.deep_get("properties", "autonomousSystemNumber", default="<NO_ASN>")
return context
Rule specification
AnalysisType: rule
Filename: azure_excessive_account_lockouts.py
RuleID: "Azure.Audit.ExcessiveAccountLockouts"
DisplayName: "Azure Excessive Account Lockouts"
Enabled: true
Status: Experimental
LogTypes:
- Azure.Audit
Severity: High
DedupPeriodMinutes: 60
Threshold: 20
Description: >
Detects high volumes of failed Microsoft Entra ID sign-in attempts resulting in account lockouts,
indicating potential brute-force credential attacks such as password spraying, password guessing, or
credential stuffing. When adversaries repeatedly attempt authentication with incorrect credentials,
Entra ID Smart Lockout policies trigger account lockouts (error code 50053).
Tags:
- Credential Access
- Brute Force
Reports:
MITRE ATT&CK:
- TA0006:T1110
- TA0006:T1110.001
- TA0006:T1110.003
- TA0006:T1110.004
Runbook: |
1. Query Azure.Audit logs for all authentication attempts from the attacking callerIpAddress in the 60 minutes before and after the alert to identify the full scope of targeted accounts and any successful compromises
2. Analyze properties:userAgent strings for automation indicators like Python, PowerShell, curl, or wget which suggest scripted attacks rather than manual login attempts
3. Check if any of the affected user accounts had successful sign-ins from the same callerIpAddress before or after the lockouts, which would indicate successful credential compromise requiring immediate password reset and session revocation
Reference: https://github.com/elastic/detection-rules/blob/main/rules/integrations/azure/credential_access_entra_id_excessive_account_lockouts.toml
SummaryAttributes:
- callerIpAddress
- properties:userPrincipalName
- properties:userAgent
- properties:location:city
- properties:location:countryOrRegion
Stages and Predicates
Fires on Azure.Audit events when all of the conditions below hold.
Condition
properties.status.errorCodeis50053properties.autonomousSystemNumberis not one of8075,8068,8069,8070,12076properties.userPrincipalNameis present
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
properties.autonomousSystemNumber | in | 12076, 8068, 8069, 8070, 8075 | excludes:properties.autonomousSystemNumber |
Indicators
These rows show field, operator, and value matches.
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
userPrincipalName | properties.userPrincipalName |
Response runbook
1. Query Azure.Audit logs for all authentication attempts from the attacking callerIpAddress in the 60 minutes before and after the alert to identify the full scope of targeted accounts and any successful compromises
2. Analyze properties:userAgent strings for automation indicators like Python, PowerShell, curl, or wget which suggest scripted attacks rather than manual login attempts
3. Check if any of the affected user accounts had successful sign-ins from the same callerIpAddress before or after the lockouts, which would indicate successful credential compromise requiring immediate password reset and session revocation
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"Level": "4",
"callerIpAddress": "4.4.4.4",
"category": "SignInLogs",
"correlationId": "lockout-attack-001",
"durationMs": 0,
"operationName": "Sign-in activity",
"operationVersion": "1.0",
"p_event_time": "2025-01-15 08:15:23.456",
"p_log_type": "Azure.Audit",
"p_row_id": "lockout-row-001",
"properties": {
"appDisplayName": "Microsoft Office",
"authenticationRequirement": "singleFactorAuthentication",
"autonomousSystemNumber": "12345",
"ipAddress": "4.4.4.4",
"location": {
"city": "Unknown",
"countryOrRegion": "RU",
"state": "Unknown"
},
"resourceDisplayName": "Microsoft Graph",
"status": {
"errorCode": 50053,
"failureReason": "The account is locked, you’ve tried to sign in too many times with an incorrect user ID or password."
},
"userAgent": "python-requests/2.28.0",
"userId": "user-001",
"userPrincipalName": "boromir@lotr.com"
},
"resourceId": "/tenants/tenant-123/providers/Microsoft.aadiam",
"resultSignature": "None",
"resultType": "Failure",
"tenantId": "tenant-123",
"time": "2025-01-15 08:15:23.456"
}
Azure Excessive IP and VM Discovery
#Detects excessive read operations on Azure public IP addresses and virtual machines. Adversaries may enumerate public IPs and virtual machines to identify external attack surfaces, map network topology, and identify potential targets for exploitation. This reconnaissance pattern often precedes lateral movement attempts, privilege escalation, or targeted attacks. The threshold-based detection triggers when the same resource type is read excessively within a time window.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance | |
| Discovery |
Detection logic
from panther_azureactivity_helpers import azure_activity_alert_context, azure_activity_success
PUBLIC_IP_READ = "MICROSOFT.NETWORK/PUBLICIPADDRESSES/READ"
VM_READ = "MICROSOFT.COMPUTE/VIRTUALMACHINES/READ"
def rule(event):
operation = event.get("operationName", "").upper()
return all(
[
operation in [PUBLIC_IP_READ, VM_READ],
azure_activity_success(event),
]
)
def title(event):
resource_id = event.get("resourceId", "<UNKNOWN_RESOURCE>")
operation = event.get("operationName", "").upper()
if operation == PUBLIC_IP_READ:
resource_type = "Public IP Address"
elif operation == VM_READ:
resource_type = "Virtual Machine"
else:
resource_type = "Resource"
return f"Azure Excessive {resource_type} Read on [{resource_id}]"
def alert_context(event):
return azure_activity_alert_context(event)
Rule specification
AnalysisType: rule
Filename: azure_network_ip_discovery.py
RuleID: "Azure.MonitorActivity.Network.IPDiscovery"
DisplayName: "Azure Excessive IP and VM Discovery"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Medium
Threshold: 50
Description: >
Detects excessive read operations on Azure public IP addresses and virtual machines. Adversaries
may enumerate public IPs and virtual machines to identify external attack surfaces, map network
topology, and identify potential targets for exploitation. This reconnaissance pattern often
precedes lateral movement attempts, privilege escalation, or targeted attacks. The threshold-based
detection triggers when the same resource type is read excessively within a time window.
Reports:
MITRE ATT&CK:
- TA0007:T1046 # Discovery: Network Service Discovery
- TA0007:T1018 # Discovery: Remote System Discovery
- TA0043:T1595.002 # Reconnaissance: Active Scanning - Vulnerability Scanning
Tags:
- AZT102
- Discovery
- Reconnaissance
- Network Service Discovery
- Remote System Discovery
- Active Scanning
- Vulnerability Scanning
Runbook: |
1. Query Azure Monitor Activity logs for all public IP address and virtual machine read operations by the callerIpAddress in the 24 hours before and after the alert to identify the scope of reconnaissance activity
2. Find all resource read operations (network security groups, route tables, virtual networks, storage accounts) by the same callerIpAddress in the 6 hours before and after the alert to determine if this is part of broader reconnaissance
3. Check if the callerIpAddress has performed similar high-volume read operations in the past 90 days to establish if this is normal administrative activity or anomalous behavior
Reference: https://microsoft.github.io/Azure-Threat-Research-Matrix/Reconnaissance/AZT101/AZT102/
SummaryAttributes:
- resourceId
- location
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameis one ofMICROSOFT.NETWORK/PUBLICIPADDRESSES/READ,MICROSOFT.COMPUTE/VIRTUALMACHINES/READresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | in |
| field:"operationName" kind:in |
resultType | in |
| field:"resultType" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
resourceId |
Response runbook
1. Query Azure Monitor Activity logs for all public IP address and virtual machine read operations by the callerIpAddress in the 24 hours before and after the alert to identify the scope of reconnaissance activity
2. Find all resource read operations (network security groups, route tables, virtual networks, storage accounts) by the same callerIpAddress in the 6 hours before and after the alert to determine if this is part of broader reconnaissance
3. Check if the callerIpAddress has performed similar high-volume read operations in the past 90 days to establish if this is normal administrative activity or anomalous behavior
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Informational",
"location": "eastus",
"operationName": "MICROSOFT.NETWORK/PUBLICIPADDRESSES/READ",
"operationVersion": "2021-05-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/network-rg/providers/Microsoft.Network/publicIPAddresses/myPublicIP",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-22T10:30:00.0000000Z"
}
Azure Excessive Network Security Group Read
#Detects excessive read operations on Azure Network Security Groups. Adversaries may repeatedly read Network Security Group configurations to map network ports and firewall rules as part of reconnaissance activities. This pattern can indicate an attacker attempting to understand network security controls before launching lateral movement or exfiltration attacks. The threshold-based detection triggers when the same resource is read excessively within a time window.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance | |
| Discovery |
Detection logic
from panther_azureactivity_helpers import azure_activity_alert_context, azure_activity_success
NETWORK_READ = "MICROSOFT.NETWORK/NETWORKSECURITYGROUP/READ"
def rule(event):
return all(
[
event.get("operationName", "").upper() == NETWORK_READ,
azure_activity_success(event),
]
)
def title(event):
resource_id = event.get("resourceId", "<UNKNOWN_RESOURCE>")
return f"Azure Excessive Network Read on [{resource_id}]"
def alert_context(event):
return azure_activity_alert_context(event)
Rule specification
AnalysisType: rule
Filename: azure_network_port_mapping.py
RuleID: "Azure.MonitorActivity.Network.PortMapping"
DisplayName: "Azure Excessive Network Security Group Read"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Medium
Threshold: 50
Description: >
Detects excessive read operations on Azure Network Security Groups. Adversaries may repeatedly
read Network Security Group configurations to map network ports and firewall rules as part of
reconnaissance activities. This pattern can indicate an attacker attempting to understand network
security controls before launching lateral movement or exfiltration attacks. The threshold-based
detection triggers when the same resource is read excessively within a time window.
Reports:
MITRE ATT&CK:
- TA0007:T1046 # Discovery: Network Service Discovery
- TA0043:T1595.002 # Reconnaissance: Active Scanning - Vulnerability Scanning
Tags:
- AZT101
- Discovery
- Reconnaissance
- Network Service Discovery
- Active Scanning
- Vulnerability Scanning
Runbook: |
1. Query Azure Monitor Activity logs for all Network Security Group read operations by the callerIpAddress in the 24 hours before and after the alert
2. Check if the same caller has performed other reconnaissance activities (reading route tables, virtual networks, or other network configurations)
3. Review the callerIpAddress and caller identity to determine if this is legitimate administrative activity or unauthorized access
4. Verify if the threshold (50 reads) aligns with normal operational patterns for your environment
Reference: https://microsoft.github.io/Azure-Threat-Research-Matrix/Reconnaissance/AZT101/AZT101/
SummaryAttributes:
- resourceId
- location
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.NETWORK/NETWORKSECURITYGROUP/READresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.NETWORK/NETWORKSECURITYGROUP/READ" |
resultType | in |
| field:"resultType" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
resourceId |
Response runbook
1. Query Azure Monitor Activity logs for all Network Security Group read operations by the callerIpAddress in the 24 hours before and after the alert
2. Check if the same caller has performed other reconnaissance activities (reading route tables, virtual networks, or other network configurations)
3. Review the callerIpAddress and caller identity to determine if this is legitimate administrative activity or unauthorized access
4. Verify if the threshold (50 reads) aligns with normal operational patterns for your environment
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Informational",
"location": "eastus",
"operationName": "MICROSOFT.NETWORK/NETWORKSECURITYGROUP/READ",
"operationVersion": "2021-05-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/network-rg/providers/Microsoft.Network/networkSecurityGroups/nsg-prod",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-22T10:30:00.0000000Z"
}
Azure Firewall Policy Deleted
#Detects when an Azure Firewall policy is deleted. Firewall policies define critical network security rules that control traffic flow and protect resources. Adversaries may delete firewall policies to disable network security controls, allow malicious traffic, or enable data exfiltration. This activity is a strong indicator of defense evasion or preparation for follow-on attacks.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | Activity Log event Microsoft.Network/firewallPolicies/delete: Deletes a Firewall Policy |
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
FIREWALL_POLICY_DELETE_OPERATION = "MICROSOFT.NETWORK/FIREWALLPOLICIES/DELETE"
def rule(event):
return event.get(
"operationName", ""
).upper() == FIREWALL_POLICY_DELETE_OPERATION and azure_activity_success(event)
def title(event):
resource_id = event.get("resourceId", "<UNKNOWN_RESOURCE>")
policy_name = extract_resource_name_from_id(
resource_id, "firewallPolicies", default="<UNKNOWN_POLICY>"
)
return f"Azure Firewall Policy Deleted: [{policy_name}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
policy_name = extract_resource_name_from_id(resource_id, "firewallPolicies", default="")
if policy_name:
context["firewall_policy_name"] = policy_name
resource_group = extract_resource_name_from_id(resource_id, "resourceGroups", default="")
if resource_group:
context["resource_group"] = resource_group
return context
Rule specification
AnalysisType: rule
Filename: azure_firewall_policy_deleted.py
RuleID: "Azure.MonitorActivity.Network.FirewallPolicyDeleted"
DisplayName: "Azure Firewall Policy Deleted"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: High
Description: >
Detects when an Azure Firewall policy is deleted. Firewall policies define critical network security
rules that control traffic flow and protect resources. Adversaries may delete firewall policies to
disable network security controls, allow malicious traffic, or enable data exfiltration. This activity
is a strong indicator of defense evasion or preparation for follow-on attacks.
Reports:
MITRE ATT&CK:
- TA0005:T1562.004 # Defense Evasion: Impair Defenses - Disable or Modify System Firewall
Tags:
- Defense Evasion
- Impair Defenses
- Disable or Modify System Firewall
Runbook: |
1. Query Azure Monitor Activity logs for all network security operations (firewall policy changes, NSG deletions, network watcher deletions) by the callerIpAddress in the 24 hours before and after the alert
2. Find all firewall policy deletions and modifications in the past 6 hours to determine if this is part of a coordinated attack on network security controls
3. Check if the callerIpAddress has performed similar network security changes in the past 90 days to establish if this is typical operational activity
Reference: https://github.com/elastic/detection-rules/blob/main/rules/integrations/azure/defense_evasion_azure_firewall_policy_deletion.toml
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
- location
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.NETWORK/FIREWALLPOLICIES/DELETEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.NETWORK/FIREWALLPOLICIES/DELETE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all network security operations (firewall policy changes, NSG deletions, network watcher deletions) by the callerIpAddress in the 24 hours before and after the alert
2. Find all firewall policy deletions and modifications in the past 6 hours to determine if this is part of a coordinated attack on network security controls
3. Check if the callerIpAddress has performed similar network security changes in the past 90 days to establish if this is typical operational activity
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Informational",
"location": "eastus",
"operationName": "MICROSOFT.NETWORK/FIREWALLPOLICIES/DELETE",
"operationVersion": "2021-05-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/network-rg/providers/Microsoft.Network/firewallPolicies/production-firewall-policy",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-22T10:30:00.0000000Z"
}
Azure High-Risk Sign-In
#Detects high-risk sign-in attempts flagged by Microsoft Entra ID Protection. These alerts indicate potential account compromise where Microsoft's machine learning has identified suspicious authentication patterns. High-risk sign-ins may result from credential theft, impossible travel, or unfamiliar locations.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry coverage
Detection logic
from panther_azuresignin_helpers import actor_user, azure_signin_alert_context, is_sign_in_event
def rule(event):
if not is_sign_in_event(event):
return False
risk_state = event.deep_get("properties", "riskState", default="").lower()
if risk_state in ["dismissed", "remediated"]:
return False
risk_level_during_signin = event.deep_get(
"properties", "riskLevelDuringSignIn", default=""
).lower()
risk_level_aggregated = event.deep_get("properties", "riskLevelAggregated", default="").lower()
return risk_level_during_signin == "high" or risk_level_aggregated == "high"
def title(event):
principal = actor_user(event)
if principal is None:
principal = "<NO_PRINCIPALNAME>"
ip_address = event.deep_get("properties", "ipAddress", default="<UNKNOWN_IP>")
return f"High-Risk Sign-In Detected: [{principal}] from [{ip_address}]"
def alert_context(event):
context = azure_signin_alert_context(event)
return context
Rule specification
AnalysisType: rule
Filename: azure_high_risk_signin.py
RuleID: "Azure.Audit.HighRiskSignIn"
DisplayName: "Azure High-Risk Sign-In"
Enabled: true
LogTypes:
- Azure.Audit
Severity: High
DedupPeriodMinutes: 60
Description: >
Detects high-risk sign-in attempts flagged by Microsoft Entra ID Protection. These alerts indicate potential account compromise where Microsoft's machine learning has identified suspicious authentication patterns. High-risk sign-ins may result from credential theft, impossible travel, or unfamiliar locations.
Reports:
MITRE ATT&CK:
- TA0001:T1078
Runbook: |
1. Query Azure.Audit sign-in logs for all authentication events by properties:userPrincipalName in the 24 hours before and after the alert to establish normal sign-in patterns
2. Check if callerIpAddress has been used by this user in the past 30 days and verify if the location matches expected geographic regions for the user
3. Find other high-risk or failed sign-in attempts for this user or from this IP address in the past 7 days to identify potential credential compromise patterns
Reference: https://learn.microsoft.com/en-us/entra/id-protection/howto-identity-protection-configure-risk-policies
SummaryAttributes:
- properties:userPrincipalName
- properties:servicePrincipalName
- callerIpAddress
- properties:riskLevelAggregated
- properties:riskLevelDuringSignIn
- properties:riskEventTypes
Stages and Predicates
Fires on Azure.Audit events when all of the conditions below hold.
Condition
operationNameisSign-in activityproperties.riskStateis not one ofdismissed,remediatedany of:
properties.riskLevelDuringSignInishighproperties.riskLevelAggregatedishigh
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
properties.riskState | in | dismissed, remediated | excludes:properties.riskState field:"properties.riskState" value:"dismissed" field:"properties.riskState" value:"remediated" |
Indicators
These rows show field, operator, and value matches.
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
ipAddress | properties.ipAddress |
Response runbook
1. Query Azure.Audit sign-in logs for all authentication events by properties:userPrincipalName in the 24 hours before and after the alert to establish normal sign-in patterns
2. Check if callerIpAddress has been used by this user in the past 30 days and verify if the location matches expected geographic regions for the user
3. Find other high-risk or failed sign-in attempts for this user or from this IP address in the past 7 days to identify potential credential compromise patterns
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"Level": "4",
"callerIpAddress": "4.4.4.4",
"category": "SignInLogs",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"durationMs": 0,
"location": "RU",
"operationName": "Sign-in activity",
"operationVersion": "1.0",
"p_event_time": "2025-01-15 14:23:10.123",
"p_log_type": "Azure.Audit",
"properties": {
"appId": "00000002-0000-0ff1-ce00-111111111111",
"authenticationProtocol": "oAuth2",
"clientAppUsed": "Browser",
"conditionalAccessStatus": "success",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"createdDateTime": "2025-01-15T14:23:10.1234567Z",
"ipAddress": "4.4.4.4",
"isInteractive": true,
"location": {
"city": "Moscow",
"countryOrRegion": "RU",
"geoCoordinates": {
"latitude": 55.7558,
"longitude": 37.6173
},
"state": "Moscow"
},
"resourceDisplayName": "Microsoft 365",
"resourceId": "00000002-0000-0ff1-ce00-111111111111",
"riskDetail": "aiConfirmedSigninSafe",
"riskEventTypes": [
"unfamiliarFeatures",
"anonymizedIPAddress"
],
"riskLevelAggregated": "none",
"riskLevelDuringSignIn": "high",
"riskState": "atRisk",
"status": {
"errorCode": 0
},
"tokenIssuerType": "AzureAD",
"userId": "user123-456-789",
"userPrincipalName": "john@justice.org"
},
"resourceId": "/tenants/tenant-id-123/providers/Microsoft.aadiam",
"resultSignature": "SUCCESS",
"resultType": "0",
"tenantId": "tenant-id-123",
"time": "2025-01-15 14:23:10.123"
}
Azure Invite External Users
#This detection looks for a Azure users inviting external users
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | Invite external user |
Rules detecting the same action
These rules filter on the same operation.
- [Entra ID] Authentication Method Changed for Privileged Account (Kusto)
- Account Created and Deleted in Short Timeframe (Kusto)
- Account Created And Deleted Within A Close Time Frame (Sigma)
- Account created from non-approved sources (Kusto)
- Account created or deleted by non-approved user (Kusto)
- Addition of a Temporary Access Pass to a Privileged Account (Kusto)
- Authentication Method Changed for Privileged Account (Kusto)
- Authentication Methods Changed for Privileged Account (Kusto)
Detection logic
from panther_msft_helpers import azure_rule_context, azure_success
def rule(event):
if not azure_success(event) or event.get("operationName") != "Invite external user":
return False
user_who_sent_invite = event.deep_get(
"properties", "initiatedBy", "user", "userPrincipalName", default=""
)
user_who_received_invite = event.deep_walk(
"properties", "additionalDetails", "value", return_val="last", default=""
)
domain = user_who_sent_invite.split("@")[-1]
different_domain = not user_who_received_invite.endswith(domain)
return different_domain
def title(event):
user_who_sent_invite = event.deep_get(
"properties", "initiatedBy", "user", "userPrincipalName", default=""
)
user_who_received_invite = event.deep_walk(
"properties", "additionalDetails", "value", return_val="last", default=""
)
return (
f"{user_who_sent_invite} invited {user_who_received_invite} to join as an EntraID member."
)
def alert_context(event):
return azure_rule_context(event)
Rule specification
AnalysisType: rule
Filename: azure_invite_external_users.py
RuleID: "Azure.Audit.InviteExternalUsers"
DisplayName: "Azure Invite External Users"
Enabled: true
LogTypes:
- Azure.Audit
Severity: Low
Description: >
This detection looks for a Azure users inviting external users
Reports:
MITRE ATT&CK:
- TA0001:T1078
Runbook: >
Verify the user permissions and investigate the external user details. If unauthorized, revoke access and block further invites. Update security policies.
Reference: https://learn.microsoft.com/en-us/entra/identity/authentication/overview-authentication
SummaryAttributes:
- properties:ServicePrincipalName
- properties:UserPrincipalName
- properties:initiatedBy:user:ipAddress
Stages and Predicates
Fires on Azure.Audit events when all of the conditions below hold.
Condition
properties.resultissuccessoperationNameisInvite external user
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
operationName | ne | Invite external user | excludes:operationName field:"operationName" value:"Invite external user" |
properties.result | ne | success | excludes:properties.result field:"properties.result" value:"success" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
operationName | |
category | properties.category |
actor_id | properties.initiatedBy.user.id |
actor_upn | properties.initiatedBy.user.userPrincipalName |
source_ip_address | properties.initiatedBy.user.ipAddress |
target_id | properties.targetResources.id |
target_name | properties.targetResources.displayName |
value | properties.additionalDetails.value |
Response runbook
Verify the user permissions and investigate the external user details. If unauthorized, revoke access and block further invites. Update security policies.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"Level": "4",
"callerIpAddress": "1.1.1.1",
"category": "AuditLogs",
"correlationId": "123456789",
"durationMs": 0,
"operationName": "Invite external user",
"operationVersion": "1.0",
"properties": {
"activityDateTime": "2024-09-23 14:33:09.049661100",
"activityDisplayName": "Invite external user",
"additionalDetails": [
{
"key": "oid",
"value": "123456789"
},
{
"key": "tid",
"value": "0123456789"
},
{
"key": "ipaddr",
"value": "1.2.3.4"
},
{
"key": "wids",
"value": "123456789"
},
{
"key": "InvitationId",
"value": "123456789"
},
{
"key": "invitedUserEmailAddress",
"value": "john@justice.org"
}
],
"category": "UserManagement",
"correlationId": "123456789",
"id": "Invited Users_123456789",
"initiatedBy": {
"user": {
"id": "123456789",
"ipAddress": "1.2.3.4",
"roles": [],
"userPrincipalName": "denethor@lotr.com"
}
},
"loggedByService": "Invited Users",
"operationType": "Add",
"result": "success",
"targetResources": [
{
"administrativeUnits": [],
"displayName": "Zeus.Theboss",
"id": "123456789",
"type": "User"
}
]
},
"resourceId": "/tenants/123456789/providers/Microsoft.aadiam",
"resultSignature": "None",
"tenantId": "123456789",
"time": "2024-12-10 14:33:09.049661100"
}
Azure Key Vault Certificate Accessed
#Detects when Azure Key Vault certificates are accessed via read operations. Adversaries may attempt to dump certificates to extract credentials for service principals or establish persistence through certificate-based authentication.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access | |
| Collection |
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
CERTIFICATE_READ = "MICROSOFT.KEYVAULT/VAULTS/CERTIFICATES/READ"
def rule(event):
operation = event.get("operationName", "").upper()
return operation == CERTIFICATE_READ and azure_activity_success(event)
def title(event):
resource_id = event.get("resourceId", "")
keyvault_name = extract_resource_name_from_id(resource_id, "vaults", default="UNKNOWN")
caller = event.get("callerIpAddress", default="<UNKNOWN_CALLER>")
return f"Azure Key Vault certificate accessed from [{keyvault_name}] by [{caller}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
keyvault_name = extract_resource_name_from_id(resource_id, "vaults", default="")
if keyvault_name:
context["keyvault_name"] = keyvault_name
certificate_name = extract_resource_name_from_id(resource_id, "certificates", default="")
if certificate_name:
context["certificate_name"] = certificate_name
return context
Rule specification
AnalysisType: rule
Filename: azure_keyvault_certificate_accessed.py
RuleID: "Azure.MonitorActivity.KeyVault.CertificateAccessed"
DisplayName: "Azure Key Vault Certificate Accessed"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Info
Description: >
Detects when Azure Key Vault certificates are accessed via read operations.
Adversaries may attempt to dump certificates to extract credentials for service
principals or establish persistence through certificate-based authentication.
Reports:
MITRE ATT&CK:
- TA0006:T1555 # Credential Access: Credentials from Password Stores
- TA0009:T1530 # Collection: Data from Cloud Storage
Tags:
- AZT604
- AZT604.2
- Credential Access
- Collection
- Credentials from Password Stores
- Data from Cloud Storage
Runbook: |
1. Find all Key Vault certificate and secret operations by the callerIpAddress in the 6 hours before and after this alert to identify patterns of bulk credential access
2. Query for the certificate details to determine which service principals or applications use this certificate for authentication
3. Check if the identity accessing the certificate has a history of Key Vault access in the past 30 days to determine if this is normal behavior
Reference: https://microsoft.github.io/Azure-Threat-Research-Matrix/CredentialAccess/AZT604/AZT604-2
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.KEYVAULT/VAULTS/CERTIFICATES/READresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.KEYVAULT/VAULTS/CERTIFICATES/READ" |
resultType | in |
| field:"resultType" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
callerIpAddress |
Response runbook
1. Find all Key Vault certificate and secret operations by the callerIpAddress in the 6 hours before and after this alert to identify patterns of bulk credential access
2. Query for the certificate details to determine which service principals or applications use this certificate for authentication
3. Check if the identity accessing the certificate has a history of Key Vault access in the past 30 days to determine if this is normal behavior
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"identity": {
"claims": {
"appid": "12345678-1234-1234-1234-123456789abc"
}
},
"location": "eastus",
"operationName": "Microsoft.KeyVault/vaults/certificates/read",
"operationVersion": "7.0",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.KeyVault/vaults/myvault/certificates/app-service-cert",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-23T10:30:00.0000000Z"
}
Azure Key Vault Deleted
#Detects when an Azure Key Vault is deleted. Key Vault deletion is a destructive operation that may indicate ransomware activity or malicious destruction of secrets and encryption keys.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | Activity Log event Microsoft.KeyVault/vaults/delete: Deletes a key vault |
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
KEYVAULT_DELETE = "MICROSOFT.KEYVAULT/VAULTS/DELETE"
def rule(event):
return event.get("operationName", "").upper() == KEYVAULT_DELETE and azure_activity_success(
event
)
def title(event):
resource_id = event.get("resourceId", "")
keyvault = extract_resource_name_from_id(resource_id, "vaults", default="<UNKNOWN_KEYVAULT>")
caller = event.get("callerIpAddress", default="<UNKNOWN_CALLER>")
return f"Azure Key Vault deleted [{keyvault}] from [{caller}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
keyvault_name = extract_resource_name_from_id(resource_id, "vaults", default="")
if keyvault_name:
context["keyvault_name"] = keyvault_name
return context
Rule specification
AnalysisType: rule
Filename: azure_keyvault_deleted.py
RuleID: "Azure.MonitorActivity.KeyVault.Deleted"
DisplayName: "Azure Key Vault Deleted"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Medium
Description: >
Detects when an Azure Key Vault is deleted.
Key Vault deletion is a destructive operation that may indicate ransomware activity or malicious destruction of secrets and encryption keys.
Reports:
MITRE ATT&CK:
- TA0040:T1485 # Impact: Data Destruction
- TA0040:T1490 # Impact: Inhibit System Recovery
Tags:
- Impact
- Data Destruction
- Inhibit System Recovery
- Ransomware
Runbook: |
1. Query Azure Monitor Activity logs for all Key Vault operations by the callerIpAddress in the 24 hours before this alert to establish if this is part of a larger attack pattern
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure resource deletions or key/secret access events from the same user or IP in the past 7 days to assess the scope of potential impact
Reference: https://learn.microsoft.com/en-us/azure/key-vault/general/soft-delete-overview
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.KEYVAULT/VAULTS/DELETEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.KEYVAULT/VAULTS/DELETE" |
resultType | in |
| field:"resultType" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
callerIpAddress |
Response runbook
1. Query Azure Monitor Activity logs for all Key Vault operations by the callerIpAddress in the 24 hours before this alert to establish if this is part of a larger attack pattern
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure resource deletions or key/secret access events from the same user or IP in the past 7 days to assess the scope of potential impact
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"location": "eastus",
"operationName": "Microsoft.KeyVault/vaults/delete",
"operationVersion": "2021-04-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.KeyVault/vaults/mykeyvault",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Key Vault Key Accessed or Recovered
#Detects when Azure Key Vault cryptographic keys are accessed via read operations or recovered/restored. Key Vault keys contain public key information used for encryption, decryption, and signing operations. While private keys cannot be directly exported from Key Vault, adversaries may access key metadata and properties to understand the encryption architecture, identify sensitive keys, or enumerate available cryptographic resources for targeted attacks.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access | |
| Discovery |
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
KEY_READ = "MICROSOFT.KEYVAULT/VAULTS/KEYS/READ/ACTION"
KEY_RESTORED = "MICROSOFT.KEYVAULT/VAULTS/KEYS/RESTORE/ACTION"
KEY_RECOVERED = "MICROSOFT.KEYVAULT/VAULTS/KEYS/RECOVER/ACTION"
def rule(event):
operation = event.get("operationName", "").upper()
return operation in [KEY_READ, KEY_RESTORED, KEY_RECOVERED] and azure_activity_success(event)
def title(event):
resource_id = event.get("resourceId", "")
keyvault_name = extract_resource_name_from_id(resource_id, "vaults", default="")
caller = event.get("callerIpAddress", default="<UNKNOWN_CALLER>")
operation = event.get("operationName", "").upper()
action = None
if operation == KEY_READ:
action = "read"
elif operation == KEY_RESTORED:
action = "restored"
elif operation == KEY_RECOVERED:
action = "recovered"
if action is not None:
return f"Azure Key Vault key {action} from [{keyvault_name}] by [{caller}]"
return f"Azure Key Vault key accessed from [{keyvault_name}] by [{caller}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
keyvault_name = extract_resource_name_from_id(resource_id, "vaults", default="")
if keyvault_name:
context["keyvault_name"] = keyvault_name
key_name = extract_resource_name_from_id(resource_id, "keys", default="")
if key_name:
context["key_name"] = key_name
return context
Rule specification
AnalysisType: rule
Filename: azure_keyvault_key_accessed_or_recovered.py
RuleID: "Azure.MonitorActivity.KeyVault.KeyAccessed.Recovered"
DisplayName: "Azure Key Vault Key Accessed or Recovered"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Medium
Description: >
Detects when Azure Key Vault cryptographic keys are accessed via read operations or recovered/restored.
Key Vault keys contain public key information used for encryption, decryption, and signing operations.
While private keys cannot be directly exported from Key Vault, adversaries may access key metadata
and properties to understand the encryption architecture, identify sensitive keys, or enumerate
available cryptographic resources for targeted attacks.
Reports:
MITRE ATT&CK:
- TA0006:T1555 # Credential Access: Credentials from Password Stores
- TA0007:T1087.004 # Discovery: Account Discovery - Cloud Account
Tags:
- AZT604
- AZT604.3
- AZT704
- AZT704.1
- Credential Access
- Discovery
- Credentials from Password Stores
- Account Discovery
- Cloud Account
Runbook: |
1. Find all Key Vault key, certificate, and secret operations by the callerIpAddress in the 6 hours before and after this alert to identify patterns of credential enumeration
2. Query for the key usage to determine which applications or services rely on this key for encryption operations
3. Check if the identity accessing the key has performed similar Key Vault operations in the past 30 days to determine if this is normal behavior
Reference: https://microsoft.github.io/Azure-Threat-Research-Matrix/CredentialAccess/AZT604/AZT604-3
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameis one ofMICROSOFT.KEYVAULT/VAULTS/KEYS/READ/ACTION,MICROSOFT.KEYVAULT/VAULTS/KEYS/RESTORE/ACTION,MICROSOFT.KEYVAULT/VAULTS/KEYS/RECOVER/ACTIONresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | in |
| field:"operationName" kind:in |
resultType | in |
| field:"resultType" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
callerIpAddress |
Response runbook
1. Find all Key Vault key, certificate, and secret operations by the callerIpAddress in the 6 hours before and after this alert to identify patterns of credential enumeration
2. Query for the key usage to determine which applications or services rely on this key for encryption operations
3. Check if the identity accessing the key has performed similar Key Vault operations in the past 30 days to determine if this is normal behavior
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"identity": {
"claims": {
"appid": "12345678-1234-1234-1234-123456789abc"
}
},
"location": "eastus",
"operationName": "Microsoft.KeyVault/vaults/keys/read/action",
"operationVersion": "7.0",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.KeyVault/vaults/myvault/keys/encryption-key",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-23T10:30:00.0000000Z"
}
Azure Key Vault Key Permanently Purged
#Detects when an Azure Key Vault key is permanently purged. Purging a key is an irreversible operation that permanently destroys cryptographic keys. If done on an unrecognized key, it may indicate ransomware or malicious destruction.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact |
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
KEYVAULT_KEY_PURGE = "MICROSOFT.KEYVAULT/VAULTS/KEYS/PURGE/ACTION"
def rule(event):
return event.get("operationName", "").upper() == KEYVAULT_KEY_PURGE and azure_activity_success(
event
)
def title(event):
resource_id = event.get("resourceId", "")
keyvault = extract_resource_name_from_id(resource_id, "vaults", default="<UNKNOWN_KEYVAULT>")
caller = event.get("callerIpAddress", default="<UNKNOWN_CALLER>")
return f"Azure Key Vault key permanently purged on [{keyvault}] from [{caller}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
keyvault_name = extract_resource_name_from_id(resource_id, "vaults", default="")
key_name = extract_resource_name_from_id(resource_id, "keys", default="")
if keyvault_name:
context["keyvault_name"] = keyvault_name
if key_name:
context["key_name"] = key_name
return context
Rule specification
AnalysisType: rule
Filename: azure_keyvault_key_purged.py
RuleID: "Azure.MonitorActivity.KeyVault.KeyPurged"
DisplayName: "Azure Key Vault Key Permanently Purged"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Info
Description: >
Detects when an Azure Key Vault key is permanently purged.
Purging a key is an irreversible operation that permanently destroys cryptographic keys.
If done on an unrecognized key, it may indicate ransomware or malicious destruction.
Reports:
MITRE ATT&CK:
- TA0040:T1485 # Impact: Data Destruction
- TA0040:T1490 # Impact: Inhibit System Recovery
Tags:
- Impact
- Data Destruction
- Inhibit System Recovery
- Ransomware
Runbook: |
1. Query Azure Monitor Activity logs for all Key Vault operations by the callerIpAddress in the 24 hours before this alert to identify if multiple keys or secrets are being purged
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure Key Vault deletions, purges, or destructive operations from the same user or IP in the past 7 days to assess the scope of potential data destruction
Reference: https://learn.microsoft.com/en-us/azure/key-vault/general/soft-delete-overview
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.KEYVAULT/VAULTS/KEYS/PURGE/ACTIONresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.KEYVAULT/VAULTS/KEYS/PURGE/ACTION" |
resultType | in |
| field:"resultType" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
callerIpAddress |
Response runbook
1. Query Azure Monitor Activity logs for all Key Vault operations by the callerIpAddress in the 24 hours before this alert to identify if multiple keys or secrets are being purged
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure Key Vault deletions, purges, or destructive operations from the same user or IP in the past 7 days to assess the scope of potential data destruction
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"location": "eastus",
"operationName": "Microsoft.KeyVault/vaults/keys/purge/action",
"operationVersion": "2021-04-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.KeyVault/vaults/mykeyvault/keys/mykey",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Key Vault Permanently Purged
#Detects when an entire Azure Key Vault is permanently purged. Purging a Key Vault is an irreversible operation that permanently destroys all keys, secrets, and certificates stored within it. This is more destructive than deleting a vault and may indicate ransomware activity or malicious data destruction.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
KEYVAULT_PURGE = "MICROSOFT.KEYVAULT/LOCATIONS/DELETEDVAULTS/PURGE/ACTION"
def rule(event):
return event.get("operationName", "").upper() == KEYVAULT_PURGE and azure_activity_success(
event
)
def title(event):
resource_id = event.get("resourceId", "")
keyvault = extract_resource_name_from_id(
resource_id, "deletedVaults", default="<UNKNOWN_KEYVAULT>"
)
caller = event.get("callerIpAddress", default="<UNKNOWN_CALLER>")
return f"Azure Key Vault permanently purged [{keyvault}] from [{caller}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
keyvault_name = extract_resource_name_from_id(resource_id, "deletedVaults", default="")
if keyvault_name:
context["keyvault_name"] = keyvault_name
return context
Rule specification
AnalysisType: rule
Filename: azure_keyvault_purged.py
RuleID: "Azure.MonitorActivity.KeyVault.Purged"
DisplayName: "Azure Key Vault Permanently Purged"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Low
Description: >
Detects when an entire Azure Key Vault is permanently purged.
Purging a Key Vault is an irreversible operation that permanently destroys all keys, secrets, and certificates stored within it.
This is more destructive than deleting a vault and may indicate ransomware activity or malicious data destruction.
Reports:
MITRE ATT&CK:
- TA0040:T1485 # Impact: Data Destruction
- TA0040:T1490 # Impact: Inhibit System Recovery
Tags:
- Impact
- Data Destruction
- Inhibit System Recovery
- Ransomware
Runbook: |
1. Query Azure MonitorActivity logs for all Key Vault delete and purge operations by the callerIpAddress in the 24 hours before and after this alert to identify if multiple vaults are being targeted
2. Check if the callerIpAddress is associated with known cloud providers, VPN services, or threat intelligence indicators
3. Search for other Azure resource deletion or purge operations from the same callerIpAddress in the past 7 days to determine the scope of data destruction activity
Reference: https://learn.microsoft.com/en-us/azure/key-vault/general/soft-delete-overview
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.KEYVAULT/LOCATIONS/DELETEDVAULTS/PURGE/ACTIONresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.KEYVAULT/LOCATIONS/DELETEDVAULTS/PURGE/ACTION" |
resultType | in |
| field:"resultType" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
callerIpAddress |
Response runbook
1. Query Azure MonitorActivity logs for all Key Vault delete and purge operations by the callerIpAddress in the 24 hours before and after this alert to identify if multiple vaults are being targeted
2. Check if the callerIpAddress is associated with known cloud providers, VPN services, or threat intelligence indicators
3. Search for other Azure resource deletion or purge operations from the same callerIpAddress in the past 7 days to determine the scope of data destruction activity
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"location": "eastus",
"operationName": "Microsoft.KeyVault/locations/deletedVaults/purge/action",
"operationVersion": "2021-04-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/providers/Microsoft.KeyVault/locations/eastus/deletedVaults/mykeyvault",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Key Vault Secret Accessed or Recovered
#Detects when Azure Key Vault secrets are accessed or when soft-deleted secrets are recovered. Recovering soft-deleted secrets may indicate an attacker attempting to extract previously deleted credentials.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access | |
| Collection |
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
SECRET_GET = "MICROSOFT.KEYVAULT/VAULTS/SECRETS/GETSECRET/ACTION" # nosec B105
SECRET_RECOVER = "MICROSOFT.KEYVAULT/VAULTS/SECRETS/RECOVER/ACTION" # nosec B105
SECRET_RESTORE = "MICROSOFT.KEYVAULT/VAULTS/SECRETS/RESTORE/ACTION" # nosec B105
def rule(event):
operation = event.get("operationName", "").upper()
return all(
[
operation in [SECRET_GET, SECRET_RECOVER, SECRET_RESTORE],
azure_activity_success(event),
]
)
def title(event):
operation = event.get("operationName", "").upper()
resource_id = event.get("resourceId", "")
keyvault_name = extract_resource_name_from_id(resource_id, "vaults", default="")
caller = event.get("callerIpAddress", default="<UNKNOWN_CALLER>")
if operation == SECRET_RECOVER:
return (
f"Azure Key Vault soft-deleted secret recovered "
f"from [{keyvault_name}] by [{caller}]"
)
return f"Azure Key Vault secret accessed from [{keyvault_name}] by [{caller}]"
def severity(event):
operation = event.get("operationName", "").upper()
# Recovering soft-deleted secrets is more suspicious
if operation == SECRET_RECOVER:
return "MEDIUM"
return "DEFAULT"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
keyvault_name = extract_resource_name_from_id(resource_id, "vaults", default="")
if keyvault_name:
context["keyvault_name"] = keyvault_name
secret_name = extract_resource_name_from_id(resource_id, "secrets", default="")
if secret_name:
context["secret_name"] = secret_name
return context
Rule specification
AnalysisType: rule
Filename: azure_keyvault_secret_accessed_or_recovered.py
RuleID: "Azure.MonitorActivity.KeyVault.SecretAccessedOrRecovered"
DisplayName: "Azure Key Vault Secret Accessed or Recovered"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Medium
Description: >
Detects when Azure Key Vault secrets are accessed or when soft-deleted secrets are recovered.
Recovering soft-deleted secrets may indicate an attacker attempting to extract previously deleted credentials.
Reports:
MITRE ATT&CK:
- TA0006:T1555 # Credential Access: Credentials from Password Stores
- TA0009:T1530 # Collection: Data from Cloud Storage
Tags:
- AZT604.1
- AZT704.3
- Credential Access
- Collection
- Credentials from Password Stores
- Data from Cloud Storage
Runbook: |
1. Query Azure Monitor Activity logs for all Key Vault operations by the callerIpAddress in the 6 hours before and after this alert to identify patterns of secret enumeration or bulk access
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for role assignment changes or privilege escalation events from the same identity in the 24 hours before this secret access to assess if the identity was recently compromised
Reference: https://blog.pwnedlabs.io/climbing-the-azure-ladder-part-1
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameis one ofMICROSOFT.KEYVAULT/VAULTS/SECRETS/GETSECRET/ACTION,MICROSOFT.KEYVAULT/VAULTS/SECRETS/RECOVER/ACTION,MICROSOFT.KEYVAULT/VAULTS/SECRETS/RESTORE/ACTIONresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | in |
| field:"operationName" kind:in |
resultType | in |
| field:"resultType" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
callerIpAddress |
Response runbook
1. Query Azure Monitor Activity logs for all Key Vault operations by the callerIpAddress in the 6 hours before and after this alert to identify patterns of secret enumeration or bulk access
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for role assignment changes or privilege escalation events from the same identity in the 24 hours before this secret access to assess if the identity was recently compromised
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"identity": {
"claims": {
"appid": "12345678-1234-1234-1234-123456789abc"
}
},
"location": "eastus",
"operationName": "Microsoft.KeyVault/vaults/secrets/getSecret/action",
"operationVersion": "7.0",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.KeyVault/vaults/myvault/secrets/database-password",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Kubernetes RoleBinding or ClusterRoleBinding Created
#Detects when a RoleBinding or ClusterRoleBinding is created in Azure Kubernetes Service (AKS) or Arc-enabled Kubernetes clusters. Role bindings grant permissions to Kubernetes subjects (users, groups, or service accounts) by binding them to roles with specific permissions. Adversaries with appropriate access may create malicious role bindings to escalate privileges, assign cluster-admin roles, or maintain persistent access to the Kubernetes cluster. This detection applies to both AKS managed clusters and Arc-enabled connected clusters.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
ROLEBINDING_OPERATIONS = [
# Arc-enabled Kubernetes clusters
"MICROSOFT.KUBERNETES/CONNECTEDCLUSTERS/RBAC.AUTHORIZATION.K8S.IO/ROLEBINDINGS/WRITE",
"MICROSOFT.KUBERNETES/CONNECTEDCLUSTERS/RBAC.AUTHORIZATION.K8S.IO/CLUSTERROLEBINDINGS/WRITE",
# AKS managed clusters
"MICROSOFT.CONTAINERSERVICE/MANAGEDCLUSTERS/RBAC.AUTHORIZATION.K8S.IO/ROLEBINDINGS/WRITE",
(
"MICROSOFT.CONTAINERSERVICE/MANAGEDCLUSTERS/RBAC.AUTHORIZATION.K8S.IO/"
"CLUSTERROLEBINDINGS/WRITE"
),
]
def rule(event):
return event.get(
"operationName", ""
).upper() in ROLEBINDING_OPERATIONS and azure_activity_success(event)
def title(event):
operation = event.get("operationName", "").upper()
binding_type = "ClusterRoleBinding" if "CLUSTERROLEBINDINGS" in operation else "RoleBinding"
resource_id = event.get("resourceId", "<UNKNOWN_RESOURCE>")
cluster_name = extract_resource_name_from_id(resource_id, "connectedClusters", default="")
if not cluster_name:
cluster_name = extract_resource_name_from_id(
resource_id, "managedClusters", default="<UNKNOWN_CLUSTER>"
)
title_str = f"Azure Kubernetes {binding_type} Created in [{cluster_name}]"
return title_str
def alert_context(event):
context = azure_activity_alert_context(event)
operation = event.get("operationName", "").upper()
context["binding_type"] = (
"cluster_role_binding" if "CLUSTERROLEBINDINGS" in operation else "role_binding"
)
resource_id = event.get("resourceId", "")
cluster_name = extract_resource_name_from_id(resource_id, "connectedClusters", default="")
if cluster_name:
context["cluster_name"] = cluster_name
context["cluster_type"] = "arc_enabled"
else:
cluster_name = extract_resource_name_from_id(resource_id, "managedClusters", default="")
if cluster_name:
context["cluster_name"] = cluster_name
context["cluster_type"] = "aks_managed"
return context
Rule specification
AnalysisType: rule
Filename: azure_kubernetes_rolebinding_created.py
RuleID: "Azure.MonitorActivity.Kubernetes.RoleBindingCreated"
DisplayName: "Azure Kubernetes RoleBinding or ClusterRoleBinding Created"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Medium
Description: >
Detects when a RoleBinding or ClusterRoleBinding is created in Azure Kubernetes Service (AKS)
or Arc-enabled Kubernetes clusters. Role bindings grant permissions to Kubernetes subjects
(users, groups, or service accounts) by binding them to roles with specific permissions.
Adversaries with appropriate access may create malicious role bindings to escalate privileges,
assign cluster-admin roles, or maintain persistent access to the Kubernetes cluster. This
detection applies to both AKS managed clusters and Arc-enabled connected clusters.
Reports:
MITRE ATT&CK:
- TA0004:T1078.004 # Privilege Escalation: Valid Accounts - Cloud Accounts
- TA0003:T1098 # Persistence: Account Manipulation
Tags:
- Privilege Escalation
- Persistence
- Valid Accounts
- Cloud Accounts
- Account Manipulation
Runbook: |
1. Query Azure Monitor Activity logs for all Kubernetes RBAC operations (rolebinding creation, clusterrolebinding creation, role modifications) by the callerIpAddress in the 24 hours before and after the alert
2. Find all rolebinding and clusterrolebinding creations across all Kubernetes clusters in the past 6 hours to identify if this is part of a privilege escalation campaign
3. Check if the callerIpAddress has created role bindings in the past 90 days to determine if this is typical cluster administration activity
Reference: https://github.com/elastic/detection-rules/blob/main/rules/integrations/azure/privilege_escalation_kubernetes_aks_rolebinding_created.toml
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
- location
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameis one ofMICROSOFT.KUBERNETES/CONNECTEDCLUSTERS/RBAC.AUTHORIZATION.K8S.IO/ROLEBINDINGS/WRITE,MICROSOFT.KUBERNETES/CONNECTEDCLUSTERS/RBAC.AUTHORIZATION.K8S.IO/CLUSTERROLEBINDINGS/WRITE,MICROSOFT.CONTAINERSERVICE/MANAGEDCLUSTERS/RBAC.AUTHORIZATION.K8S.IO/ROLEBINDINGS/WRITE,MICROSOFT.CONTAINERSERVICE/MANAGEDCLUSTERS/RBAC.AUTHORIZATION.K8S.IO/CLUSTERROLEBINDINGS/WRITEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | in |
| field:"operationName" kind:in |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all Kubernetes RBAC operations (rolebinding creation, clusterrolebinding creation, role modifications) by the callerIpAddress in the 24 hours before and after the alert
2. Find all rolebinding and clusterrolebinding creations across all Kubernetes clusters in the past 6 hours to identify if this is part of a privilege escalation campaign
3. Check if the callerIpAddress has created role bindings in the past 90 days to determine if this is typical cluster administration activity
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Informational",
"location": "eastus",
"operationName": "MICROSOFT.KUBERNETES/CONNECTEDCLUSTERS/RBAC.AUTHORIZATION.K8S.IO/ROLEBINDINGS/WRITE",
"operationVersion": "2021-10-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/kubernetes-rg/providers/Microsoft.Kubernetes/connectedClusters/arc-cluster-01/rbac.authorization.k8s.io/rolebindings/malicious-binding",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-22T10:30:00.0000000Z"
}
Azure Log Analytics Workspace Deleted
#Detects when an Azure Log Analytics Workspace is deleted. Deleting a Log Analytics Workspace destroys centralized logging infrastructure and is a defense evasion technique.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment | |
| Impact |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
WORKSPACE_DELETE = "MICROSOFT.OPERATIONALINSIGHTS/WORKSPACES/DELETE"
def rule(event):
return event.get("operationName", "").upper() == WORKSPACE_DELETE and azure_activity_success(
event
)
def title(event):
resource_id = event.get("resourceId", "")
workspace = extract_resource_name_from_id(
resource_id, "workspaces", default="<UNKNOWN_WORKSPACE>"
)
return f"Azure Log Analytics Workspace deleted [{workspace}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
workspace_name = extract_resource_name_from_id(resource_id, "workspaces", default="")
if workspace_name:
context["workspace_name"] = workspace_name
resource_group = extract_resource_name_from_id(resource_id, "resourceGroups", default="")
if resource_group:
context["resource_group"] = resource_group
return context
Rule specification
AnalysisType: rule
Filename: azure_log_analytics_workspace_deleted.py
RuleID: "Azure.MonitorActivity.LogAnalyticsWorkspace.Deleted"
DisplayName: "Azure Log Analytics Workspace Deleted"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Medium
Description: >
Detects when an Azure Log Analytics Workspace is deleted.
Deleting a Log Analytics Workspace destroys centralized logging infrastructure and is a defense evasion technique.
Reports:
MITRE ATT&CK:
- TA0005:T1562.008 # Defense Evasion: Impair Defenses - Disable Cloud Logs
- TA0040:T1485 # Impact: Data Destruction
Tags:
- Defense Evasion
- Impact
- Impair Defenses
- Disable Cloud Logs
- Data Destruction
Runbook: |
1. Query Azure Monitor Activity logs for all Log Analytics and monitoring operations by the callerIpAddress in the 24 hours before this alert to identify if this is part of a larger attack on monitoring infrastructure
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure monitoring resource deletions or security tool disablements from the same user or IP in the past 7 days to assess the scope of defense evasion
Reference: https://learn.microsoft.com/en-us/azure/azure-monitor/logs/delete-workspace?tabs=azure-portal
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.OPERATIONALINSIGHTS/WORKSPACES/DELETEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.OPERATIONALINSIGHTS/WORKSPACES/DELETE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all Log Analytics and monitoring operations by the callerIpAddress in the 24 hours before this alert to identify if this is part of a larger attack on monitoring infrastructure
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure monitoring resource deletions or security tool disablements from the same user or IP in the past 7 days to assess the scope of defense evasion
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"location": "eastus",
"operationName": "Microsoft.OperationalInsights/workspaces/delete",
"operationVersion": "2021-06-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.OperationalInsights/workspaces/myworkspace",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Many Failed SignIns
#This detection looks for a number of failed sign-ins for the same ServicePrincipalName or UserPrincipalName
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access |
Telemetry coverage
Detection logic
from panther_azuresignin_helpers import actor_user, azure_signin_alert_context, is_sign_in_event
def rule(event):
if not is_sign_in_event(event):
return False
error_code = event.deep_get("properties", "status", "errorCode", default=0)
return error_code > 0
def title(event):
principal = actor_user(event)
if principal is None:
principal = "<NO_PRINCIPALNAME>"
return f"AzureSignIn: Multiple Failed LogIns for Principal [{principal}]"
def dedup(event):
principal = actor_user(event)
if principal is None:
principal = "<NO_PRINCIPALNAME>"
return principal
def alert_context(event):
return azure_signin_alert_context(event)
Rule specification
AnalysisType: rule
Filename: azure_failed_signins.py
RuleID: "Azure.Audit.ManyFailedSignIns"
DisplayName: "Azure Many Failed SignIns"
Enabled: true
# Ten Failed Sign-Ins(Threshold) in Ten Minutes(DedupPeriodMinutes)
Threshold: 10
DedupPeriodMinutes: 10
LogTypes:
- Azure.Audit
Severity: Medium
Description: >
This detection looks for a number of failed sign-ins for the same
ServicePrincipalName or UserPrincipalName
Reports:
MITRE ATT&CK:
- TA0006:T1110
- TA0001:T1078
Runbook: |
Querying Sign-In logs for the ServicePrincipalName or UserPrincipalName may indicate
that the principal is under attack, or that a sign-in credential rolled and some
user of the credential didn't get updated.
Reference: https://learn.microsoft.com/en-us/entra/identity/authentication/overview-authentication
SummaryAttributes:
- properties:ServicePrincipalName
- properties:UserPrincipalName
- properties:ipAddress
Stages and Predicates
Fires on Azure.Audit events when all of the conditions below hold.
Condition
operationNameisSign-in activityproperties.status.errorCodeis greater than0
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"Sign-in activity" |
properties.status.errorCode | gt |
| field:"properties.status.errorCode" kind:gt value:"0" |
Response runbook
Querying Sign-In logs for the ServicePrincipalName or UserPrincipalName may indicate
that the principal is under attack, or that a sign-in credential rolled and some
user of the credential didn't get updated.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"Level": "4",
"calleripaddress": "12.12.12.12",
"category": "ServicePrincipalSignInLogs",
"correlationid": "e1f237ef-6548-4172-be79-03818c04c06e",
"durationms": 0,
"location": "IE",
"operationname": "Sign-in activity",
"operationversion": 1,
"p_event_time": "2023-07-26 23:00:20.889",
"p_log_type": "Azure.Audit",
"properties": {
"appId": "cfceb902-8fab-4f8c-88ba-374d3c975c3a",
"authenticationProcessingDetails": [
{
"key": "Azure AD App Authentication Library",
"value": ""
}
],
"authenticationProtocol": "none",
"clientCredentialType": "none",
"conditionalAccessStatus": "notApplied",
"correlationId": "5889315c-c4ac-4807-99da-e17417eae786",
"createdDateTime": "2023-07-26 22:58:30.983201900",
"crossTenantAccessType": "none",
"flaggedForReview": false,
"id": "36658c78-02d9-4d8f-84ee-5ca4a3fdefef",
"incomingTokenType": "none",
"ipAddress": "12.12.12.12",
"isInteractive": false,
"isTenantRestricted": false,
"location": {
"city": "Dublin",
"countryOrRegion": "IE",
"geoCoordinates": {
"latitude": 51.35555555555555,
"longitude": -5.244444444444444
},
"state": "Dublin"
},
"managedIdentityType": "none",
"processingTimeInMilliseconds": 0,
"resourceDisplayName": "Azure Storage",
"resourceId": "037694de-8c7d-498d-917d-edb650090fa5",
"resourceServicePrincipalId": "a225221f-8cc5-411a-9cc7-5e1394b8a5b8",
"riskDetail": "none",
"riskLevelAggregated": "low",
"riskLevelDuringSignIn": "low",
"riskState": "none",
"servicePrincipalId": "b1c34143-e405-4058-8e29-84596ad737b8",
"servicePrincipalName": "some-service-principal",
"status": {
"errorCode": 7000215
},
"tokenIssuerType": "AzureAD",
"uniqueTokenIdentifier": "NDDDDDDDDDDDDDDDDDD_DD"
},
"resourceid": "/tenants/c0dd2fa0-71be-4df8-b2a6-24cee7de069a/providers/Microsoft.aadiam",
"resultsignature": "None",
"resulttype": 7000215,
"tenantid": "a2aa49aa-2c0c-49d2-af87-f402c421df0b",
"time": "2023-07-26 23:00:20.889"
}
Azure MFA Disabled
#This detection looks for MFA being disabled in conditional access policy
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence | |
| Stealth | |
| Defense Impairment | |
| Credential Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | Update conditional access policy |
Rules detecting the same action
These rules filter on the same operation.
- Azure Policy Violation Detected (Panther)
- CA Policy Removed by Non Approved Actor (Sigma)
- CA Policy Updated by Non Approved Actor (Sigma)
- Certificate-Based Authentication Enabled (Sigma)
- Changes to Device Registration Policy (Sigma)
- Conditional Access - A Conditional Access app exclusion has changed (Kusto)
- Conditional Access - A Conditional Access Device platforms condition has changed (the Device platforms condition can be spoofed) (Kusto)
- Conditional Access - A Conditional Access policy was deleted (Kusto)
Detection logic
import json
from panther_base_helpers import deep_walk
from panther_msft_helpers import azure_rule_context
def get_mfa(policy):
parse_one = json.loads(policy)
mfa_get = deep_walk(parse_one, "grantControls", "builtInControls", default=[])
mfa_standardized = [n.lower() for n in mfa_get]
return mfa_standardized
def rule(event):
if event.get("operationName", default="") != "Update conditional access policy":
return False
old_value = event.deep_walk(
"properties",
"targetResources",
"modifiedProperties",
"oldValue",
return_val="first",
default="",
)
new_value = event.deep_walk(
"properties",
"targetResources",
"modifiedProperties",
"newValue",
return_val="first",
default="",
)
old_value_parsed = get_mfa(old_value)
new_value_parsed = get_mfa(new_value)
return "mfa" in old_value_parsed and "mfa" not in new_value_parsed
def title(event):
actor_name = event.deep_get(
"properties", "initiatedBy", "user", "userPrincipalName", default="<UNKNOWN ACTOR>"
)
policy = event.deep_walk("properties", "targetResources", "displayName", default="")
return f"MFA disabled by {actor_name} on the policy {policy}"
def alert_context(event):
return azure_rule_context(event)
Rule specification
AnalysisType: rule
Filename: azure_mfa_disabled.py
RuleID: "Azure.Audit.MFADisabled"
DisplayName: "Azure MFA Disabled"
Enabled: true
LogTypes:
- Azure.Audit
Severity: High
Description: >
This detection looks for MFA being disabled in conditional access policy
Reports:
MITRE ATT&CK:
- TA0005:T1556
- TA0001:T1078
Runbook: >
Verify if the change was authorized and investigate the user activity. If unauthorized, re-enable MFA, revoke access.
Reference: https://learn.microsoft.com/en-us/entra/identity/authentication/overview-authentication
SummaryAttributes:
- properties:ServicePrincipalName
- properties:UserPrincipalName
- properties:ipAddress
Stages and Predicates
Fires on Azure.Audit events when the condition below holds.
Condition
operationNameisUpdate conditional access policy
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"Update conditional access policy" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
operationName | |
category | properties.category |
actor_id | properties.initiatedBy.user.id |
actor_upn | properties.initiatedBy.user.userPrincipalName |
source_ip_address | properties.initiatedBy.user.ipAddress |
target_id | properties.targetResources.id |
target_name | properties.targetResources.displayName |
Response runbook
Verify if the change was authorized and investigate the user activity. If unauthorized, re-enable MFA, revoke access.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"Level": "4",
"callerIpAddress": "1.2.3.4",
"category": "AuditLogs",
"correlationId": "123456789",
"durationMs": 0,
"operationName": "Update conditional access policy",
"operationVersion": "1.0",
"properties": {
"activityDateTime": "2024-11-27T03:31:26.7088498+00:00",
"activityDisplayName": "Update conditional access policy",
"additionalDetails": [
{
"key": "Category",
"value": "Conditional Access"
}
],
"category": "Policy",
"correlationId": "123456789",
"id": "IPCGraph_123456789",
"identity": "",
"initiatedBy": {
"user": {
"displayName": null,
"id": "123456789b",
"ipAddress": "1.2.3.4",
"roles": [],
"userPrincipalName": "denethor@lotr.com"
}
},
"loggedByService": "Conditional Access",
"operationName": "Update conditional access policy",
"operationType": "Update",
"result": "success",
"resultDescription": "",
"resultReason": null,
"resultType": "",
"targetResources": [
{
"administrativeUnits": [],
"displayName": "MFA",
"id": "123456789",
"modifiedProperties": [
{
"displayName": "ConditionalAccessPolicy",
"newValue": "{\"id\":\"123456789\",\"displayName\":\"MFA\",\"createdDateTime\":\"2024-11-21T16:48:48.1196443+00:00\",\"modifiedDateTime\":\"2024-11-27T03:31:25.4989035+00:00\",\"state\":\"enabled\",\"conditions\":{\"applications\":{\"includeApplications\":[\"None\"],\"excludeApplications\":[],\"includeUserActions\":[],\"includeAuthenticationContextClassReferences\":[],\"applicationFilter\":null},\"users\":{\"includeUsers\":[\"All\"],\"excludeUsers\":[],\"includeGroups\":[],\"excludeGroups\":[],\"includeRoles\":[],\"excludeRoles\":[]},\"userRiskLevels\":[],\"signInRiskLevels\":[],\"clientAppTypes\":[\"all\"],\"servicePrincipalRiskLevels\":[]},\"sessionControls\":{\"signInFrequency\":{\"value\":90,\"type\":\"days\",\"authenticationType\":\"primaryAndSecondaryAuthentication\",\"frequencyInterval\":\"timeBased\",\"isEnabled\":true}}}",
"oldValue": "{\"id\":\"123456789\",\"displayName\":\"MFA\",\"createdDateTime\":\"2024-11-21T16:48:48.1196443+00:00\",\"modifiedDateTime\":\"2024-11-21T16:56:13.9120766+00:00\",\"state\":\"enabled\",\"conditions\":{\"applications\":{\"includeApplications\":[\"None\"],\"excludeApplications\":[],\"includeUserActions\":[],\"includeAuthenticationContextClassReferences\":[],\"applicationFilter\":null},\"users\":{\"includeUsers\":[\"All\"],\"excludeUsers\":[],\"includeGroups\":[],\"excludeGroups\":[],\"includeRoles\":[],\"excludeRoles\":[]},\"userRiskLevels\":[],\"signInRiskLevels\":[],\"clientAppTypes\":[\"all\"],\"servicePrincipalRiskLevels\":[]},\"grantControls\":{\"operator\":\"OR\",\"builtInControls\":[\"MFA\"],\"customAuthenticationFactors\":[],\"termsOfUse\":[]},\"sessionControls\":{\"signInFrequency\":{\"value\":90,\"type\":\"days\",\"authenticationType\":\"primaryAndSecondaryAuthentication\",\"frequencyInterval\":\"timeBased\",\"isEnabled\":true}}}"
}
],
"type": "Policy"
}
],
"tenantGeo": "NA",
"tenantId": "123456789",
"userAgent": null
},
"resourceId": "/tenants/123456789/providers/Microsoft.aadiam",
"resultSignature": "None",
"tenantId": "123456789",
"time": "2024-11-27T03:31:26.7088498Z"
}
Azure Microsoft Graph Single Session from Multiple IP Addresses
#Detects when a user signs in to Microsoft Entra ID and subsequently accesses Microsoft Graph from a different IP address using the same session ID. This behavior may indicate OAuth application abuse, session hijacking, token replay attacks, or adversary-in-the-middle attacks where an attacker has obtained a valid session token and is using it from their own infrastructure.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Stealth | |
| Lateral Movement |
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
Detection logic
from panther_azuresignin_helpers import azure_signin_alert_context, azure_signin_success
# Whitelisted application IDs (common Microsoft services that may legitimately use multiple IPs)
WHITELISTED_APP_IDS = {
# Office 365 / Microsoft 365
"00000003-0000-0ff1-ce00-000000000000", # Office 365
"00000006-0000-0ff1-ce00-000000000000", # Office 365 Exchange Online
"00b41c95-dab0-4487-9791-b9d2c32c80f2", # Office 365 Management APIs
"d3590ed6-52b3-4102-aeff-aad2292ab01c", # Microsoft Office
# Microsoft Teams
"1fec8e78-bce4-4aaf-ab1b-5451cc387264", # Microsoft Teams
"5e3ce6c0-2b1f-4285-8d4b-75ee78787346", # Microsoft Teams Services
"cc15fd57-2c6c-4117-a88c-83b1d56b4bbe", # Microsoft Teams - Device Profile Service
# OneDrive
"ab9b8c07-8f02-4f72-87fa-80105867a763", # OneDrive SyncEngine
# Microsoft Edge
"ea5a67f6-b6f3-4338-b240-c655ddc3cc8e", # Microsoft Edge Insider Addons
"ecd6b820-32c2-49b6-98a6-444530e5a77a", # Edge Remote Settings
"f44b1140-bc5e-48c6-8dc0-5cf5a53c0e34", # Microsoft Edge
# Azure Infrastructure Services
"797f4846-ba00-4fd7-ba43-dac1f8f63013", # Azure Resource Manager
"8edd93e1-2103-40b4-bd70-6e34e586362d", # Windows Azure Security Resource Provider
"c44b4083-3bb0-49c1-b47d-974e53cbdf3c", # Azure Portal
"04b07795-8ddb-461a-bbee-02f9e1bf7b46", # Microsoft Azure CLI
"4962773b-9cdb-44cf-a8bf-237846a00ab7", # Microsoft.EventGrid
# Security & Threat Protection Services
"fc780465-2017-40d4-a0c5-307022471b92", # WindowsDefenderATP
"8ee8fdad-f234-4243-8f3b-15c294843740", # Microsoft Threat Protection
"2793995e-0a7d-40d7-bd35-6968ba142197", # Microsoft Defender for Cloud
"3f6aecb4-6dbf-4e45-9141-440abdced562", # PROD Microsoft Defender For Cloud XDR
"7b7531ad-5926-4f2d-8a1d-38495ad33e17", # Azure Advanced Threat Protection
"df77edef-903d-416b-bcc0-cc8b91af54ea", # Defender for IoT
"8b3391f4-af01-4ee8-b4ea-9871b2499735", # O365 Secure Score
# Microsoft Graph & Identity Services
"f8f7a2aa-e116-4ba6-8aea-ca162cfa310d", # Microsoft Graph Connectors Core
"01fc33a7-78ba-4d2f-a4b7-768e336e890e", # MS-PIM (Privileged Identity Management)
"bd11ca0f-4fd6-4bb7-a259-4a36693b6e13", # MCAPI AAD Bridge Service
"abc63b55-0325-4305-9e1e-3463b182a6dc", # TenantSearchProcessors
"eace8149-b661-472f-b40d-939f89085bd4", # Substrate Instant Revocation Pipeline
"b46c3ac5-9da6-418f-a849-0a07a10b3c6c", # Cloud Infrastructure Entitlement Management
"0469d4cd-df37-4d93-8a61-f8c75b809164", # Policy Administration Service
# Other Microsoft Services
"18fbca16-2224-45f6-85b0-f7bf2b39b3f3", # Microsoft Docs
"98db8bd6-0cc0-4e67-9de5-f187f1cd1b41", # Microsoft Substrate Management
# Panther Integrations (distributed infrastructure)
"6821c7a6-ae62-49ba-8669-3f2e72d8d803", # GL - Panther Graph Integration
# Microsoft Account Services
"7eadcef8-456d-4611-9480-4fff72b8b9e2", # Microsoft Account Controls V2
}
def rule(event):
resource_display_name = event.deep_get("properties", "resourceDisplayName", default="")
session_id = event.deep_get("properties", "sessionId", default="")
user_principal_name = event.deep_get("properties", "userPrincipalName", default="")
source_ip = event.deep_get("properties", "ipAddress", default="")
app_id = event.deep_get("properties", "appId", default="")
# Skip if not Microsoft Graph access or whitelisted applications
if "Microsoft Graph" not in resource_display_name or app_id in WHITELISTED_APP_IDS:
return False
is_interactive = event.deep_get("properties", "isInteractive", default=True)
category = event.get("category", "")
if not is_interactive or category == "NonInteractiveUserSignInLogs":
return False
# Skip if essential fields are missing or failed attempts
if (
not session_id
or not user_principal_name
or not source_ip
or not azure_signin_success(event)
):
return False
return True
def unique(event):
return event.deep_get("properties", "ipAddress", default="")
def dedup(event):
session_id = event.deep_get("properties", "sessionId", default="")
user_principal_name = event.deep_get("properties", "userPrincipalName", default="")
return f"{session_id}-{user_principal_name}"
def title(event):
user_principal_name = event.deep_get(
"properties", "userPrincipalName", default="<UNKNOWN_USER>"
)
session_id = event.deep_get("properties", "sessionId", default="<UNKNOWN_SESSION>")
return (
f"Microsoft Graph Session Access from Multiple IPs: "
f"User [{user_principal_name}] in "
f"session [{session_id}]"
)
def alert_context(event):
context = azure_signin_alert_context(event)
# Add Graph access specific context
context["session_id"] = event.deep_get("properties", "sessionId", default="<NO_SESSION>")
context["app_display_name"] = event.deep_get("properties", "appDisplayName", default="<NO_APP>")
context["app_id"] = event.deep_get("properties", "appId", default="<NO_APP_ID>")
context["resource_display_name"] = event.deep_get(
"properties", "resourceDisplayName", default="<NO_RESOURCE>"
)
context["authentication_protocol"] = event.deep_get(
"properties", "authenticationProtocol", default="<NO_PROTOCOL>"
)
context["conditional_access_status"] = event.deep_get(
"properties", "conditionalAccessStatus", default="<NO_CA_STATUS>"
)
context["is_interactive"] = event.deep_get("properties", "isInteractive", default=None)
context["user_agent"] = event.deep_get("properties", "userAgent", default="<NO_USER_AGENT>")
return context
Rule specification
AnalysisType: rule
Filename: azure_graph_session_multiple_ips.py
RuleID: "Azure.Audit.GraphSessionMultipleIPs"
DisplayName: "Azure Microsoft Graph Single Session from Multiple IP Addresses"
Enabled: true
Status: Experimental
LogTypes:
- Azure.Audit
Severity: Medium
Threshold: 2
DedupPeriodMinutes: 60
Description: >
Detects when a user signs in to Microsoft Entra ID and subsequently accesses Microsoft Graph from
a different IP address using the same session ID. This behavior may indicate OAuth application abuse,
session hijacking, token replay attacks, or adversary-in-the-middle attacks where an attacker has
obtained a valid session token and is using it from their own infrastructure.
Tags:
- Initial Access
- Valid Accounts
- Use Alternate Authentication Material
Reports:
MITRE ATT&CK:
- TA0001:T1078
- TA0001:T1078.004
- TA0005:T1550
- TA0005:T1550.001
Runbook: |
1. Query Azure.Audit logs for all Microsoft Graph access events with properties:sessionId in the 4 hours around this alert to identify all source IP addresses, user agents, and accessed resources
2. Compare the geographic locations and ASN information for all distinct callerIpAddress values to determine if they represent different countries, cloud providers, or inconsistent network types
3. Review Azure.Audit logs for properties:userPrincipalName in the 7 days before this session to identify OAuth consent grants, new application registrations, risky sign-ins, or token issuance events that may indicate initial compromise
Reference: https://github.com/elastic/detection-rules/blob/main/rules/integrations/azure/initial_access_entra_id_graph_single_session_from_multiple_addresses.toml
SummaryAttributes:
- properties:userPrincipalName
- callerIpAddress
- properties:sessionId
- properties:appDisplayName
Stages and Predicates
Fires on Azure.Audit events when all of the conditions below hold.
Condition
properties.resourceDisplayNamecontainsMicrosoft Graphproperties.appIdis not one of00000003-0000-0ff1-ce00-000000000000,00000006-0000-0ff1-ce00-000000000000,00b41c95-dab0-4487-9791-b9d2c32c80f2,d3590ed6-52b3-4102-aeff-aad2292ab01c,1fec8e78-bce4-4aaf-ab1b-5451cc387264properties.isInteractiveis presentcategoryis notNonInteractiveUserSignInLogsproperties.sessionIdis presentproperties.userPrincipalNameis presentproperties.ipAddressis presentresultSignatureisSUCCESS
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
properties.resourceDisplayName | contains | Microsoft Graph | excludes:properties.resourceDisplayName field:"properties.resourceDisplayName" value:"Microsoft Graph" |
properties.appId | in | 00000003-0000-0ff1-ce00-000000000000, 00000006-0000-0ff1-ce00-000000000000, 00b41c95-dab0-4487-9791-b9d2c32c80f2, 01fc33a7-78ba-4d2f-a4b7-768e336e890e, 0469d4cd-df37-4d93-8a61-f8c75b809164, 04b07795-8ddb-461a-bbee-02f9e1bf7b46, 18fbca16-2224-45f6-85b0-f7bf2b39b3f3, 1fec8e78-bce4-4aaf-ab1b-5451cc387264, 2793995e-0a7d-40d7-bd35-6968ba142197, 3f6aecb4-6dbf-4e45-9141-440abdced562, 4962773b-9cdb-44cf-a8bf-237846a00ab7, 5e3ce6c0-2b1f-4285-8d4b-75ee78787346, 6821c7a6-ae62-49ba-8669-3f2e72d8d803, 797f4846-ba00-4fd7-ba43-dac1f8f63013, 7b7531ad-5926-4f2d-8a1d-38495ad33e17, 7eadcef8-456d-4611-9480-4fff72b8b9e2, 8b3391f4-af01-4ee8-b4ea-9871b2499735, 8edd93e1-2103-40b4-bd70-6e34e586362d, 8ee8fdad-f234-4243-8f3b-15c294843740, 98db8bd6-0cc0-4e67-9de5-f187f1cd1b41, ab9b8c07-8f02-4f72-87fa-80105867a763, abc63b55-0325-4305-9e1e-3463b182a6dc, b46c3ac5-9da6-418f-a849-0a07a10b3c6c, bd11ca0f-4fd6-4bb7-a259-4a36693b6e13, c44b4083-3bb0-49c1-b47d-974e53cbdf3c, cc15fd57-2c6c-4117-a88c-83b1d56b4bbe, d3590ed6-52b3-4102-aeff-aad2292ab01c, df77edef-903d-416b-bcc0-cc8b91af54ea, ea5a67f6-b6f3-4338-b240-c655ddc3cc8e, eace8149-b661-472f-b40d-939f89085bd4, ecd6b820-32c2-49b6-98a6-444530e5a77a, f44b1140-bc5e-48c6-8dc0-5cf5a53c0e34, f8f7a2aa-e116-4ba6-8aea-ca162cfa310d, fc780465-2017-40d4-a0c5-307022471b92 | excludes:properties.appId |
category | eq | NonInteractiveUserSignInLogs | excludes:category field:"category" value:"NonInteractiveUserSignInLogs" |
properties.isInteractive | is_null | excludes:properties.isInteractive | |
properties.ipAddress | is_null | excludes:properties.ipAddress | |
properties.sessionId | is_null | excludes:properties.sessionId | |
properties.userPrincipalName | is_null | excludes:properties.userPrincipalName | |
resultSignature | ne | SUCCESS | excludes:resultSignature field:"resultSignature" value:"SUCCESS" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
properties.resourceDisplayName | contains |
| field:"properties.resourceDisplayName" kind:contains value:"Microsoft Graph" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
userPrincipalName | properties.userPrincipalName |
sessionId | properties.sessionId |
Response runbook
1. Query Azure.Audit logs for all Microsoft Graph access events with properties:sessionId in the 4 hours around this alert to identify all source IP addresses, user agents, and accessed resources
2. Compare the geographic locations and ASN information for all distinct callerIpAddress values to determine if they represent different countries, cloud providers, or inconsistent network types
3. Review Azure.Audit logs for properties:userPrincipalName in the 7 days before this session to identify OAuth consent grants, new application registrations, risky sign-ins, or token issuance events that may indicate initial compromise
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"Level": "4",
"callerIpAddress": "5.5.5.5",
"category": "SignInLogs",
"correlationId": "graph-session-002",
"durationMs": 0,
"operationName": "Sign-in activity",
"operationVersion": "1.0",
"p_event_time": "2025-01-15 09:35:20.456",
"p_log_type": "Azure.Audit",
"p_parse_time": "2025-01-15 09:35:21.000",
"p_row_id": "row-456",
"properties": {
"appDisplayName": "Custom Graph App",
"appId": "app-custom-789",
"authenticationProtocol": "oAuth2",
"conditionalAccessStatus": "success",
"createdDateTime": "2025-01-15T09:35:20.4567890Z",
"ipAddress": "5.5.5.5",
"isInteractive": true,
"resourceDisplayName": "Microsoft Graph",
"resourceId": "00000003-0000-0000-c000-111111111111",
"sessionId": "session-abc-123",
"userAgent": "python-requests/2.28.1",
"userId": "user-123",
"userPrincipalName": "gandalf@lotr.com"
},
"resourceId": "/tenants/tenant-123/providers/Microsoft.aadiam",
"resultSignature": "SUCCESS",
"resultType": "Success",
"tenantId": "tenant-123",
"time": "2025-01-15 09:35:20.456"
}
Azure Network Packet Capture Enabled
#Detects when Azure's Network Watcher packet capture feature is activated. Packet capture operations could enable threat actors to inspect unencrypted network traffic and potentially extract sensitive credentials or data. While packet capture is a legitimate network diagnostics tool, adversaries may abuse it to sniff credentials or intercept sensitive data in transit.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Detection logic
from panther_azureactivity_helpers import azure_activity_alert_context, azure_activity_success
PACKET_CAPTURE_OPERATIONS = [
"MICROSOFT.NETWORK/NETWORKWATCHERS/STARTPACKETCAPTURE/ACTION",
"MICROSOFT.NETWORK/NETWORKWATCHERS/VPNCONNECTIONS/STARTPACKETCAPTURE/ACTION",
"MICROSOFT.NETWORK/NETWORKWATCHERS/PACKETCAPTURES/WRITE",
]
def rule(event):
operation_name = event.get("operationName", "").upper()
return any(
pattern in operation_name for pattern in PACKET_CAPTURE_OPERATIONS
) and azure_activity_success(event)
def title(event):
location = event.get("location", "<UNKNOWN_LOCATION>")
return f"Azure Network Packet Capture Enabled in [{location}]"
def alert_context(event):
return azure_activity_alert_context(event)
Rule specification
AnalysisType: rule
Filename: azure_network_packet_capture_enabled.py
RuleID: "Azure.MonitorActivity.Network.PacketCaptureEnabled"
DisplayName: "Azure Network Packet Capture Enabled"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Medium
Description: >
Detects when Azure's Network Watcher packet capture feature is activated.
Packet capture operations could enable threat actors to inspect unencrypted
network traffic and potentially extract sensitive credentials or data. While packet capture
is a legitimate network diagnostics tool, adversaries may abuse it to sniff credentials or
intercept sensitive data in transit.
Reports:
MITRE ATT&CK:
- TA0006:T1040 # Credential Access: Network Sniffing
Tags:
- Credential Access
- Network Sniffing
Runbook: |
1. Query Azure Monitor Activity logs for all network diagnostic operations (packet capture start/stop, network watcher operations) by the callerIpAddress in the 24 hours before and after the alert
2. Find all packet capture sessions started in the past 6 hours to identify the scope of potential credential sniffing activity
3. Check if the callerIpAddress has enabled packet capture in the past 90 days to determine if this is typical network troubleshooting behavior
Reference: https://github.com/elastic/detection-rules/blob/main/rules/integrations/azure/credential_access_network_full_network_packet_capture_detected.toml
SummaryAttributes:
- resourceId
- location
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
any of:
operationNamecontainsMICROSOFT.NETWORK/NETWORKWATCHERS/STARTPACKETCAPTURE/ACTIONoperationNamecontainsMICROSOFT.NETWORK/NETWORKWATCHERS/VPNCONNECTIONS/STARTPACKETCAPTURE/ACTIONoperationNamecontainsMICROSOFT.NETWORK/NETWORKWATCHERS/PACKETCAPTURES/WRITE
resultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | contains |
| field:"operationName" kind:contains |
resultType | in |
| field:"resultType" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
location |
Response runbook
1. Query Azure Monitor Activity logs for all network diagnostic operations (packet capture start/stop, network watcher operations) by the callerIpAddress in the 24 hours before and after the alert
2. Find all packet capture sessions started in the past 6 hours to identify the scope of potential credential sniffing activity
3. Check if the callerIpAddress has enabled packet capture in the past 90 days to determine if this is typical network troubleshooting behavior
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Informational",
"location": "eastus",
"operationName": "MICROSOFT.NETWORK/NETWORKWATCHERS/STARTPACKETCAPTURE/ACTION",
"operationVersion": "2021-05-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/NetworkWatcherRG/providers/Microsoft.Network/networkWatchers/NetworkWatcher_eastus",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-22T10:30:00.0000000Z"
}
Azure Network Security Configuration Modified or Deleted
#Identifies when a network security configuration is modified or deleted. This includes Network Security Group (NSG) changes, security rule modifications, NSG joins to subnets/interfaces, and diagnostic settings changes. These actions may indicate defense evasion, persistence, or preparation for data exfiltration.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment | |
| Impact |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import azure_activity_alert_context, azure_activity_success
NSG_OPERATIONS = [
"MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/WRITE",
"MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/DELETE",
"MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/SECURITYRULES/WRITE",
"MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/SECURITYRULES/DELETE",
"MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/JOIN/ACTION",
"MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/PROVIDERS/MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/WRITE",
]
def rule(event):
return event.get("operationName", "").upper() in NSG_OPERATIONS and azure_activity_success(
event
)
def title(event):
operation = event.get("operationName", "").upper()
# Determine action description based on operation
if "DELETE" in operation:
action = "deleted"
elif "WRITE" in operation:
action = "modified"
elif "JOIN" in operation:
action = "joined"
else:
action = "configuration changed for"
# Determine resource type
if "SECURITYRULES" in operation:
resource_type = "Network Security Rule"
elif "DIAGNOSTICSETTINGS" in operation:
resource_type = "NSG Diagnostic Settings"
else:
resource_type = "Network Security Group"
return f"Azure {resource_type} {action}"
def alert_context(event):
return azure_activity_alert_context(event)
Rule specification
AnalysisType: rule
Filename: azure_nsg_deleted_or_modified.py
RuleID: "Azure.MonitorActivity.Network.NSGModifiedOrDeleted"
DisplayName: "Azure Network Security Configuration Modified or Deleted"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Medium
Status: Experimental
Description: >
Identifies when a network security configuration is modified or deleted.
This includes Network Security Group (NSG) changes, security rule modifications, NSG joins to subnets/interfaces,
and diagnostic settings changes. These actions may indicate defense evasion, persistence, or preparation for data exfiltration.
Reports:
MITRE ATT&CK:
- TA0005:T1562.007 # Defense Evasion: Impair Defenses - Disable or Modify Cloud Firewall
- TA0040:T1485 # Impact: Data Destruction
Tags:
- AZT506
- Defense Evasion
- Impact
- Impair Defenses
- Disable or Modify Cloud Firewall
- Data Destruction
Runbook: |
1. Find all network security group and firewall operations by the callerIpAddress in the 24 hours before and after this alert to identify if multiple security controls are being modified
2. Query for all network configuration changes from the same caller in the past 7 days to determine if this is part of a broader defense evasion pattern
3. Check if the callerIpAddress is associated with known VPNs or corporate network ranges used by authorized network administrators
Reference: https://attack.mitre.org/techniques/T1562/007/
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameis one ofMICROSOFT.NETWORK/NETWORKSECURITYGROUPS/WRITE,MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/DELETE,MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/SECURITYRULES/WRITE,MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/SECURITYRULES/DELETE,MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/JOIN/ACTIONresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | in |
| field:"operationName" kind:in |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Find all network security group and firewall operations by the callerIpAddress in the 24 hours before and after this alert to identify if multiple security controls are being modified
2. Query for all network configuration changes from the same caller in the past 7 days to determine if this is part of a broader defense evasion pattern
3. Check if the callerIpAddress is associated with known VPNs or corporate network ranges used by authorized network administrators
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"location": "",
"operationName": "Microsoft.Network/networkSecurityGroups/delete",
"operationVersion": "2021-04-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.Network/networkSecurityGroups/mynsg",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Network Watcher Deleted
#Detects when an Azure Network Watcher is deleted. Network Watcher is a regional service that enables monitoring and diagnostics for network resources in Azure, including packet capture, connection monitoring, flow logging, and network performance diagnostics. Adversaries may delete Network Watchers to disable network visibility and evade detection during lateral movement, data exfiltration, or other network-based attacks.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | Activity Log event Microsoft.Network/networkWatchers/delete: Deletes a network watcher |
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
NETWORK_WATCHER_DELETE_OPERATION = "MICROSOFT.NETWORK/NETWORKWATCHERS/DELETE"
def rule(event):
return all(
[
event.get("operationName", "").upper() == NETWORK_WATCHER_DELETE_OPERATION,
azure_activity_success(event),
]
)
def title(event):
resource_id = event.get("resourceId", "")
network_watcher = extract_resource_name_from_id(
resource_id, "networkWatchers", default="<UNKNOWN_WATCHER>"
)
return f"Azure Network Watcher [{network_watcher}] Deleted"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
network_watcher_name = extract_resource_name_from_id(resource_id, "networkWatchers", default="")
if network_watcher_name:
context["network_watcher_name"] = network_watcher_name
resource_group = extract_resource_name_from_id(resource_id, "resourceGroups", default="")
if resource_group:
context["resource_group"] = resource_group
return context
Rule specification
AnalysisType: rule
Filename: azure_network_watcher_deleted.py
RuleID: "Azure.MonitorActivity.Network.WatcherDeleted"
DisplayName: "Azure Network Watcher Deleted"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Medium
Description: >
Detects when an Azure Network Watcher is deleted. Network Watcher is a regional service that
enables monitoring and diagnostics for network resources in Azure, including packet capture,
connection monitoring, flow logging, and network performance diagnostics. Adversaries may
delete Network Watchers to disable network visibility and evade detection during lateral
movement, data exfiltration, or other network-based attacks.
Reports:
MITRE ATT&CK:
- TA0005:T1562.001 # Defense Evasion: Impair Defenses - Disable or Modify Tools
Tags:
- Defense Evasion
- Impair Defenses
- Disable or Modify Tools
Runbook: |
1. Query Azure Monitor Activity logs for all network monitoring operations (network watcher deletions, NSG flow log deletions, packet capture operations) by the callerIpAddress in the 24 hours before and after the alert
2. Find all network watcher deletions and NSG flow log deletions in the past 6 hours to determine if this is part of a coordinated attack on network visibility
3. Check if the callerIpAddress has deleted network monitoring resources in the past 90 days to establish if this is typical infrastructure maintenance
Reference: https://learn.microsoft.com/en-us/azure/network-watcher/network-watcher-overview
SummaryAttributes:
- resourceId
- location
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.NETWORK/NETWORKWATCHERS/DELETEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.NETWORK/NETWORKWATCHERS/DELETE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all network monitoring operations (network watcher deletions, NSG flow log deletions, packet capture operations) by the callerIpAddress in the 24 hours before and after the alert
2. Find all network watcher deletions and NSG flow log deletions in the past 6 hours to determine if this is part of a coordinated attack on network visibility
3. Check if the callerIpAddress has deleted network monitoring resources in the past 90 days to establish if this is typical infrastructure maintenance
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Informational",
"location": "eastus",
"operationName": "MICROSOFT.NETWORK/NETWORKWATCHERS/DELETE",
"operationVersion": "2021-05-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/NetworkWatcherRG/providers/Microsoft.Network/networkWatchers/NetworkWatcher_eastus",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-22T10:30:00.0000000Z"
}
Azure Policy Changed
#This detection looks for policy changes in AuditLogs
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Discovery |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Detection logic
from panther_msft_helpers import azure_rule_context, azure_success
POLICY_OPERATION = "policy"
IGNORE_ACTIONS = ["Add", "Added"]
def rule(event):
operation = event.get("operationName", default="")
if not azure_success(event) or not operation.endswith(POLICY_OPERATION):
return False
# Ignore added policies
if any((operation.startswith(ignore) for ignore in IGNORE_ACTIONS)):
return False
return True
def title(event):
operation_name = event.get("operationName", default="<UNKNOWN OPERATION>")
actor_name = event.deep_get(
"properties", "initiatedBy", "user", "userPrincipalName", default="<UNKNOWN ACTOR>"
)
policy = event.deep_walk(
"properties", "targetResources", "displayName", default="<UNKNOWN POLICY>"
)
return f"{operation_name} by {actor_name} on the policy {policy}"
def alert_context(event):
return azure_rule_context(event)
Rule specification
AnalysisType: rule
Filename: azure_policy_changed.py
RuleID: "Azure.Audit.PolicyChanged"
DisplayName: "Azure Policy Changed"
Enabled: true
LogTypes:
- Azure.Audit
Severity: Low
DedupPeriodMinutes: 10
Description: >
This detection looks for policy changes in AuditLogs
Reports:
MITRE ATT&CK:
- TA0005:T1526
Runbook: >
Verify if the change was authorized and review the modifications. If unauthorized, revert the policy, notify relevant teams, and investigate the user actions.
Reference: https://learn.microsoft.com/en-us/entra/identity/authentication/overview-authentication
SummaryAttributes:
- properties:ServicePrincipalName
- properties:UserPrincipalName
- properties:initiatedBy:user:ipAddress
Stages and Predicates
Fires on Azure.Audit events when all of the conditions below hold.
Condition
properties.resultissuccessoperationNameends withpolicyoperationNamedoes not start withAddoperationNamedoes not start withAdded
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
operationName | ends_with | policy | excludes:operationName field:"operationName" value:"policy" |
properties.result | ne | success | excludes:properties.result field:"properties.result" value:"success" |
operationName | starts_with | Add | excludes:operationName field:"operationName" value:"Add" |
operationName | starts_with | Added | excludes:operationName field:"operationName" value:"Added" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | ends_with |
| field:"operationName" kind:ends_with value:"policy" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
operationName | |
category | properties.category |
actor_id | properties.initiatedBy.user.id |
actor_upn | properties.initiatedBy.user.userPrincipalName |
source_ip_address | properties.initiatedBy.user.ipAddress |
target_id | properties.targetResources.id |
target_name | properties.targetResources.displayName |
Response runbook
Verify if the change was authorized and review the modifications. If unauthorized, revert the policy, notify relevant teams, and investigate the user actions.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"Level": "4",
"callerIpAddress": "1.2.3.4",
"category": "AuditLogs",
"correlationId": "1324515",
"durationMs": 0,
"operationName": "Delete conditional access policy",
"operationVersion": "1.0",
"properties": {
"activityDateTime": "2024-11-27T02:22:58.727028+00:00",
"activityDisplayName": "Delete conditional access policy",
"additionalDetails": [
{
"key": "Category",
"value": "Conditional Access"
}
],
"category": "Policy",
"correlationId": "23456234",
"id": "IPCGraph_af23466234",
"identity": "",
"initiatedBy": {
"user": {
"displayName": null,
"id": "234526234",
"ipAddress": "1.2.3.4",
"roles": [],
"userPrincipalName": "aragorn@lotr.com"
}
},
"loggedByService": "Conditional Access",
"operationName": "Delete conditional access policy",
"operationType": "Delete",
"result": "success",
"resultDescription": "",
"resultReason": null,
"resultType": "",
"targetResources": [
{
"administrativeUnits": [],
"displayName": "Outside MFA",
"id": "5e3cb481-2814-4295-b4cc-2440a6d66c86",
"modifiedProperties": [
{
"displayName": "ConditionalAccessPolicy",
"newValue": null,
"oldValue": "{\"id\":\"5e3cb481-2814-4295-b4cc-2440a6d66c86\",\"displayName\":\"Outside MFA\",\"createdDateTime\":\"2024-11-27T02:22:19.8926587+00:00\",\"state\":\"enabled\",\"conditions\":{\"applications\":{\"includeApplications\":[\"None\"],\"excludeApplications\":[],\"includeUserActions\":[],\"includeAuthenticationContextClassReferences\":[],\"applicationFilter\":null},\"users\":{\"includeUsers\":[],\"excludeUsers\":[],\"includeGroups\":[],\"excludeGroups\":[],\"includeRoles\":[],\"excludeRoles\":[],\"includeGuestsOrExternalUsers\":{\"guestOrExternalUserTypes\":63,\"externalTenants\":{}}},\"userRiskLevels\":[],\"signInRiskLevels\":[],\"clientAppTypes\":[\"all\"],\"servicePrincipalRiskLevels\":[]},\"grantControls\":{\"operator\":\"OR\",\"builtInControls\":[\"mfa\"],\"customAuthenticationFactors\":[],\"termsOfUse\":[]}}"
}
],
"type": "Policy"
}
],
"tenantGeo": "NA",
"tenantId": "132455112",
"userAgent": null
},
"resourceId": "/tenants/123145/providers/Microsoft.aadiam",
"resultSignature": "None",
"tenantId": "12341234",
"time": "2024-12-10T02:22:58.7270280Z"
}
Azure Policy DeployIfNotExists Action Triggered
#Detects when an Azure Policy with the DeployIfNotExists effect is triggered and executes a deployment. The DeployIfNotExists effect allows policies to automatically deploy resources when certain conditions are met. Adversaries may abuse this feature to establish persistence by creating policies that automatically deploy backdoors, malicious configurations, or unauthorized resources when specific conditions occur. This technique enables stealthy persistence as deployments appear to be legitimate policy enforcement actions.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Stealth |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
POLICY_OPERATIONS = [
"MICROSOFT.AUTHORIZATION/POLICIES/DEPLOYIFNOTEXISTS/ACTION",
]
def rule(event):
return event.get("operationName", "").upper() in POLICY_OPERATIONS and azure_activity_success(
event
)
def title(event):
resource_id = event.get("resourceId", "<UNKNOWN_RESOURCE>")
policy_name = extract_resource_name_from_id(
resource_id, "policyDefinitions", default="<UNKNOWN_POLICY>"
)
return f"Azure Policy DeployIfNotExists Triggered: [{policy_name}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
policy_name = extract_resource_name_from_id(resource_id, "policyDefinitions", default="")
if policy_name:
context["policy_name"] = policy_name
policy_assignment = extract_resource_name_from_id(resource_id, "policyAssignments", default="")
if policy_assignment:
context["policy_assignment"] = policy_assignment
return context
Rule specification
AnalysisType: rule
Filename: azure_policy_deployifnotexists.py
RuleID: "Azure.MonitorActivity.Policy.DeployIfNotExists"
DisplayName: "Azure Policy DeployIfNotExists Action Triggered"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Medium
Description: >
Detects when an Azure Policy with the DeployIfNotExists effect is triggered and executes a deployment.
The DeployIfNotExists effect allows policies to automatically deploy resources when certain conditions are met.
Adversaries may abuse this feature to establish persistence by creating policies that automatically deploy
backdoors, malicious configurations, or unauthorized resources when specific conditions occur. This technique
enables stealthy persistence as deployments appear to be legitimate policy enforcement actions.
Reports:
MITRE ATT&CK:
- TA0003:T1078.004 # Persistence: Valid Accounts - Cloud Accounts
- TA0005:T1564 # Defense Evasion: Hide Artifacts
Tags:
- AZT508
- Persistence
- Defense Evasion
- Valid Accounts
- Cloud Accounts
- Hide Artifacts
Runbook: |
1. Find all policy-related operations by the callerIpAddress in the 48 hours before and after this alert to identify if new policies were created or assignments were modified
2. Query for the policy definition details to understand what resources are being deployed and review the deployment template for malicious configurations
3. Check if similar DeployIfNotExists actions have been triggered from this policy in the past 30 days to determine if this is expected behavior
Reference: https://microsoft.github.io/Azure-Threat-Research-Matrix/Persistence/AZT508/AZT508
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameis one ofMICROSOFT.AUTHORIZATION/POLICIES/DEPLOYIFNOTEXISTS/ACTIONresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | in |
| field:"operationName" kind:in value:"MICROSOFT.AUTHORIZATION/POLICIES/DEPLOYIFNOTEXISTS/ACTION" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Find all policy-related operations by the callerIpAddress in the 48 hours before and after this alert to identify if new policies were created or assignments were modified
2. Query for the policy definition details to understand what resources are being deployed and review the deployment template for malicious configurations
3. Check if similar DeployIfNotExists actions have been triggered from this policy in the past 30 days to determine if this is expected behavior
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Informational",
"location": "eastus",
"operationName": "Microsoft.Authorization/policies/deployIfNotExists/action",
"operationVersion": "2021-06-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/policy-rg/providers/Microsoft.Authorization/policyDefinitions/AutoDeployBackdoor",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-23T10:30:00.0000000Z"
}
Azure Policy Violation Detected
#Detects when an Azure resource is found to be non-compliant with assigned Azure Policies. Policy violations indicate that resources do not meet organizational compliance requirements for security, networking, encryption, or other governance controls. Repeated violations may indicate configuration drift or potential security misconfigurations.
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- Azure MFA Disabled (Panther)
- CA Policy Removed by Non Approved Actor (Sigma)
- CA Policy Updated by Non Approved Actor (Sigma)
- Certificate-Based Authentication Enabled (Sigma)
- Changes to Device Registration Policy (Sigma)
- Conditional Access - A Conditional Access app exclusion has changed (Kusto)
- Conditional Access - A Conditional Access Device platforms condition has changed (the Device platforms condition can be spoofed) (Kusto)
- Conditional Access - A Conditional Access policy was deleted (Kusto)
Detection logic
from panther_azureactivity_helpers import azure_activity_alert_context, azure_parse_json_string
POLICY_AUDIT_OPERATIONS = [
"MICROSOFT.AUTHORIZATION/POLICIES/AUDIT/ACTION",
"MICROSOFT.AUTHORIZATION/POLICIES/AUDITIFNOTEXISTS/ACTION",
]
POLICY_CATEGORY = "Policy"
WARNING_LEVEL = "Warning"
def rule(event):
# Detects Azure Policy violations (audit failures) that indicate resources
# are not compliant with assigned Azure Policies.
return all(
[
event.get("operationName", "").upper() in POLICY_AUDIT_OPERATIONS,
event.get("category", "") == POLICY_CATEGORY,
event.get("level", "") == WARNING_LEVEL,
]
)
def title(event):
entity = event.deep_get("properties", "entity", default="<UNKNOWN_RESOURCE>")
# Extract first policy name if available
policies_json = event.deep_get("properties", "policies", default="[]")
policies = azure_parse_json_string(policies_json)
if policies and isinstance(policies, list) and len(policies) > 0:
policy_name = policies[0].get("policyDefinitionDisplayName", "<UNKNOWN_POLICY>")
else:
policy_name = "<UNKNOWN_POLICY>"
return f"Azure Policy Violation: [{policy_name}] on [{entity}]"
def alert_context(event):
context = azure_activity_alert_context(event)
# Add policy-specific context
context["entity"] = event.deep_get("properties", "entity", default=None)
context["message"] = event.deep_get("properties", "message", default=None)
context["is_compliance_check"] = event.deep_get("properties", "isComplianceCheck", default=None)
context["resource_location"] = event.deep_get("properties", "resourceLocation", default=None)
return context
Rule specification
AnalysisType: rule
Filename: azure_policy_violation.py
RuleID: "Azure.MonitorActivity.Policy.Violation"
DisplayName: "Azure Policy Violation Detected"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Info
Status: Experimental
Description: >
Detects when an Azure resource is found to be non-compliant with assigned Azure Policies.
Policy violations indicate that resources do not meet organizational compliance requirements
for security, networking, encryption, or other governance controls. Repeated violations
may indicate configuration drift or potential security misconfigurations.
Runbook: |
1. Query Azure Monitor Activity logs for all configuration changes to the resource ID in the 48 hours before the policy violation to identify what triggered the non-compliance
2. Find all policy violations for the same resource ID in the past 30 days to determine if this is an isolated incident or pattern of configuration drift
3. Check for other policy violations with the same policy definition name across all resources in the past 7 days to identify broader compliance issues
Reference: https://learn.microsoft.com/en-us/azure/governance/policy/overview
SummaryAttributes:
- properties.entity
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameis one ofMICROSOFT.AUTHORIZATION/POLICIES/AUDIT/ACTION,MICROSOFT.AUTHORIZATION/POLICIES/AUDITIFNOTEXISTS/ACTIONcategoryisPolicylevelisWarning
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
category | eq |
| field:"category" kind:eq value:"Policy" |
level | eq |
| field:"level" kind:eq value:"Warning" |
operationName | in |
| field:"operationName" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
entity | properties.entity |
Response runbook
1. Query Azure Monitor Activity logs for all configuration changes to the resource ID in the 48 hours before the policy violation to identify what triggered the non-compliance
2. Find all policy violations for the same resource ID in the past 30 days to determine if this is an isolated incident or pattern of configuration drift
3. Check for other policy violations with the same policy definition name across all resources in the past 7 days to identify broader compliance issues
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Policy",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Warning",
"location": "eastus",
"operationName": "MICROSOFT.AUTHORIZATION/POLICIES/AUDIT/ACTION",
"operationVersion": "2021-06-01",
"properties": {
"entity": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/storage-rg/providers/Microsoft.Storage/storageAccounts/mystorage",
"eventCategory": "Policy",
"isComplianceCheck": "False",
"message": "Microsoft.Authorization/policies/audit/action",
"policies": "[{\"policyDefinitionId\":\"/providers/Microsoft.Authorization/policyDefinitions/2a1a9cdf-e04d-429a-8416-3bfb72a1b26f\",\"policySetDefinitionId\":\"/providers/Microsoft.Authorization/policySetDefinitions/1f3afdf9-d0c9-4c3d-847f-89da613e70a8\",\"policyDefinitionReferenceId\":\"storageAccountsShouldRestrictNetworkAccessUsingVirtualNetworkRulesMonitoringEffect\",\"policySetDefinitionName\":\"1f3afdf9-d0c9-4c3d-847f-89da613e70a8\",\"policySetDefinitionDisplayName\":\"Microsoft cloud security benchmark\",\"policyDefinitionName\":\"2a1a9cdf-e04d-429a-8416-3bfb72a1b26f\",\"policyDefinitionDisplayName\":\"Storage accounts should restrict network access using virtual network rules\",\"policyDefinitionEffect\":\"Audit\",\"policyAssignmentId\":\"/subscriptions/12345678-1234-1234-1234-123456789abc/providers/Microsoft.Authorization/policyAssignments/SecurityCenterBuiltIn\",\"policyAssignmentName\":\"SecurityCenterBuiltIn\",\"policyAssignmentDisplayName\":\"ASC Default\"}]",
"resourceLocation": "westus2"
},
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/storage-rg/providers/Microsoft.Storage/storageAccounts/mystorage",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-22T10:30:00.0000000Z"
}
Azure Privileged or Elevated Role Assignment
#Detects when a privileged or elevated Azure role is assigned. Privileged roles include Owner, Contributor, User Access Administrator, Security Admin, and other high-impact administrative roles. Elevated roles include resource-specific roles with significant permissions like Storage Blob Data Owner, Key Vault Administrator, etc.
MITRE ATT&CK coverage
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
add_role_assignment_fields,
azure_activity_alert_context,
azure_activity_success,
azure_parse_json_string,
get_role_definition_id,
match_role_name,
)
ROLE_ASSIGNMENT_WRITE = "MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE"
ELEVATE_ACCESS_ACTION = "MICROSOFT.AUTHORIZATION/ELEVATEACCESS/ACTION"
# Common privileged role definition IDs (subscription-level built-in roles)
PRIVILEGED_ROLES = {
"8e3af657-a8ff-443c-a75c-2fe8c4bcb635": "Owner",
"b24988ac-6180-42a0-ab88-20f7382dd24c": "Contributor",
"18d7d88d-d35e-4fb5-a5c3-7773c20a72d9": "User Access Administrator",
"fb1c8493-542b-48eb-b624-b4c8fea62acd": "Security Admin",
"92b92042-07d9-4307-87f7-36a593fc5850": "Azure File Sync Administrator",
"a8889054-8d42-49c9-bc1c-52486c10e7cd": "Reservations Administrator",
"f58310d9-a9f6-439a-9e8d-f62e7b41a168": "Role Based Access Control Administrator",
"150f5e0c-0603-4f03-8c7f-cf70034c4e90": "Data Purger",
}
ELEVATED_ROLES = {
"ba92f5b4-2d11-453d-a403-e96b0029c9fe": "Storage Blob Data Contributor",
"b7e6dc6d-f1e8-4753-8033-0f276bb0955b": "Storage Blob Data Owner",
"dffb1e0c-446f-4dde-a09f-99eb5cc68b96": "Azure Arc Kubernetes Admin",
"a001fd3d-188f-4b5d-821b-7da978bf7442": "Cognitive Services OpenAI Contributor",
"00482a5a-887f-4fb3-b363-3b7fe8e74483": "Key Vault Administrator",
"8b54135c-b56d-4d72-a534-26097cfdc8d8": "Key Vault Data Access Administrator",
"4633458b-17de-408a-b874-0445c86b69e6": "Key Vault Secrets User",
}
# Combine all role mappings for lookup
ALL_ROLES = {**PRIVILEGED_ROLES, **ELEVATED_ROLES}
def extract_role_name(event):
# Extract and return the role name being assigned from the event
request_body = azure_parse_json_string(
event.deep_get("properties", "requestbody", default=None)
)
role_def_id = get_role_definition_id(request_body)
return match_role_name(role_def_id, ALL_ROLES)
def rule(event):
# For elevate access, operationName is a dict with "value" key
operation_name_value = event.deep_get("operationName", "value", default="")
if operation_name_value:
operation_name_value = str(operation_name_value).upper()
# Check if this is the elevate access action (subscription-level elevation)
if (
operation_name_value == ELEVATE_ACCESS_ACTION
and event.deep_get("status", "value") == "Succeeded"
):
return True
# For role assignments, operationName is a string
operation_name = event.get("operationName", "")
if isinstance(operation_name, str):
operation_name = operation_name.upper()
# Check if this is a privileged/elevated role assignment
if operation_name == ROLE_ASSIGNMENT_WRITE:
return extract_role_name(event) is not None and azure_activity_success(event)
return False
def title(event):
operation_name_value = event.deep_get(
"operationName", "value", default="<UNKNOWN_OP_VALUE>"
).upper()
# Handle elevate access action (subscription-level elevation)
if operation_name_value == ELEVATE_ACCESS_ACTION:
caller_identity = event.deep_get("identity", "claims", "name", default="<UNKNOWN_USER>")
return f"Azure subscription access elevated by [{caller_identity}]"
# Handle role assignment
role_assignment = event.deep_get("resourceId", default="<UNKNOWN_ASSIGNMENT>")
role_name = extract_role_name(event) or "<UNKNOWN_ROLE>"
role_str = "<UNKNOWN_ROLE_TYPE>"
if role_name in PRIVILEGED_ROLES.values():
role_str = "privileged"
if role_name in ELEVATED_ROLES.values():
role_str = "elevated"
return f"Azure [{role_str}] role " f"[{role_name}] assigned on " f"[{role_assignment}]"
def severity(event):
operation_name_value = event.deep_get(
"operationName", "value", default="<UNKNOWN_OP_VALUE>"
).upper()
# Elevate access action grants User Access Administrator at root scope
if operation_name_value == ELEVATE_ACCESS_ACTION:
return "HIGH"
# Get the role name being assigned
role_name = extract_role_name(event)
if role_name:
# Check if it's a privileged role
if role_name in PRIVILEGED_ROLES.values():
return "HIGH"
# Check if it's an elevated role
if role_name in ELEVATED_ROLES.values():
return "MEDIUM"
return "DEFAULT"
def alert_context(event):
context = azure_activity_alert_context(event)
# Parse and add request body fields
request_body = azure_parse_json_string(
event.deep_get("properties", "requestbody", default=None)
)
add_role_assignment_fields(context, request_body)
# Add role name
role_name = extract_role_name(event)
if role_name:
context["role_name"] = role_name
return context
Rule specification
AnalysisType: rule
Filename: azure_role_assignment_privileged_or_elevated.py
RuleID: "Azure.MonitorActivity.RoleAssignment.PrivilegedOrElevated"
DisplayName: "Azure Privileged or Elevated Role Assignment"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Medium
Description: >
Detects when a privileged or elevated Azure role is assigned.
Privileged roles include Owner, Contributor, User Access Administrator, Security Admin, and other high-impact administrative roles.
Elevated roles include resource-specific roles with significant permissions like Storage Blob Data Owner, Key Vault Administrator, etc.
Reports:
MITRE ATT&CK:
- TA0004:T1098 # Persistence: Account Manipulation
- TA0003:T1098.003 # Persistence: Add Office 365 Global Administrator Role
- TA0005:T1078.004 # Defense Evasion: Valid Accounts - Cloud Accounts
Tags:
- AZT402
- Persistence
- Defense Evasion
- Account Manipulation
- Add Office 365 Global Administrator Role
- Valid Accounts
- Cloud Accounts
Runbook: |
1. Find all Azure Monitor Activity role assignment and elevate access operations by the callerIpAddress in the 24 hours before and after the alert to identify if multiple privileged roles are being granted
2. Query for all API calls by the principalId in the 6 hours after the role assignment to determine if newly granted permissions were immediately exploited
3. Check if the callerIpAddress is associated with known VPN services or corporate IP ranges and compare to the caller's authentication patterns in the past 30 days
Reference: https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when any of the conditions below holds.
Condition
any of:
all of:
operationName.valueis presentoperationName.valueisMICROSOFT.AUTHORIZATION/ELEVATEACCESS/ACTIONstatus.valueisSucceeded
all of:
operationNameisMICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITEresultTypeis one ofSuccess,Succeeded
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE" |
operationName.value | eq |
| field:"operationName.value" kind:eq value:"MICROSOFT.AUTHORIZATION/ELEVATEACCESS/ACTION" |
operationName.value | is_not_null | field:"operationName.value" kind:is_not_null | |
resultType | in |
| field:"resultType" kind:in |
status.value | eq |
| field:"status.value" kind:eq value:"Succeeded" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
name | identity.claims.name |
resourceId |
Response runbook
1. Find all Azure Monitor Activity role assignment and elevate access operations by the callerIpAddress in the 24 hours before and after the alert to identify if multiple privileged roles are being granted
2. Query for all API calls by the principalId in the 6 hours after the role assignment to determine if newly granted permissions were immediately exploited
3. Check if the callerIpAddress is associated with known VPN services or corporate IP ranges and compare to the caller's authentication patterns in the past 30 days
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "f1e2d3c4-b5a6-7890-bcde-f12345678901",
"identity": {
"authorization": {
"action": "Microsoft.Authorization/roleAssignments/write",
"evidence": {
"principalId": "6b6d44f0-b13a-46a0-bfde-161324d4c34d",
"principalType": "User",
"role": "Owner",
"roleAssignmentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"roleAssignmentScope": "/subscriptions/12345678-1234-1234-1234-123456789abc",
"roleDefinitionId": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635"
}
}
},
"location": "",
"operationName": "Microsoft.Authorization/roleAssignments/write",
"operationVersion": "2021-04-01",
"properties": {
"requestbody": "{\"Id\":\"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\"Properties\":{\"PrincipalId\":\"770797c4-e05a-43f9-bda2-1f1f379987ae\",\"PrincipalType\":\"ServicePrincipal\",\"RoleDefinitionId\":\"/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635\",\"Scope\":\"/subscriptions/12345678-1234-1234-1234-123456789abc\"}}"
},
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/providers/Microsoft.Authorization/roleAssignments/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Protection Multiple Alerts for User
#Detects when a user account triggers multiple Microsoft Entra ID Protection risk alerts within a short time window, indicating a potentially ongoing attack or compromised account. Entra ID Protection uses machine learning and heuristics to detect risky sign-in activity including anonymous IP usage, atypical travel patterns, malware-linked IP addresses, unfamiliar sign-in properties, password spray attempts, leaked credentials, and other suspicious behaviors.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry coverage
Detection logic
from panther_azuresignin_helpers import (
azure_signin_alert_context,
azure_signin_success,
is_sign_in_event,
)
# Risk states that indicate protection alerts
RISK_STATES = {
"atrisk",
"confirmedcompromised",
}
# Risk levels that should be tracked
RISK_LEVELS = {
"high",
"medium",
}
def rule(event):
if not is_sign_in_event(event) or not azure_signin_success(event):
return False
# Only track events with actual risk
risk_state = event.deep_get("properties", "riskState", default="")
if not risk_state or risk_state.lower() not in RISK_STATES:
return False
# Check risk levels
risk_level_during = event.deep_get("properties", "riskLevelDuringSignIn", default="").lower()
risk_level_agg = event.deep_get("properties", "riskLevelAggregated", default="").lower()
has_risk = risk_level_during in RISK_LEVELS or risk_level_agg in RISK_LEVELS
if not has_risk:
return False
# Get user principal name for tracking
user_principal_name = event.deep_get("properties", "userPrincipalName", default="")
return bool(user_principal_name)
def dedup(event):
return event.deep_get("properties", "userPrincipalName", default="<UNKNOWN_USER>")
def title(event):
user_principal_name = event.deep_get(
"properties", "userPrincipalName", default="<UNKNOWN_USER>"
)
return (
f"Multiple Entra ID Protection Alerts: User [{user_principal_name}] "
f"has multiple risk events "
)
def alert_context(event):
context = azure_signin_alert_context(event)
# Add risk-specific context
context["risk_state"] = event.deep_get("properties", "riskState", default="<NO_RISK_STATE>")
context["risk_level_during_signin"] = event.deep_get(
"properties", "riskLevelDuringSignIn", default="<NO_RISK_LEVEL>"
)
context["risk_level_aggregated"] = event.deep_get(
"properties", "riskLevelAggregated", default="<NO_RISK_LEVEL>"
)
context["risk_detail"] = event.deep_get("properties", "riskDetail", default="<NO_RISK_DETAIL>")
context["risk_event_types"] = event.deep_get(
"properties", "riskEventTypes", default="<NO_RISK_TYPES>"
)
# Add authentication details
context["authentication_protocol"] = event.deep_get(
"properties", "authenticationProtocol", default="<NO_PROTOCOL>"
)
context["client_app_used"] = event.deep_get(
"properties", "clientAppUsed", default="<NO_CLIENT_APP>"
)
context["device_detail_browser"] = event.deep_get(
"properties", "deviceDetail", "browser", default="<NO_BROWSER>"
)
context["device_detail_os"] = event.deep_get(
"properties", "deviceDetail", "operatingSystem", default="<NO_OS>"
)
context["is_interactive"] = event.deep_get("properties", "isInteractive", default=None)
context["user_agent"] = event.deep_get("properties", "userAgent", default="<NO_USER_AGENT>")
# Add location details
context["location_city"] = event.deep_get("properties", "location", "city", default="<NO_CITY>")
context["location_country"] = event.deep_get(
"properties", "location", "countryOrRegion", default="<NO_COUNTRY>"
)
return context
Rule specification
AnalysisType: rule
Filename: azure_multiple_protection_alerts.py
RuleID: "Azure.Audit.MultipleProtectionAlerts"
DisplayName: "Azure Protection Multiple Alerts for User"
Enabled: true
LogTypes:
- Azure.Audit
Severity: High
Threshold: 3
DedupPeriodMinutes: 15
Description: >
Detects when a user account triggers multiple Microsoft Entra ID Protection risk alerts within
a short time window, indicating a potentially ongoing attack or compromised account. Entra ID
Protection uses machine learning and heuristics to detect risky sign-in activity including
anonymous IP usage, atypical travel patterns, malware-linked IP addresses, unfamiliar sign-in
properties, password spray attempts, leaked credentials, and other suspicious behaviors.
Reports:
MITRE ATT&CK:
- TA0001:T1078
- TA0001:T1078.004
Runbook: |
1. Query Azure.Audit logs for all sign-in events by properties:userPrincipalName in the 1 hour surrounding this alert to identify the full sequence of risk events, noting properties:riskEventTypes, callerIpAddress, properties:location, and properties:userAgent for each authentication attempt
2. Review properties:riskDetail and properties:riskState for each event to understand the specific risk detections that triggered the alerts, and check if multiple different risk types occurred (e.g., anonymous IP + atypical travel + unfamiliar properties) which strongly indicates account compromise rather than false positives
3. Immediately disable the user account or revoke all active sessions if suspicious activity is confirmed, force password reset with MFA re-enrollment, review Azure.Audit logs for any API calls or resource access by the user in the 24 hours after the first risk event to identify data exfiltration or privilege escalation, and contact the user through out-of-band communication to verify if they are aware of the authentication attempts
Reference: https://github.com/elastic/detection-rules/blob/main/rules/integrations/azure/initial_access_entra_id_protection_alerts_for_user.toml
SummaryAttributes:
- properties:userPrincipalName
- callerIpAddress
- properties:riskState
- properties:riskLevelDuringSignIn
Stages and Predicates
Fires on Azure.Audit events when all of the conditions below hold.
Condition
operationNameisSign-in activityresultSignatureisSUCCESSproperties.riskStateis presentproperties.riskStateis one ofatrisk,confirmedcompromisedany of:
properties.riskLevelDuringSignInis one ofhigh,mediumproperties.riskLevelAggregatedis one ofhigh,medium
properties.userPrincipalNameis present
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
properties.riskState | in | atrisk, confirmedcompromised | excludes:properties.riskState field:"properties.riskState" value:"atrisk" field:"properties.riskState" value:"confirmedcompromised" |
properties.riskState | is_null | excludes:properties.riskState | |
operationName | ne | Sign-in activity | excludes:operationName field:"operationName" value:"Sign-in activity" |
resultSignature | ne | SUCCESS | excludes:resultSignature field:"resultSignature" value:"SUCCESS" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
properties.riskLevelAggregated | in |
| field:"properties.riskLevelAggregated" kind:in |
properties.riskLevelDuringSignIn | in |
| field:"properties.riskLevelDuringSignIn" kind:in |
properties.riskState | in |
| field:"properties.riskState" kind:in |
properties.userPrincipalName | is_not_null | field:"properties.userPrincipalName" kind:is_not_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
userPrincipalName | properties.userPrincipalName |
Response runbook
1. Query Azure.Audit logs for all sign-in events by properties:userPrincipalName in the 1 hour surrounding this alert to identify the full sequence of risk events, noting properties:riskEventTypes, callerIpAddress, properties:location, and properties:userAgent for each authentication attempt
2. Review properties:riskDetail and properties:riskState for each event to understand the specific risk detections that triggered the alerts, and check if multiple different risk types occurred (e.g., anonymous IP + atypical travel + unfamiliar properties) which strongly indicates account compromise rather than false positives
3. Immediately disable the user account or revoke all active sessions if suspicious activity is confirmed, force password reset with MFA re-enrollment, review Azure.Audit logs for any API calls or resource access by the user in the 24 hours after the first risk event to identify data exfiltration or privilege escalation, and contact the user through out-of-band communication to verify if they are aware of the authentication attempts
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"Level": "4",
"callerIpAddress": "3.3.3.3",
"category": "SignInLogs",
"correlationId": "risk-event-003",
"durationMs": 0,
"operationName": "Sign-in activity",
"operationVersion": "1.0",
"p_event_time": "2025-01-15 09:35:45.789",
"p_log_type": "Azure.Audit",
"p_parse_time": "2025-01-15 09:35:46.000",
"p_row_id": "row-003",
"properties": {
"authenticationProtocol": "oAuth2",
"clientAppUsed": "Browser",
"createdDateTime": "2025-01-15T09:35:45.7890123Z",
"deviceDetail": {
"browser": "Firefox",
"operatingSystem": "Linux"
},
"ipAddress": "3.3.3.3",
"isInteractive": true,
"location": {
"city": "Beijing",
"countryOrRegion": "China"
},
"riskDetail": "aiConfirmedSigninCompromised",
"riskEventTypes": [
"leakedCredentials",
"anomalousToken"
],
"riskLevelAggregated": "high",
"riskLevelDuringSignIn": "high",
"riskState": "confirmedCompromised",
"userAgent": "Mozilla/5.0 (X11; Linux x86_64)",
"userId": "user-victim-123",
"userPrincipalName": "aragorn@lotr.com"
},
"resourceId": "/tenants/tenant-123/providers/Microsoft.aadiam",
"resultSignature": "SUCCESS",
"resultType": "0",
"tenantId": "tenant-123",
"time": "2025-01-15 09:35:45.789"
}
Azure Recovery Services Protection Container Deleted
#Detects deletion of Azure Recovery Services protection containers containing VM and workload backups. Storm-0501 systematically deletes backup containers before deploying ransomware to prevent recovery. This operation permanently destroys all recovery points for protected resources.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment | |
| Impact |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
PROTECTION_CONTAINER_DELETE = (
"MICROSOFT.RECOVERYSERVICES/VAULTS/BACKUPFABRICS/PROTECTIONCONTAINERS/DELETE"
)
def rule(event):
return event.get(
"operationName", ""
).upper() == PROTECTION_CONTAINER_DELETE and azure_activity_success(event)
def title(event):
resource_id = event.get("resourceId", "")
vault_name = extract_resource_name_from_id(resource_id, "vaults", default="<UNKNOWN_VAULT>")
container_name = extract_resource_name_from_id(
resource_id, "protectionContainers", default="<UNKNOWN_CONTAINER>"
)
return (
f"Azure Recovery Services protection container [{container_name}] "
f"deleted from vault [{vault_name}]"
)
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
vault_name = extract_resource_name_from_id(resource_id, "vaults", default="")
if vault_name:
context["vault_name"] = vault_name
container_name = extract_resource_name_from_id(resource_id, "protectionContainers", default="")
if container_name:
context["protection_container"] = container_name
fabric_name = extract_resource_name_from_id(resource_id, "backupFabrics", default="")
if fabric_name:
context["backup_fabric"] = fabric_name
resource_group = extract_resource_name_from_id(resource_id, "resourceGroups", default="")
if resource_group:
context["resource_group"] = resource_group
return context
Rule specification
AnalysisType: rule
Filename: azure_recovery_services_container_deleted.py
RuleID: "Azure.MonitorActivity.RecoveryServices.ProtectionContainerDeleted"
DisplayName: "Azure Recovery Services Protection Container Deleted"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: High
Description: >
Detects deletion of Azure Recovery Services protection containers containing VM and workload backups.
Storm-0501 systematically deletes backup containers before deploying ransomware to prevent recovery.
This operation permanently destroys all recovery points for protected resources.
Reports:
MITRE ATT&CK:
- TA0040:T1490 # Impact: Inhibit System Recovery
- TA0040:T1485 # Impact: Data Destruction
- TA0005:T1562 # Defense Evasion: Impair Defenses
Reference: https://www.microsoft.com/en-us/security/blog/2025/08/27/storm-0501s-evolving-techniques-lead-to-cloud-based-ransomware/
Tags:
- Impact
- Inhibit System Recovery
- Data Destruction
- Defense Evasion
- Impair Defenses
- Ransomware
- Storm-0501
- Backup
Runbook: |
1. Query Azure Monitor Activity logs for all protection container deletions by the callerIpAddress in the past 6 hours to calculate the total number of backup containers destroyed
2. Search for resource lock deletions on the same vault by the same caller in the 6 hours before this deletion to identify Storm-0501 attack pattern
3. Find all role assignment operations for the caller identity in the 48 hours before deletion to identify if Owner or Backup Operator permissions were recently granted
4. Check Azure.Audit logs for authentication events from the callerIpAddress in the 48-72 hours before to identify account compromise indicators
5. Search for other backup destruction or ransomware-related alerts triggered by the same callerIpAddress across all subscriptions in the past 7 days
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.RECOVERYSERVICES/VAULTS/BACKUPFABRICS/PROTECTIONCONTAINERS/DELETEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.RECOVERYSERVICES/VAULTS/BACKUPFABRICS/PROTECTIONCONTAINERS/DELETE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all protection container deletions by the callerIpAddress in the past 6 hours to calculate the total number of backup containers destroyed
2. Search for resource lock deletions on the same vault by the same caller in the 6 hours before this deletion to identify Storm-0501 attack pattern
3. Find all role assignment operations for the caller identity in the 48 hours before deletion to identify if Owner or Backup Operator permissions were recently granted
4. Check Azure.Audit logs for authentication events from the callerIpAddress in the 48-72 hours before to identify account compromise indicators
5. Search for other backup destruction or ransomware-related alerts triggered by the same callerIpAddress across all subscriptions in the past 7 days
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "203.0.113.42",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"identity": {
"claims": {
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn": "compromised-admin@company.com",
"name": "compromised-admin@company.com"
}
},
"level": "Informational",
"location": "eastus",
"operationName": "MICROSOFT.RECOVERYSERVICES/VAULTS/BACKUPFABRICS/PROTECTIONCONTAINERS/DELETE",
"operationVersion": "2021-12-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/backup-rg/providers/Microsoft.RecoveryServices/vaults/ProductionBackupVault/backupFabrics/Azure/protectionContainers/IaasVMContainer;iaasvmcontainerv2;prod-rg;prod-vm-001",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-01-27T14:55:00.0000000Z"
}
Azure Resource Group Deleted
#Detects when an Azure Resource Group is deleted. Resource group deletion removes all resources within the group and may indicate mass destruction or legitimate cleanup.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
RESOURCE_GROUP_DELETE = "MICROSOFT.RESOURCES/SUBSCRIPTIONS/RESOURCEGROUPS/DELETE"
def rule(event):
return event.get(
"operationName", ""
).upper() == RESOURCE_GROUP_DELETE and azure_activity_success(event)
def title(event):
resource_id = event.get("resourceId", "")
resource_group = extract_resource_name_from_id(
resource_id, "resourceGroups", default="<UNKNOWN_RESOURCE_GROUP>"
)
return f"Azure Resource Group [{resource_group}] deleted"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
resource_group_name = extract_resource_name_from_id(resource_id, "resourceGroups", default="")
if resource_group_name:
context["resource_group_name"] = resource_group_name
return context
Rule specification
AnalysisType: rule
Filename: azure_resource_group_deleted.py
RuleID: "Azure.MonitorActivity.ResourceGroup.Deleted"
DisplayName: "Azure Resource Group Deleted"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Medium
Description: >
Detects when an Azure Resource Group is deleted.
Resource group deletion removes all resources within the group and may indicate mass destruction or legitimate cleanup.
Reports:
MITRE ATT&CK:
- TA0040:T1485 # Impact: Data Destruction
Tags:
- Impact
- Data Destruction
Runbook: |
1. Query Azure Monitor Activity logs for all resource operations by the callerIpAddress in the 24 hours before this alert to establish if this is part of a larger resource deletion pattern
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure resource deletions or destructive operations from the same user or IP in the past 7 days to assess the scope of potential impact
Reference: https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/delete-resource-group?tabs=azure-powershell
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.RESOURCES/SUBSCRIPTIONS/RESOURCEGROUPS/DELETEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.RESOURCES/SUBSCRIPTIONS/RESOURCEGROUPS/DELETE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all resource operations by the callerIpAddress in the 24 hours before this alert to establish if this is part of a larger resource deletion pattern
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure resource deletions or destructive operations from the same user or IP in the past 7 days to assess the scope of potential impact
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"location": "eastus",
"operationName": "Microsoft.Resources/subscriptions/resourceGroups/delete",
"operationVersion": "2021-04-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Resource Lock Deleted
#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.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment | |
| Impact |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | Activity Log event Microsoft.Authorization/locks/delete: Delete locks at the specified scope. |
Detection logic
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
Rule specification
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
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.AUTHORIZATION/LOCKS/DELETEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.AUTHORIZATION/LOCKS/DELETE" |
resultType | in |
| field:"resultType" kind:in |
Response 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
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "203.0.113.42",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"identity": {
"claims": {
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn": "compromised-admin@company.com",
"name": "compromised-admin@company.com"
}
},
"level": "Informational",
"location": "eastus",
"operationName": "MICROSOFT.AUTHORIZATION/LOCKS/DELETE",
"operationVersion": "2017-04-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/critical-data-rg/providers/Microsoft.Storage/storageAccounts/criticaldata001/providers/Microsoft.Authorization/locks/DoNotDelete",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-01-27T14:23:00.0000000Z"
}
Azure Restore Point Collection Deleted
#Detects when an Azure restore point collection is deleted. Restore point collections contain crash-consistent and application-consistent recovery points for virtual machines. Adversaries may delete these collections to prevent system recovery, destroy forensic evidence, or undermine backup strategies before launching ransomware attacks. This is a strong indicator of inhibiting system recovery capabilities.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
RESTORE_POINT_DELETE_OPERATION = "MICROSOFT.COMPUTE/RESTOREPOINTCOLLECTIONS/DELETE"
def rule(event):
return event.get(
"operationName", ""
).upper() == RESTORE_POINT_DELETE_OPERATION and azure_activity_success(event)
def title(event):
resource_id = event.get("resourceId", "<UNKNOWN_RESOURCE>")
collection_name = extract_resource_name_from_id(
resource_id, "restorePointCollections", default="<UNKNOWN_COLLECTION>"
)
return f"Azure Restore Point Collection Deleted: [{collection_name}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
resource_group = extract_resource_name_from_id(resource_id, "resourceGroups", default="")
if resource_group:
context["resource_group"] = resource_group
return context
Rule specification
AnalysisType: rule
Filename: azure_restore_point_collection_deleted.py
RuleID: "Azure.MonitorActivity.Compute.RestorePointCollectionDeleted"
DisplayName: "Azure Restore Point Collection Deleted"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Medium
Description: >
Detects when an Azure restore point collection is deleted. Restore point collections contain
crash-consistent and application-consistent recovery points for virtual machines. Adversaries
may delete these collections to prevent system recovery, destroy forensic evidence, or undermine
backup strategies before launching ransomware attacks. This is a strong indicator of inhibiting
system recovery capabilities.
Reports:
MITRE ATT&CK:
- TA0040:T1490 # Impact: Inhibit System Recovery
- TA0040:T1485 # Impact: Data Destruction
Tags:
- Impact
- Data Destruction
- Inhibit System Recovery
- Ransomware
Runbook: |
1. Query Azure Monitor Activity logs for all backup and recovery operations (restore point deletions, snapshot deletions, disk deletions) by the callerIpAddress in the 24 hours before and after the alert
2. Find all restore point collection and snapshot deletions in the past 6 hours to determine if this is part of a pre-ransomware attack pattern
3. Check if the callerIpAddress has deleted recovery resources in the past 90 days to establish if this is normal maintenance activity
Reference: https://github.com/elastic/detection-rules/blob/main/rules/integrations/azure/impact_azure_virtual_machine_restore_point_collection_deleted.toml
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
- location
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.COMPUTE/RESTOREPOINTCOLLECTIONS/DELETEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.COMPUTE/RESTOREPOINTCOLLECTIONS/DELETE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all backup and recovery operations (restore point deletions, snapshot deletions, disk deletions) by the callerIpAddress in the 24 hours before and after the alert
2. Find all restore point collection and snapshot deletions in the past 6 hours to determine if this is part of a pre-ransomware attack pattern
3. Check if the callerIpAddress has deleted recovery resources in the past 90 days to establish if this is normal maintenance activity
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Informational",
"location": "eastus",
"operationName": "MICROSOFT.COMPUTE/RESTOREPOINTCOLLECTIONS/DELETE",
"operationVersion": "2021-12-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/backup-rg/providers/Microsoft.Compute/restorePointCollections/vm-restore-points",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-22T10:30:00.0000000Z"
}
Azure RiskLevel Passthrough
#This detection surfaces an alert based on riskLevelAggregated, riskLevelDuringSignIn, and riskState. riskLevelAggregated and riskLevelDuringSignIn are only expected for Azure AD Premium P2 customers.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access |
Telemetry coverage
Detection logic
from panther_azuresignin_helpers import actor_user, azure_signin_alert_context, is_sign_in_event
PASSTHROUGH_SEVERITIES = {"low", "medium", "high"}
def rule(event):
if not is_sign_in_event(event):
return False
global IDENTIFIED_RISK_LEVEL # pylint: disable=global-variable-undefined
IDENTIFIED_RISK_LEVEL = ""
# Do not pass through risks marked as dismissed or remediated in AD
if event.deep_get("properties", "riskState", default="").lower() in [
"dismissed",
"remediated",
]:
return False
# check riskLevelAggregated
for risk_type in ["riskLevelAggregated", "riskLevelDuringSignIn"]:
if event.deep_get("properties", risk_type, default="").lower() in PASSTHROUGH_SEVERITIES:
IDENTIFIED_RISK_LEVEL = event.deep_get("properties", risk_type).lower()
return True
return False
def title(event):
principal = actor_user(event)
if principal is None:
principal = "<NO_PRINCIPALNAME>"
return f"AzureSignIn: RiskRanked Activity for Principal [{principal}]"
def alert_context(event):
a_c = azure_signin_alert_context(event)
a_c["riskLevel"] = IDENTIFIED_RISK_LEVEL
return a_c
def severity(_):
if IDENTIFIED_RISK_LEVEL:
return IDENTIFIED_RISK_LEVEL
return "INFO"
Rule specification
AnalysisType: rule
Filename: azure_risklevel_passthrough.py
RuleID: "Azure.Audit.RiskLevelPassthrough"
DisplayName: "Azure RiskLevel Passthrough"
Enabled: true
Threshold: 1
DedupPeriodMinutes: 40
LogTypes:
- Azure.Audit
Severity: Medium
Description: >
This detection surfaces an alert based on
riskLevelAggregated, riskLevelDuringSignIn, and riskState.
riskLevelAggregated and riskLevelDuringSignIn are only
expected for Azure AD Premium P2 customers.
Reference: https://learn.microsoft.com/en-us/entra/id-protection/howto-identity-protection-configure-risk-policies
Reports:
MITRE ATT&CK:
- TA0006:T1110
- TA0001:T1078
Runbook: >
There are a variety of potential responses to these sign-in risks.
MSFT has provided an in-depth reference material at https://learn.microsoft.com/en-us/azure/active-directory/identity-protection/howto-identity-protection-risk-feedback
SummaryAttributes:
- properties:ServicePrincipalName
- properties:UserPrincipalName
- properties:ipAddress
- properties:riskLevelAggregated
- properties:riskLevelDuringSignIn
- properties:riskState
Stages and Predicates
Fires on Azure.Audit events when all of the conditions below hold.
Condition
operationNameisSign-in activityproperties.riskStateis not one ofdismissed,remediatedany of:
propertiesis one oflow,medium,highpropertiesis one oflow,medium,high
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
properties.riskState | in | dismissed, remediated | excludes:properties.riskState field:"properties.riskState" value:"dismissed" field:"properties.riskState" value:"remediated" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"Sign-in activity" |
properties | in |
| field:"properties" kind:in |
Response runbook
There are a variety of potential responses to these sign-in risks. MSFT has provided an in-depth reference material at https://learn.microsoft.com/en-us/azure/active-directory/identity-protection/howto-identity-protection-risk-feedback
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"Level": "4",
"calleripaddress": "12.12.12.12",
"category": "ServicePrincipalSignInLogs",
"correlationid": "e1f237ef-6548-4172-be79-03818c04c06e",
"durationms": 0,
"location": "IE",
"operationname": "Sign-in activity",
"operationversion": 1,
"p_event_time": "2023-07-26 23:00:20.889",
"p_log_type": "Azure.Audit",
"properties": {
"appId": "cfceb902-8fab-4f8c-88ba-374d3c975c3a",
"authenticationProcessingDetails": [
{
"key": "Azure AD App Authentication Library",
"value": ""
}
],
"authenticationProtocol": "none",
"clientCredentialType": "none",
"conditionalAccessStatus": "notApplied",
"correlationId": "5889315c-c4ac-4807-99da-e17417eae786",
"createdDateTime": "2023-07-26 22:58:30.983201900",
"crossTenantAccessType": "none",
"flaggedForReview": false,
"id": "36658c78-02d9-4d8f-84ee-5ca4a3fdefef",
"incomingTokenType": "none",
"ipAddress": "12.12.12.12",
"isInteractive": false,
"isTenantRestricted": false,
"location": {
"city": "Dublin",
"countryOrRegion": "IE",
"geoCoordinates": {
"latitude": 51.35555555555555,
"longitude": -5.244444444444444
},
"state": "Dublin"
},
"managedIdentityType": "none",
"processingTimeInMilliseconds": 0,
"resourceDisplayName": "Azure Storage",
"resourceId": "037694de-8c7d-498d-917d-edb650090fa5",
"resourceServicePrincipalId": "a225221f-8cc5-411a-9cc7-5e1394b8a5b8",
"riskDetail": "none",
"riskLevelAggregated": "low",
"riskLevelDuringSignIn": "none",
"riskState": "none",
"servicePrincipalId": "b1c34143-e405-4058-8e29-84596ad737b8",
"servicePrincipalName": "some-service-principal",
"status": {
"errorCode": 7000215
},
"tokenIssuerType": "AzureAD",
"uniqueTokenIdentifier": "NDDDDDDDDDDDDDDDDDD_DD"
},
"resourceid": "/tenants/c0dd2fa0-71be-4df8-b2a6-24cee7de069a/providers/Microsoft.aadiam",
"resultsignature": "None",
"resulttype": 7000215,
"tenantid": "a2aa49aa-2c0c-49d2-af87-f402c421df0b",
"time": "2023-07-26 23:00:20.889"
}
Azure Role Changed PIM
#This detection looks for a change in member's PIM roles in EntraID
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Resource Development |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Detection logic
from panther_msft_helpers import azure_rule_context, azure_success, get_target_name
def rule(event):
operation = event.get("operationName", default="")
if azure_success(event) and "Add member to role in PIM completed" in operation:
return True
return False
def title(event):
operation_name = event.get("operationName", default="<UNKNOWN OPERATION>")
actor_name = event.deep_get(
"properties", "initiatedBy", "user", "userPrincipalName", default="<UNKNOWN USER>"
)
target_name = get_target_name(event)
role = event.deep_walk(
"properties", "targetResources", "displayName", return_val="first", default="<UNKNOWN_ROLE>"
)
return f"{actor_name} added {target_name} as {role} successfully with {operation_name}"
def dedup(event):
# Ensure every event is a separate alert
return event.get("p_row_id", "<UNKNOWN_ROW_ID>")
def alert_context(event):
return azure_rule_context(event)
Rule specification
AnalysisType: rule
Filename: azure_role_changed_pim.py
RuleID: "Azure.Audit.RoleChangedPIM"
DisplayName: "Azure Role Changed PIM"
Enabled: true
LogTypes:
- Azure.Audit
Severity: Medium
DedupPeriodMinutes: 5
Description: >
This detection looks for a change in member's PIM roles in EntraID
Reports:
MITRE ATT&CK:
- TA0042:T1586
Runbook: >
Verify if the role change was authorized and review the affected user. If unauthorized, revert the role change, notify relevant teams,
Reference: https://learn.microsoft.com/en-us/entra/identity/authentication/overview-authentication
SummaryAttributes:
- properties:ServicePrincipalName
- properties:UserPrincipalName
- properties:ipAddress
Stages and Predicates
Fires on Azure.Audit events when all of the conditions below hold.
Condition
properties.resultissuccessoperationNamecontainsAdd member to role in PIM completed
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | contains |
| field:"operationName" kind:contains value:"Add member to role in PIM completed" |
properties.result | eq |
| field:"properties.result" kind:eq value:"success" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
operationName | |
category | properties.category |
actor_id | properties.initiatedBy.user.id |
actor_upn | properties.initiatedBy.user.userPrincipalName |
source_ip_address | properties.initiatedBy.user.ipAddress |
target_id | properties.targetResources.id |
target_name | properties.targetResources.displayName |
Response runbook
Verify if the role change was authorized and review the affected user. If unauthorized, revert the role change, notify relevant teams,
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"Level": "4",
"category": "AuditLogs",
"correlationId": "1234155",
"durationMs": 0,
"identity": "Ju Cho",
"operationName": "Add member to role in PIM completed (permanent)",
"operationVersion": "1.0",
"p_row_id": "2316902d-b9a4-4f37-a1a5-5ed03993110f",
"properties": {
"activityDateTime": "2024-12-16 16:32:16.087554000",
"activityDisplayName": "Add member to role in PIM completed (permanent)",
"additionalDetails": [
{
"key": "RoleDefinitionOriginId",
"value": "123451235"
},
{
"key": "RoleDefinitionOriginType",
"value": "BuiltInRole"
},
{
"key": "TemplateId",
"value": "123412351"
},
{
"key": "StartTime",
"value": "2024-12-16T16:32:15.8441686Z"
},
{
"key": "Justification",
"value": "test assign"
},
{
"key": "oid",
"value": "12351534"
},
{
"key": "tid",
"value": "345667733"
},
{
"key": "wids",
"value": "234523454"
},
{
"key": "ipaddr",
"value": "1.2.3.4"
},
{
"key": "RequestId",
"value": "111111111111"
}
],
"category": "RoleManagement",
"correlationId": "12345",
"id": "PIM_123415",
"initiatedBy": {
"user": {
"displayName": "Ju Cho",
"id": "12345",
"roles": [],
"userPrincipalName": "Radahn@Starscourge.onmicrosoft.com"
}
},
"loggedByService": "PIM",
"operationType": "Update",
"result": "success",
"resultReason": "test assign",
"targetResources": [
{
"administrativeUnits": [],
"displayName": "Application Administrator",
"id": "12345",
"modifiedProperties": [
{
"displayName": "RoleDefinitionOriginId",
"newValue": "\"12345\"",
"oldValue": "\"\""
},
{
"displayName": "RoleDefinitionOriginType",
"newValue": "\"BuiltInRole\"",
"oldValue": "\"\""
},
{
"displayName": "TemplateId",
"newValue": "\"12345\"",
"oldValue": "\"\""
}
],
"type": "Role"
},
{
"administrativeUnits": [],
"id": "12345",
"type": "Request"
},
{
"administrativeUnits": [],
"displayName": "Malenia",
"id": "12345",
"type": "User"
},
{
"administrativeUnits": [],
"displayName": "Panther",
"id": "12345",
"type": "Directory"
},
{
"administrativeUnits": [],
"id": "12345",
"type": "Other"
}
]
},
"resourceId": "/tenants/12345/providers/Microsoft.aadiam",
"resultSignature": "None",
"tenantId": "12345",
"time": "2024-12-16 16:32:16.087554000"
}
Azure ROPC Login Attempt Without MFA
#Detects Resource Owner Password Credentials (ROPC) OAuth 2.0 authentication attempts in Microsoft Entra ID using single-factor authentication without MFA enforcement. ROPC is a deprecated legacy flow that allows applications to directly collect user credentials to obtain access tokens, bypassing modern authentication and MFA requirements. Adversaries commonly exploit ROPC during credential enumeration and password spraying campaigns using tools like TeamFiltration and MSOLSpray.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry coverage
Detection logic
from panther_azuresignin_helpers import (
azure_signin_alert_context,
azure_signin_success,
is_sign_in_event,
)
def rule(event):
# Check for sign-in events
if not is_sign_in_event(event) or not azure_signin_success(event):
return False
# Check for ROPC authentication protocol
auth_protocol = event.deep_get("properties", "authenticationProtocol", default="").lower()
if auth_protocol != "ropc":
return False
auth_requirement = event.deep_get("properties", "authenticationRequirement", default="").lower()
# Only alert on no MFA
if auth_requirement != "singlefactorauthentication":
return False
# Check user type for Member account
user_type = event.deep_get("properties", "userType", default="").lower()
if user_type and user_type != "member":
return False
return True
def title(event):
user_principal_name = event.deep_get(
"properties", "userPrincipalName", default="<UNKNOWN_USER>"
)
source_ip = event.deep_get("properties", "ipAddress", default="<UNKNOWN_IP>")
app_display_name = event.deep_get("properties", "appDisplayName", default="<UNKNOWN_APP>")
return (
f"ROPC Login Without MFA: User [{user_principal_name}] authenticated via "
f"ROPC protocol from IP [{source_ip}] to app [{app_display_name}]"
)
def alert_context(event):
context = azure_signin_alert_context(event)
# Add ROPC-specific fields
fields = {
"authentication_protocol": ("properties", "authenticationProtocol", "<NO_PROTOCOL>"),
"authentication_requirement": (
"properties",
"authenticationRequirement",
"<NO_REQUIREMENT>",
),
"user_type": ("properties", "userType", "<NO_USER_TYPE>"),
"app_display_name": ("properties", "appDisplayName", "<NO_APP>"),
"app_id": ("properties", "appId", "<NO_APP_ID>"),
"client_app_used": ("properties", "clientAppUsed", "<NO_CLIENT_APP>"),
"user_agent": ("properties", "userAgent", "<NO_USER_AGENT>"),
"is_interactive": ("properties", "isInteractive", None),
"conditional_access_status": ("properties", "conditionalAccessStatus", "<NO_CA_STATUS>"),
"device_detail_browser": ("properties", "deviceDetail", "browser", "<NO_BROWSER>"),
"device_detail_os": ("properties", "deviceDetail", "operatingSystem", "<NO_OS>"),
}
for key, (*path, default) in fields.items():
context[key] = event.deep_get(*path, default=default)
return context
Rule specification
AnalysisType: rule
Filename: azure_ropc_login_no_mfa.py
RuleID: "Azure.Audit.ROPCLoginNoMFA"
DisplayName: "Azure ROPC Login Attempt Without MFA"
Enabled: true
Status: Experimental
LogTypes:
- Azure.Audit
Severity: Medium
DedupPeriodMinutes: 60
Description: >
Detects Resource Owner Password Credentials (ROPC) OAuth 2.0 authentication attempts in Microsoft
Entra ID using single-factor authentication without MFA enforcement. ROPC is a deprecated legacy
flow that allows applications to directly collect user credentials to obtain access tokens, bypassing
modern authentication and MFA requirements. Adversaries commonly exploit ROPC during credential
enumeration and password spraying campaigns using tools like TeamFiltration and MSOLSpray.
Reports:
MITRE ATT&CK:
- TA0001:T1078
- TA0001:T1078.004
Runbook: |
1. Query Azure.Audit logs for all ROPC authentication attempts by properties:userPrincipalName in the 24 hours surrounding this event to determine if this represents isolated testing or part of a broader password spraying or credential enumeration campaign with multiple failed attempts across different accounts
2. Identify the application using ROPC by reviewing properties:appDisplayName and properties:appId, and verify with application owners whether this application has a legitimate business requirement for ROPC authentication or if it should be migrated to modern OAuth flows with interactive authentication
3. Review the source IP address callerIpAddress and user agent properties:userAgent for indicators of automated attack tools (e.g., python-requests, curl, TeamFiltration signatures), check if the IP is associated with known malicious infrastructure, and if suspicious activity is confirmed, block the application from using ROPC through Azure AD application policies and force password reset for the affected user
Reference: https://github.com/elastic/detection-rules/blob/main/rules/integrations/azure/initial_access_entra_id_unusual_ropc_login_attempt.toml
SummaryAttributes:
- properties:userPrincipalName
- callerIpAddress
- properties:appDisplayName
Stages and Predicates
Fires on Azure.Audit events when all of the conditions below hold.
Condition
operationNameisSign-in activityresultSignatureisSUCCESSproperties.authenticationProtocolisropcproperties.authenticationRequirementissinglefactorauthenticationany of:
properties.userTypeis emptyproperties.userTypeismember
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
operationName | ne | Sign-in activity | excludes:operationName field:"operationName" value:"Sign-in activity" |
resultSignature | ne | SUCCESS | excludes:resultSignature field:"resultSignature" value:"SUCCESS" |
properties.userType | is_not_null | excludes:properties.userType | |
properties.userType | ne | member | excludes:properties.userType field:"properties.userType" value:"member" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
properties.authenticationProtocol | eq |
| field:"properties.authenticationProtocol" kind:eq value:"ropc" |
properties.authenticationRequirement | eq |
| field:"properties.authenticationRequirement" kind:eq value:"singlefactorauthentication" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
userPrincipalName | properties.userPrincipalName |
ipAddress | properties.ipAddress |
appDisplayName | properties.appDisplayName |
Response runbook
1. Query Azure.Audit logs for all ROPC authentication attempts by properties:userPrincipalName in the 24 hours surrounding this event to determine if this represents isolated testing or part of a broader password spraying or credential enumeration campaign with multiple failed attempts across different accounts
2. Identify the application using ROPC by reviewing properties:appDisplayName and properties:appId, and verify with application owners whether this application has a legitimate business requirement for ROPC authentication or if it should be migrated to modern OAuth flows with interactive authentication
3. Review the source IP address callerIpAddress and user agent properties:userAgent for indicators of automated attack tools (e.g., python-requests, curl, TeamFiltration signatures), check if the IP is associated with known malicious infrastructure, and if suspicious activity is confirmed, block the application from using ROPC through Azure AD application policies and force password reset for the affected user
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"Level": "4",
"callerIpAddress": "2.2.2.2",
"category": "SignInLogs",
"correlationId": "ropc-signin-001",
"durationMs": 0,
"operationName": "Sign-in activity",
"operationVersion": "1.0",
"p_event_time": "2025-01-15 09:30:25.123",
"p_log_type": "Azure.Audit",
"properties": {
"appDisplayName": "Legacy Application",
"appId": "app-legacy-456",
"authenticationDetails": [
{
"authenticationMethod": "Password",
"authenticationStepDateTime": "2025-01-15T09:30:25.1234567Z"
}
],
"authenticationProtocol": "ropc",
"authenticationRequirement": "singleFactorAuthentication",
"clientAppUsed": "Other clients",
"conditionalAccessStatus": "notApplied",
"createdDateTime": "2025-01-15T09:30:25.1234567Z",
"deviceDetail": {
"browser": "Unknown",
"operatingSystem": "Unknown"
},
"ipAddress": "2.2.2.2",
"isInteractive": false,
"resourceDisplayName": "Microsoft Graph",
"resourceId": "00000003-0000-0000-c000-111111111111",
"userAgent": "python-requests/2.28.1",
"userId": "user-123",
"userPrincipalName": "gandalf@lotr.com",
"userType": "Member"
},
"resourceId": "/tenants/tenant-123/providers/Microsoft.aadiam",
"resultSignature": "SUCCESS",
"resultType": "0",
"tenantId": "tenant-123",
"time": "2025-01-15 09:30:25.123"
}
Azure Serverless Script Execution
#Detects when serverless resources execute PowerShell or Python scripts through Azure Automation runbook jobs or Azure Function Apps. Adversaries may abuse access to serverless resources to execute commands with inherited permissions from managed identities, RunAs accounts, or hybrid worker groups.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Execution |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
# AZT302.1/2/3 - Automation Account Runbook Job Execution
RUNBOOK_JOB_WRITE = "MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/JOBS/WRITE"
# AZT302.4 - Function Application Execution
FUNCTION_APP_ACTION = "MICROSOFT.WEB/SITES/HOSTRUNTIME/HOST/ACTION"
AZT302_OPERATIONS = [
RUNBOOK_JOB_WRITE,
FUNCTION_APP_ACTION,
]
def rule(event):
operation = event.get("operationName", "").upper()
return all([operation in AZT302_OPERATIONS, azure_activity_success(event)])
def title(event):
resource_id = event.get("resourceId", "<UNKNOWN_RESOURCE>")
operation = event.get("operationName", "").upper()
if operation == RUNBOOK_JOB_WRITE:
technique = "Automation Runbook Job"
resource_name = extract_resource_name_from_id(
resource_id, "automationAccounts", default="<UNKNOWN_ACCOUNT>"
)
elif operation == FUNCTION_APP_ACTION:
technique = "Function App"
resource_name = extract_resource_name_from_id(resource_id, "sites", default="<UNKNOWN_APP>")
else:
technique = "Serverless"
resource_name = "<UNKNOWN_RESOURCE>"
return f"Azure {technique} Execution detected on [{resource_name}]"
def alert_context(event):
context = azure_activity_alert_context(event)
operation = event.get("operationName", "").upper()
resource_id = event.get("resourceId", "")
if operation == RUNBOOK_JOB_WRITE:
context["resource_type"] = "Automation Runbook Job"
automation_account = extract_resource_name_from_id(
resource_id, "automationAccounts", default=""
)
if automation_account:
context["automation_account"] = automation_account
runbook_name = extract_resource_name_from_id(resource_id, "runbooks", default="")
if runbook_name:
context["runbook_name"] = runbook_name
job_id = extract_resource_name_from_id(resource_id, "jobs", default="")
if job_id:
context["job_id"] = job_id
elif operation == FUNCTION_APP_ACTION:
context["resource_type"] = "Function App"
function_app_name = extract_resource_name_from_id(resource_id, "sites", default="")
if function_app_name:
context["function_app_name"] = function_app_name
return context
Rule specification
AnalysisType: rule
Filename: azure_serverless_execution.py
RuleID: "Azure.MonitorActivity.Automation.ServerlessExecution"
DisplayName: "Azure Serverless Script Execution"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Info
Description: >
Detects when serverless resources execute PowerShell or Python scripts through Azure Automation
runbook jobs or Azure Function Apps. Adversaries may abuse access to serverless resources to
execute commands with inherited permissions from managed identities, RunAs accounts, or hybrid
worker groups.
Reports:
MITRE ATT&CK:
- TA0002:T1059 # Execution: Command and Scripting Interpreter
- TA0002:T1651 # Execution: Cloud Administration Command
Tags:
- AZT404
- AZT404.3
- AZT302
- AZT302.1
- AZT302.2
- AZT302.3
- AZT302.4
- Execution
- Command and Scripting Interpreter
- Cloud Administration Command
Runbook: |
1. Query Azure Monitor Activity logs for all automation and function app execution operations by the callerIpAddress in the 24 hours before and after the alert to identify patterns of serverless execution
2. Find all runbook or function app creation/modification activities by the same callerIpAddress in the 6 hours before the alert to determine if this is part of a setup-then-execute attack pattern
3. Check if the callerIpAddress has executed runbook jobs or function apps in the past 90 days to establish if this is typical administrative activity or anomalous behavior
Reference: https://microsoft.github.io/Azure-Threat-Research-Matrix/Execution/AZT302/AZT302/
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameis one ofMICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/JOBS/WRITE,MICROSOFT.WEB/SITES/HOSTRUNTIME/HOST/ACTIONresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | in |
| field:"operationName" kind:in |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all automation and function app execution operations by the callerIpAddress in the 24 hours before and after the alert to identify patterns of serverless execution
2. Find all runbook or function app creation/modification activities by the same callerIpAddress in the 6 hours before the alert to determine if this is part of a setup-then-execute attack pattern
3. Check if the callerIpAddress has executed runbook jobs or function apps in the past 90 days to establish if this is typical administrative activity or anomalous behavior
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Informational",
"location": "eastus",
"operationName": "MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/JOBS/WRITE",
"operationVersion": "2021-06-22",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/automation-rg/providers/Microsoft.Automation/automationAccounts/MyAutomationAccount/jobs/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-22T10:30:00.0000000Z"
}
Azure Service Principal Credentials Added
#Detects when new credentials (client secrets or certificates) are added to Microsoft Entra ID service principals or applications. Service principals are identities used by applications, services, and automation tools to access Azure resources, and they authenticate using credentials such as client secrets or certificates. Adversaries who compromise administrative credentials may add rogue credentials to existing service principals to establish persistent access that bypasses multi-factor authentication (MFA) requirements, as service principal authentication uses client credentials rather than interactive user login.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Detection logic
CREDENTIAL_OPERATION = "add service principal credentials"
def rule(event):
# Check for service principal credential addition operations
operation_name = event.get("operationName", "").lower()
activity_display_name = event.deep_get("properties", "activityDisplayName", default="").lower()
return CREDENTIAL_OPERATION in operation_name or CREDENTIAL_OPERATION in activity_display_name
def title(event):
actor = event.deep_get(
"properties", "initiatedBy", "user", "userPrincipalName", default="<UNKNOWN_ACTOR>"
)
# Get service principal name from target resources
service_principal = "<UNKNOWN_SP>"
target_resources = event.deep_get("properties", "targetResources", default=[])
for resource in target_resources or []:
if resource.get("type") in ["ServicePrincipal", "Application"]:
service_principal = resource.get("displayName", service_principal)
break
return (
f"Service Principal Credentials Added: [{actor}] added credentials "
f"to service principal [{service_principal}]"
)
def alert_context(event):
# Build context for audit logs (not sign-in logs)
context = {
"tenantId": event.get("tenantId", "<NO_TENANTID>"),
"operation_name": event.get("operationName", "<NO_OPERATION>"),
"activity_display_name": event.deep_get(
"properties", "activityDisplayName", default="<NO_ACTIVITY>"
),
"category": event.get("category", "<NO_CATEGORY>"),
"result": event.deep_get("properties", "result", default="<NO_RESULT>"),
"actor_user": event.deep_get(
"properties", "initiatedBy", "user", "userPrincipalName", default="<NO_ACTOR>"
),
"initiator_user_id": event.deep_get(
"properties", "initiatedBy", "user", "id", default="<NO_USER_ID>"
),
"initiator_display_name": event.deep_get(
"properties", "initiatedBy", "user", "displayName", default="<NO_DISPLAY_NAME>"
),
"source_ip": event.deep_get(
"properties", "initiatedBy", "user", "ipAddress", default="<NO_IP>"
),
}
# Add target service principal details
target_resources = event.deep_get("properties", "targetResources", default=[])
service_principals = []
for resource in target_resources or []:
if resource.get("type") in ["ServicePrincipal", "Application"]:
sp_info = {
"id": resource.get("id", ""),
"displayName": resource.get("displayName", ""),
"type": resource.get("type"),
}
# Extract credential details from modified properties
credential_info = [
{"property": prop.get("displayName"), "new_value": prop.get("newValue", "")}
for prop in resource.get("modifiedProperties", [])
if prop.get("displayName") in ["KeyDescription", "KeyType", "KeyUsage"]
]
if credential_info:
sp_info["credential_details"] = credential_info
service_principals.append(sp_info)
if service_principals:
context["target_service_principals"] = service_principals
return context
Rule specification
AnalysisType: rule
Filename: azure_service_principal_credentials_added.py
RuleID: "Azure.Audit.ServicePrincipalCredentialsAdded"
DisplayName: "Azure Service Principal Credentials Added"
Enabled: true
LogTypes:
- Azure.Audit
Severity: Medium
Description: >
Detects when new credentials (client secrets or certificates) are added to Microsoft Entra ID
service principals or applications. Service principals are identities used by applications, services,
and automation tools to access Azure resources, and they authenticate using credentials such as client
secrets or certificates. Adversaries who compromise administrative credentials may add rogue credentials
to existing service principals to establish persistent access that bypasses multi-factor authentication
(MFA) requirements, as service principal authentication uses client credentials rather than interactive
user login.
Reports:
MITRE ATT&CK:
- TA0003:T1098
- TA0003:T1098.001
Runbook: |
1. Query Azure.Audit logs for all operations performed by properties:initiatedBy:user:userPrincipalName in the 7 days before and after this credential addition to determine if this was part of a broader compromise campaign involving multiple service principal modifications or privilege escalations
2. Verify with the administrator whether this credential addition was authorized through your organization's change management process and review the credential type (client secret vs certificate), expiration date, and whether it aligns with standard security practices for the affected service principal
3. Query Azure sign-in logs and API access logs for authentication activity using the affected service principal (properties:targetResources:id) in the 24 hours after credential addition to identify any suspicious resource access, data exfiltration, or privilege abuse that may indicate the credential is being misused by an attacker
Reference: https://github.com/elastic/detection-rules/blob/main/rules/integrations/azure/persistence_entra_id_service_principal_credentials_added.toml
SummaryAttributes:
- properties:initiatedBy:user:userPrincipalName
- properties:targetResources:displayName
- properties:targetResources:type
Stages and Predicates
Fires on Azure.Audit events when any of the conditions below holds.
Condition
any of:
operationNamecontainsadd service principal credentialsproperties.activityDisplayNamecontainsadd service principal credentials
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | contains |
| field:"operationName" kind:contains value:"add service principal credentials" |
properties.activityDisplayName | contains |
| field:"properties.activityDisplayName" kind:contains value:"add service principal credentials" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
tenantId | |
operation_name | operationName |
activity_display_name | properties.activityDisplayName |
category | |
result | properties.result |
actor_user | properties.initiatedBy.user.userPrincipalName |
initiator_user_id | properties.initiatedBy.user.id |
initiator_display_name | properties.initiatedBy.user.displayName |
source_ip | properties.initiatedBy.user.ipAddress |
Response runbook
1. Query Azure.Audit logs for all operations performed by properties:initiatedBy:user:userPrincipalName in the 7 days before and after this credential addition to determine if this was part of a broader compromise campaign involving multiple service principal modifications or privilege escalations
2. Verify with the administrator whether this credential addition was authorized through your organization's change management process and review the credential type (client secret vs certificate), expiration date, and whether it aligns with standard security practices for the affected service principal
3. Query Azure sign-in logs and API access logs for authentication activity using the affected service principal (properties:targetResources:id) in the 24 hours after credential addition to identify any suspicious resource access, data exfiltration, or privilege abuse that may indicate the credential is being misused by an attacker
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"Level": "4",
"callerIpAddress": "2.2.2.2",
"category": "ApplicationManagement",
"correlationId": "sp-creds-001",
"durationMs": 0,
"operationName": "Add service principal credentials",
"operationVersion": "1.0",
"p_event_time": "2025-01-15 09:30:20.123",
"p_log_type": "Azure.Audit",
"properties": {
"activityDateTime": "2025-01-15T09:30:20.1234567Z",
"activityDisplayName": "Add service principal credentials",
"initiatedBy": {
"user": {
"displayName": "IT Administrator",
"id": "admin-123",
"ipAddress": "2.2.2.2",
"userPrincipalName": "frodo@lotr.com"
}
},
"loggedByService": "Core Directory",
"operationName": "Add service principal credentials",
"operationType": "Update",
"result": "success",
"targetResources": [
{
"displayName": "Production API Service",
"id": "sp-app-456",
"modifiedProperties": [
{
"displayName": "KeyDescription",
"newValue": "\"Client Secret for API Access\"",
"oldValue": null
},
{
"displayName": "KeyType",
"newValue": "\"Password\"",
"oldValue": null
}
],
"type": "ServicePrincipal"
}
]
},
"resourceId": "/tenants/tenant-123/providers/Microsoft.aadiam",
"resultSignature": "None",
"tenantId": "tenant-123",
"time": "2025-01-15 09:30:20.123"
}
Azure SignIn via Legacy Authentication Protocol
#This detection looks for Successful Logins that have used legacy authentication protocols
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- [Entra ID] Suspicious Continuous OAuth Token Usage (Kusto)
- Anomalous sign-in location by user account and authenticating application (Kusto)
- Anomalous Single Factor Signin (Kusto)
- Authentications of Privileged Accounts Outside of Expected Controls (Kusto)
- Azure Portal sign in from another Azure Tenant (Kusto)
- Azure Service Principal Sign-In Followed by Arc Cluster Credential Access (Elastic)
- Cisco - firewall block but success logon to Microsoft Entra ID (Kusto)
- Detect non-admin requesting token for admin applications (Kusto)
Detection logic
import json
from unittest.mock import MagicMock
from panther_azuresignin_helpers import actor_user, azure_signin_alert_context, is_sign_in_event
LEGACY_AUTH_USERAGENTS = ["BAV2ROPC", "CBAInPROD"] # CBAInPROD is reported to be IMAP
# Add ServicePrincipalName/UserPrincipalName to
# KNOWN_EXCEPTIONS to prevent these Principals from Alerting
KNOWN_EXCEPTIONS = []
def rule(event):
if not is_sign_in_event(event):
return False
global KNOWN_EXCEPTIONS # pylint: disable=global-statement
if isinstance(KNOWN_EXCEPTIONS, MagicMock):
KNOWN_EXCEPTIONS = json.loads(KNOWN_EXCEPTIONS()) # pylint: disable=not-callable
if actor_user(event) in KNOWN_EXCEPTIONS:
return False
user_agent = event.deep_get("properties", "userAgent", default="")
error_code = event.deep_get("properties", "status", "errorCode", default=0)
return all([user_agent in LEGACY_AUTH_USERAGENTS, error_code == 0])
def title(event):
principal = actor_user(event)
if principal is None:
principal = "<NO_PRINCIPALNAME>"
return f"AzureSignIn: Principal [{principal}] authenticated with a legacy auth protocol"
def dedup(event):
principal = actor_user(event)
if principal is None:
principal = "<NO_PRINCIPALNAME>"
return principal
def alert_context(event):
a_c = azure_signin_alert_context(event)
a_c["userAgent"] = event.deep_get("properties", "userAgent", "<NO_USERAGENT>")
return a_c
Rule specification
AnalysisType: rule
Filename: azure_legacyauth.py
RuleID: "Azure.Audit.LegacyAuth"
DisplayName: "Azure SignIn via Legacy Authentication Protocol"
Enabled: true
Threshold: 1
DedupPeriodMinutes: 10
LogTypes:
- Azure.Audit
Severity: Medium
Description: >
This detection looks for Successful Logins that have used legacy authentication protocols
Reference: https://learn.microsoft.com/en-us/azure/active-directory/reports-monitoring/workbook-legacy-authentication
Runbook: >
Based on Microsoft's analysis more than 97 percent of credential stuffing attacks use legacy authentication and more than 99 percent
of password spray attacks use legacy authentication protocols. These attacks would stop with basic authentication disabled or blocked.
see https://learn.microsoft.com/en-us/azure/active-directory/conditional-access/block-legacy-authentication
If you are aware of this Legacy Auth need, and need to continue using this mechanism, add the principal name to KNOWN_EXCEPTIONS.
The Reference link contains additional material hosted on Microsoft.com
SummaryAttributes:
- properties:ServicePrincipalName
- properties:UserPrincipalName
- properties:ipAddress
Stages and Predicates
Fires on Azure.Audit events when all of the conditions below hold.
Condition
operationNameisSign-in activityproperties.userAgentis one ofBAV2ROPC,CBAInPRODproperties.status.errorCodeis0
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"Sign-in activity" |
properties.status.errorCode | eq |
| field:"properties.status.errorCode" kind:eq value:"0" |
properties.userAgent | in |
| field:"properties.userAgent" kind:in |
Response runbook
Based on Microsoft's analysis more than 97 percent of credential stuffing attacks use legacy authentication and more than 99 percent of password spray attacks use legacy authentication protocols. These attacks would stop with basic authentication disabled or blocked. see https://learn.microsoft.com/en-us/azure/active-directory/conditional-access/block-legacy-authentication
If you are aware of this Legacy Auth need, and need to continue using this mechanism, add the principal name to KNOWN_EXCEPTIONS. The Reference link contains additional material hosted on Microsoft.com
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"Level": "4",
"calleripaddress": "1.2.3.4",
"category": "SignInLogs",
"correlationid": "1043d312-230b-4f85-89de-2438df7ad2e9",
"durationms": 0,
"identity": "Some Identity",
"location": "US",
"operationname": "Sign-in activity",
"operationversion": 1,
"p_event_time": "2023-07-21 05:05:12.056",
"p_log_type": "Azure.Audit",
"properties": {
"appDisplayName": "Office 365 Exchange Online",
"appId": "975c4feb-8c6b-4ed8-82e2-f0952ecc64d4",
"appliedConditionalAccessPolicies": [],
"authenticationContextClassReferences": [],
"authenticationDetails": [
{
"authenticationMethod": "Password",
"authenticationMethodDetail": "Password in the cloud",
"authenticationStepDateTime": "2023-01-11T11:11:11",
"authenticationStepRequirement": "",
"authenticationStepResultDetail": "Correct password",
"succeeded": true
}
],
"authenticationProcessingDetails": [],
"authenticationProtocol": "rpoc",
"authenticationRequirement": "singleFactorAuthentication",
"authenticationRequirementPolicies": [],
"authenticationStrengths": [],
"autonomousSystemNumber": 1234,
"clientAppUsed": "Authenticated SMTP",
"clientCredentialType": "clientAssertion",
"conditionalAccessStatus": "notApplied",
"correlationId": "9d5f7dd3-46a7-475a-9cc3-96160551ce49",
"createdDateTime": "2023-07-21 05:03:52.160562400",
"crossTenantAccessType": "none",
"deviceDetail": {
"deviceId": "",
"displayName": "",
"operatingSystem": "MacOs"
},
"flaggedForReview": false,
"homeTenantId": "4328f0a8-06da-4457-b0d0-2c8e115e52cc",
"id": "d9ae0e20-c959-42a3-ba74-00f59ac9f6c6",
"incomingTokenType": "none",
"ipAddress": "12.12.12.12",
"isInteractive": true,
"isTenantRestricted": false,
"location": {
"city": "Springfield",
"countryOrRegion": "US",
"geoCoordinates": {
"latitude": 34.55555555555555,
"longitude": -74.4444444444444
},
"state": "Virginia"
},
"mfaDetail": {},
"networkLocationDetails": [],
"originalRequestId": "51755ad1-bc0d-4694-acb2-a21f054869ab",
"privateLinkDetails": {},
"processingTimeInMilliseconds": 343,
"resourceDisplayName": "Office 365 Exchange Online",
"resourceId": "b54c474b-4d74-4920-b0c3-7a8d2f2c4e95",
"resourceServicePrincipalId": "d8f787f8-7f22-40d4-b057-6fc59c49ca43",
"resourceTenantId": "fc66a38e-bcba-4fe9-b2de-c67918514cc5",
"riskDetail": "none",
"riskEventTypes": [],
"riskEventTypes_v2": [],
"riskLevelAggregated": "none",
"riskLevelDuringSignIn": "none",
"riskState": "none",
"rngcStatus": 0,
"servicePrincipalId": "",
"sessionLifetimePolicies": [],
"ssoExtensionVersion": "",
"status": {
"additionalDetails": "MFA requirement satisfied by claim in the token",
"errorCode": 0
},
"tenantId": "237c496d-1ca2-4b13-aa0f-69e44d745a27",
"tokenIssuerName": "",
"tokenIssuerType": "AzureAD",
"uniqueTokenIdentifier": "hhhhhhhhhhhhhhhhhhhhhh",
"userAgent": "BAV2ROPC",
"userDisplayName": "A User Display Name",
"userId": "8105c513-d24b-4b8d-9035-f8d48854d703",
"userPrincipalName": "eve@lexcorp.com",
"userType": "Member"
},
"resourceid": "/tenants/ef3a1b35-97d6-474b-a9de-94d21d4ee71b/providers/Microsoft.aadiam",
"resultsignature": "None",
"resulttype": 0,
"tenantid": "c5fd010a-2972-4be2-9c67-986ceca2a042",
"time": "2023-07-21 05:05:12.056"
}
Azure SQL Server Deleted
#Detects when an Azure SQL Server is deleted. SQL Server deletion is a destructive operation that removes the entire database server instance and all databases within it.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | Activity Log event Microsoft.Sql/servers/delete: Deletes an existing server. |
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
SQL_SERVER_DELETE = "MICROSOFT.SQL/SERVERS/DELETE"
def rule(event):
return event.get("operationName", "").upper() == SQL_SERVER_DELETE and azure_activity_success(
event
)
def title(event):
resource_id = event.get("resourceId", "")
sql_server = extract_resource_name_from_id(
resource_id, "servers", default="<UNKNOWN_SQL_SERVER>"
)
return f"Azure SQL Server deleted [{sql_server}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
resource_group = extract_resource_name_from_id(resource_id, "resourceGroups", default="")
if resource_group:
context["resource_group"] = resource_group
return context
Rule specification
AnalysisType: rule
Filename: azure_sql_server_deleted.py
RuleID: "Azure.MonitorActivity.SQL.ServerDeleted"
DisplayName: "Azure SQL Server Deleted"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Info
Description: >
Detects when an Azure SQL Server is deleted.
SQL Server deletion is a destructive operation that removes the entire database server instance and all databases within it.
Reports:
MITRE ATT&CK:
- TA0040:T1485 # Impact: Data Destruction
- TA0040:T1490 # Impact: Inhibit System Recovery
Tags:
- Impact
- Data Destruction
- Inhibit System Recovery
Runbook: |
1. Query Azure MonitorActivity logs for all SQL Server delete operations by the callerIpAddress in the 24 hours before and after this alert to identify if multiple servers are being deleted
2. Check if the callerIpAddress is associated with known cloud providers, VPN services, or threat intelligence indicators
3. Search for other Azure resource deletion operations from the same callerIpAddress in the past 7 days to determine the scope of data destruction activity
Reference: https://docs.datadoghq.com/security/default_rules/ab7-bv8-6bt/
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.SQL/SERVERS/DELETEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.SQL/SERVERS/DELETE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure MonitorActivity logs for all SQL Server delete operations by the callerIpAddress in the 24 hours before and after this alert to identify if multiple servers are being deleted
2. Check if the callerIpAddress is associated with known cloud providers, VPN services, or threat intelligence indicators
3. Search for other Azure resource deletion operations from the same callerIpAddress in the past 7 days to determine the scope of data destruction activity
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"location": "",
"operationName": "Microsoft.SQL/servers/delete",
"operationVersion": "2021-04-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.SQL/servers/mysqlserver",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Storage Account Blob Versioning Disabled
#Detects when Azure storage account blob versioning is disabled. Disabling versioning removes protection against accidental deletion or modification and may indicate preparation for permanent data destruction.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
azure_parse_json_string,
extract_resource_name_from_id,
)
BLOB_SERVICES_WRITE = "MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/WRITE"
def rule(event):
requestbody = azure_parse_json_string(event.deep_get("properties", "requestbody", default=None))
return all(
[
event.get("operationName", "").upper() == BLOB_SERVICES_WRITE,
requestbody.get("properties", {}).get("isVersioningEnabled") is False,
azure_activity_success(event),
]
)
def title(event):
resource_id = event.get("resourceId", "")
storage_account = extract_resource_name_from_id(
resource_id, "storageAccounts", default="<UNKNOWN_ACCOUNT>"
)
return f"Azure Storage Account blob versioning disabled on [{storage_account}]"
def alert_context(event):
context = azure_activity_alert_context(event)
return context
Rule specification
AnalysisType: rule
Filename: azure_storage_account_versioning_disabled.py
RuleID: "Azure.MonitorActivity.StorageAccount.VersioningDisabled"
DisplayName: "Azure Storage Account Blob Versioning Disabled"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: High
Description: >
Detects when Azure storage account blob versioning is disabled.
Disabling versioning removes protection against accidental deletion or modification and may indicate preparation for permanent data destruction.
Reports:
MITRE ATT&CK:
- TA0040:T1485 # Impact: Data Destruction
Tags:
- Impact
- Data Destruction
Runbook: |
1. Query Azure Monitor Activity logs for all storage account blob service operations by the callerIpAddress in the 6 hours before and after this alert to identify patterns
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure storage account configuration changes or blob deletion events from the same user or IP in the past 7 days to assess if this is part of a data destruction campaign
Reference: https://www.azadvertizer.net/azpolicyadvertizer/c36a325b-ae04-4863-ad4f-19c6678f8e08.html
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/WRITEresultTypeis one ofSuccess,Succeeded
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/WRITE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all storage account blob service operations by the callerIpAddress in the 6 hours before and after this alert to identify patterns
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure storage account configuration changes or blob deletion events from the same user or IP in the past 7 days to assess if this is part of a data destruction campaign
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"durationMs": 1523,
"identity": {
"claims": {
"name": "denethor@lotr.com"
}
},
"level": "Informational",
"location": "eastus",
"operationName": "Microsoft.Storage/storageAccounts/blobServices/write",
"operationVersion": "2021-04-01",
"properties": {
"requestbody": "{\"properties\":{\"isVersioningEnabled\":false}}"
},
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.Storage/storageAccounts/mystorageaccount/blobServices/default",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Storage Account Deleted
#Detects when an Azure storage account is deleted. Storage account deletion is a destructive operation that may indicate ransomware activity or malicious data destruction.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | Activity Log event Microsoft.Storage/storageAccounts/delete: Deletes an existing storage account. |
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
STORAGE_ACCOUNT_DELETE = "MICROSOFT.STORAGE/STORAGEACCOUNTS/DELETE"
def rule(event):
return event.get(
"operationName", ""
).upper() == STORAGE_ACCOUNT_DELETE and azure_activity_success(event)
def title(event):
resource_id = event.get("resourceId", "")
storage_account = extract_resource_name_from_id(
resource_id, "storageAccounts", default="<UNKNOWN_ACCOUNT>"
)
return f"Azure Storage Account [{storage_account}] deleted"
def alert_context(event):
context = azure_activity_alert_context(event)
return context
Rule specification
AnalysisType: rule
Filename: azure_storage_account_deleted.py
RuleID: "Azure.MonitorActivity.StorageAccount.Deleted"
DisplayName: "Azure Storage Account Deleted"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: High
Description: >
Detects when an Azure storage account is deleted.
Storage account deletion is a destructive operation that may indicate ransomware activity or malicious data destruction.
Reports:
MITRE ATT&CK:
- TA0040:T1485 # Impact: Data Destruction
- TA0040:T1490 # Impact: Inhibit System Recovery
Tags:
- Impact
- Data Destruction
- Inhibit System Recovery
- Ransomware
Runbook: |
1. Query Azure Monitor Activity logs for all storage account operations by the callerIpAddress in the 24 hours before this alert to establish if this is part of a larger attack pattern
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure resource deletions or destructive operations from the same user or IP in the past 7 days to assess the scope of potential data destruction
Reference: https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActivityLog/delete-storage-account-alert.html
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.STORAGE/STORAGEACCOUNTS/DELETEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/DELETE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all storage account operations by the callerIpAddress in the 24 hours before this alert to establish if this is part of a larger attack pattern
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure resource deletions or destructive operations from the same user or IP in the past 7 days to assess the scope of potential data destruction
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"durationMs": 1523,
"level": "Informational",
"location": "eastus",
"operationName": "Microsoft.Storage/storageAccounts/delete",
"operationVersion": "2021-04-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.Storage/storageAccounts/mystorageaccount",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Storage Account HTTPS-Only Traffic Disabled
#Detects when Azure storage account HTTPS-only traffic requirement is disabled. Disabling HTTPS-only allows unencrypted HTTP connections, which is a security downgrade that may expose data in transit.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
azure_parse_json_string,
extract_resource_name_from_id,
)
STORAGE_ACCOUNT_WRITE = "MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE"
def rule(event):
requestbody = azure_parse_json_string(event.deep_get("properties", "requestbody", default=None))
return all(
[
event.get("operationName", "").upper() == STORAGE_ACCOUNT_WRITE,
requestbody.get("properties", {}).get("supportsHttpsTrafficOnly") is False,
requestbody.get("location") is None,
azure_activity_success(event),
]
)
def title(event):
resource_id = event.get("resourceId", "")
storage_account = extract_resource_name_from_id(
resource_id, "storageAccounts", default="<UNKNOWN_ACCOUNT>"
)
return f"Azure Storage Account HTTPS-only traffic disabled on [{storage_account}]"
def alert_context(event):
context = azure_activity_alert_context(event)
return context
Rule specification
AnalysisType: rule
Filename: azure_storage_https_only_disabled.py
RuleID: "Azure.MonitorActivity.StorageAccount.HttpsOnlyDisabled"
DisplayName: "Azure Storage Account HTTPS-Only Traffic Disabled"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: High
Description: >
Detects when Azure storage account HTTPS-only traffic requirement is disabled.
Disabling HTTPS-only allows unencrypted HTTP connections, which is a security downgrade that may expose data in transit.
Reports:
MITRE ATT&CK:
- TA0009:T1071 # Command and Control: Application Layer Protocol
Tags:
- Command and Control
- Application Layer Protocol
Runbook: |
1. Query Azure Monitor Activity logs for all storage account operations by the callerIpAddress in the 6 hours before and after this alert to identify patterns
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure storage account security downgrades or suspicious configuration changes from the same user or IP in the past 7 days
Reference: https://learn.microsoft.com/en-us/azure/storage/common/storage-require-secure-transfer
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.STORAGE/STORAGEACCOUNTS/WRITEresultTypeis one ofSuccess,Succeeded
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all storage account operations by the callerIpAddress in the 6 hours before and after this alert to identify patterns
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure storage account security downgrades or suspicious configuration changes from the same user or IP in the past 7 days
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"operationName": "Microsoft.Storage/storageAccounts/write",
"operationVersion": "2021-04-01",
"properties": {
"requestbody": "{\"properties\":{\"supportsHttpsTrafficOnly\":false}}"
},
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.Storage/storageAccounts/mystorageaccount",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Storage Account Key Regenerated
#Detects when an Azure storage account access key is regenerated. Key regeneration is a normal operational activity but may indicate an attacker attempting to maintain persistence or rotate credentials after compromise.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
azure_parse_json_string,
extract_resource_name_from_id,
)
REGENERATE_KEY = "MICROSOFT.STORAGE/STORAGEACCOUNTS/REGENERATEKEY/ACTION"
def rule(event):
return event.get("operationName", "").upper() == REGENERATE_KEY and azure_activity_success(
event
)
def title(event):
resource_id = event.get("resourceId", "")
storage_account = extract_resource_name_from_id(
resource_id, "storageAccounts", default="<UNKNOWN_ACCOUNT>"
)
requestbody = azure_parse_json_string(event.deep_get("properties", "requestbody", default=None))
key = requestbody.get("keyName", "<UNKNOWN_KEY>")
return f"Azure Storage Account key [{key}] regenerated on [{storage_account}]"
def alert_context(event):
context = azure_activity_alert_context(event)
requestbody = azure_parse_json_string(event.deep_get("properties", "requestbody", default=None))
context["key_name"] = requestbody.get("keyName", "<UNKNOWN_KEY>")
return context
Rule specification
AnalysisType: rule
Filename: azure_storage_account_key_regenerated.py
RuleID: "Azure.MonitorActivity.StorageAccount.KeyRegenerated"
DisplayName: "Azure Storage Account Key Regenerated"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Info
Description: >
Detects when an Azure storage account access key is regenerated.
Key regeneration is a normal operational activity but may indicate an attacker attempting to maintain persistence or rotate credentials after compromise.
Reports:
MITRE ATT&CK:
- TA0006:T1098 # Persistence: Account Manipulation
Tags:
- Persistence
- Account Manipulation
Runbook: |
1. Query Azure Monitor Activity logs for all storage account operations by the callerIpAddress in the 24 hours before and after this alert to identify suspicious patterns
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure storage account modifications, key regenerations, or access events from the same user or IP in the past 7 days to assess if this is part of unauthorized activity
Reference: https://www.elastic.co/guide/en/security/8.19/azure-storage-account-key-regenerated.html
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.STORAGE/STORAGEACCOUNTS/REGENERATEKEY/ACTIONresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/REGENERATEKEY/ACTION" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all storage account operations by the callerIpAddress in the 24 hours before and after this alert to identify suspicious patterns
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure storage account modifications, key regenerations, or access events from the same user or IP in the past 7 days to assess if this is part of unauthorized activity
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"durationMs": 523,
"level": "Informational",
"location": "eastus",
"operationName": "Microsoft.Storage/storageAccounts/regenerateKey/action",
"operationVersion": "2021-04-01",
"properties": {
"requestbody": {
"keyName": "key1"
}
},
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.Storage/storageAccounts/mystorageaccount",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Storage Account Keys Listed
#Detects when Azure Storage Account access keys are listed or retrieved. This operation returns the full access keys which could grant complete control over the storage account and all its data. Adversaries may list storage account keys to gain persistent access to blob containers, file shares, queues, and tables without needing to maintain their current permissions.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access | |
| Collection |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
KEY_LIST_OPERATIONS = [
"MICROSOFT.STORAGE/STORAGEACCOUNTS/LISTKEYS/ACTION",
]
def rule(event):
return event.get("operationName", "").upper() in KEY_LIST_OPERATIONS and azure_activity_success(
event
)
def title(event):
resource_id = event.get("resourceId", "")
storage_account_name = extract_resource_name_from_id(
resource_id, "storageAccounts", default="<UNKNOWN_STORAGE_ACCOUNT>"
)
return f"Azure Storage Account Keys Listed on [{storage_account_name}]"
def alert_context(event):
context = azure_activity_alert_context(event)
return context
Rule specification
AnalysisType: rule
Filename: azure_storage_account_keys_listed.py
RuleID: "Azure.MonitorActivity.StorageAccount.KeysListed"
DisplayName: "Azure Storage Account Keys Listed"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Medium
Threshold: 50
Status: Experimental
Description: >
Detects when Azure Storage Account access keys are listed or retrieved. This operation returns
the full access keys which could grant complete control over the storage account and all its data.
Adversaries may list storage account keys to gain persistent access to blob containers, file shares,
queues, and tables without needing to maintain their current permissions.
Reports:
MITRE ATT&CK:
- TA0006:T1552 # Credential Access: Unsecured Credentials
- TA0009:T1530 # Collection: Data from Cloud Storage
Tags:
- Credential Access
- Unsecured Credentials
- Collection
- Data from Cloud Storage
- AZT605
- AZT605.1
- AZT701.2
- Resource Secret Reveal
- Storage Account Access Key Dumping
- Automation Account Credential Secret Dump
Runbook: |
1. Query Azure Monitor Activity logs for all storage account operations on the same resourceId in the 4 hours after this key listing to identify blob downloads, container modifications, or SAS token generation that may indicate data exfiltration
2. Review Azure AD audit logs for the caller identity in the 24 hours before this operation to check if they recently obtained new role assignments or elevated privileges using correlationId
3. Check the storage account's diagnostic logs for data plane operations from callerIpAddress in the 6 hours after the key listing to identify unusual access patterns or bulk downloads
Reference: https://microsoft.github.io/Azure-Threat-Research-Matrix/CredentialAccess/AZT605/AZT605-1
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameis one ofMICROSOFT.STORAGE/STORAGEACCOUNTS/LISTKEYS/ACTIONresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | in |
| field:"operationName" kind:in value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/LISTKEYS/ACTION" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all storage account operations on the same resourceId in the 4 hours after this key listing to identify blob downloads, container modifications, or SAS token generation that may indicate data exfiltration
2. Review Azure AD audit logs for the caller identity in the 24 hours before this operation to check if they recently obtained new role assignments or elevated privileges using correlationId
3. Check the storage account's diagnostic logs for data plane operations from callerIpAddress in the 6 hours after the key listing to identify unusual access patterns or bulk downloads
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"identity": {
"claims": {
"ipaddr": "1.1.1.1",
"name": "denethor@lotr.com"
}
},
"location": "eastus",
"operationName": "Microsoft.Storage/storageAccounts/listkeys/action",
"operationVersion": "2021-04-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/prod-rg/providers/Microsoft.Storage/storageAccounts/prodstorage123",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-24T10:30:00.0000000Z"
}
Azure Storage Account Public Network Access Enabled
#Detects when an existing Azure storage account's network settings are modified to enable public network access. This could indicate a potential data exfiltration risk or misconfiguration.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
azure_parse_json_string,
extract_resource_name_from_id,
)
STORAGE_ACCOUNT_WRITE = "MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE"
def rule(event):
requestbody = azure_parse_json_string(event.deep_get("properties", "requestbody", default=None))
return all(
[
event.get("operationName", "").upper() == STORAGE_ACCOUNT_WRITE,
requestbody.get("properties", {}).get("networkAcls", {}).get("defaultAction")
== "Allow",
requestbody.get("location") is None,
azure_activity_success(event),
]
)
def title(event):
resource_id = event.get("resourceId", "")
storage_account = extract_resource_name_from_id(
resource_id, "storageAccounts", default="<UNKNOWN_ACCOUNT>"
)
return f"Azure Storage Account public network access enabled on [{storage_account}] "
def alert_context(event):
context = azure_activity_alert_context(event)
return context
Rule specification
AnalysisType: rule
Filename: azure_storage_account_public_network_access_enabled.py
RuleID: "Azure.MonitorActivity.StorageAccount.PublicNetworkAccessEnabled"
DisplayName: "Azure Storage Account Public Network Access Enabled"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: High
Description: >
Detects when an existing Azure storage account's network settings are modified to enable public network access.
This could indicate a potential data exfiltration risk or misconfiguration.
Reports:
MITRE ATT&CK:
- TA0010:T1567 # Exfiltration: Exfiltration Over Web Service
Tags:
- Exfiltration
- Exfiltration Over Web Service
Runbook: |
1. Query Azure Monitor Activity logs for all storage account operations by the callerIpAddress in the 6 hours before and after this alert to establish activity patterns
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure storage account modifications or data access events from the same user or IP in the past 7 days to identify potential data exfiltration campaigns
Reference: https://www.mitiga.io/blog/ransomware-strikes-azure-storage-are-you-ready
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.STORAGE/STORAGEACCOUNTS/WRITEresultTypeis one ofSuccess,Succeeded
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all storage account operations by the callerIpAddress in the 6 hours before and after this alert to establish activity patterns
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure storage account modifications or data access events from the same user or IP in the past 7 days to identify potential data exfiltration campaigns
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"durationMs": 1523,
"identity": {
"claims": {
"name": "denethor@lotr.com"
}
},
"level": "Informational",
"location": "eastus",
"operationName": "Microsoft.Storage/storageAccounts/write",
"operationVersion": "2021-04-01",
"properties": {
"requestbody": "{\"properties\":{\"networkAcls\":{\"defaultAction\":\"Allow\",\"bypass\":\"AzureServices\",\"virtualNetworkRules\":[],\"ipRules\":[]}}}"
},
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.Storage/storageAccounts/mystorageaccount",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Storage Account Shared Key Access Enabled
#Detects when an existing Azure storage account's shared key access is enabled (allowSharedKeyAccess: true). Shared key access uses storage account keys for authentication, which is less secure than Azure AD-based authentication.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
azure_parse_json_string,
extract_resource_name_from_id,
)
STORAGE_ACCOUNT_WRITE = "MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE"
def rule(event):
requestbody = azure_parse_json_string(event.deep_get("properties", "requestbody", default=None))
return all(
[
event.get("operationName", "").upper() == STORAGE_ACCOUNT_WRITE,
requestbody.get("properties", {}).get("allowSharedKeyAccess") is True,
requestbody.get("location") is None,
azure_activity_success(event),
]
)
def title(event):
resource_id = event.get("resourceId", "")
storage_account = extract_resource_name_from_id(
resource_id, "storageAccounts", default="<UNKNOWN_ACCOUNT>"
)
return f"Azure Storage Account shared key access enabled on [{storage_account}]"
def alert_context(event):
context = azure_activity_alert_context(event)
return context
Rule specification
AnalysisType: rule
Filename: azure_storage_account_key_access_enabled.py
RuleID: "Azure.MonitorActivity.StorageAccount.SharedKeyAccessEnabled"
DisplayName: "Azure Storage Account Shared Key Access Enabled"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Medium
Description: >
Detects when an existing Azure storage account's shared key access is enabled (allowSharedKeyAccess: true).
Shared key access uses storage account keys for authentication, which is less secure than Azure AD-based authentication.
Reports:
MITRE ATT&CK:
- TA0006:T1098 # Persistence: Account Manipulation
Tags:
- Persistence
- Account Manipulation
Runbook: |
1. Query Azure Monitor Activity logs for all storage account operations by the callerIpAddress in the 6 hours before and after this alert to identify patterns
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure storage account configuration changes or data access events from the same user or IP in the past 7 days to assess risk
Reference: https://orca.security/resources/blog/azure-shared-key-authorization-exploitation/
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.STORAGE/STORAGEACCOUNTS/WRITEresultTypeis one ofSuccess,Succeeded
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all storage account operations by the callerIpAddress in the 6 hours before and after this alert to identify patterns
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure storage account configuration changes or data access events from the same user or IP in the past 7 days to assess risk
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"durationMs": 1523,
"identity": {
"claims": {
"name": "denethor@lotr.com"
}
},
"level": "Informational",
"location": "eastus",
"operationName": "Microsoft.Storage/storageAccounts/write",
"operationVersion": "2021-04-01",
"properties": {
"requestbody": "{\"properties\":{\"allowSharedKeyAccess\":true}}"
},
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.Storage/storageAccounts/mystorageaccount",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Storage Blob Anonymous Access Enabled
#Detects when Azure storage account blob anonymous access is enabled at the account level or when container public access is configured. Enabling anonymous access allows unauthenticated users to read blob data and may lead to data exposure.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Collection |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
azure_parse_json_string,
extract_resource_name_from_id,
)
STORAGE_WRITE = "MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE"
BLOB_SERVICES_WRITE = "MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/WRITE"
CONTAINERS_WRITE = "MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/CONTAINERS/WRITE"
def rule(event):
if not azure_activity_success(event):
return False
operation = event.get("operationName", "").upper()
requestbody = azure_parse_json_string(event.deep_get("properties", "requestbody", default=None))
properties = requestbody.get("properties", {})
# Check for storage account level - allowBlobPublicAccess
if operation == STORAGE_WRITE:
return properties.get("allowBlobPublicAccess") is True
# Check for blob service or container level - publicAccess
if operation in [BLOB_SERVICES_WRITE, CONTAINERS_WRITE]:
return properties.get("publicAccess") in ["Blob", "Container"]
return False
def title(event):
resource_id = event.get("resourceId", "")
operation = event.get("operationName", "").upper()
if operation == STORAGE_WRITE:
storage_account = extract_resource_name_from_id(
resource_id, "storageAccounts", default="<UNKNOWN_ACCOUNT>"
)
return f"Azure Storage Account anonymous blob access enabled on [{storage_account}]"
# For container-level operations, try to extract container name
container = extract_resource_name_from_id(
resource_id, "containers", default="<UNKNOWN_CONTAINER>"
)
if container:
storage_resource = container
else:
storage_resource = extract_resource_name_from_id(
resource_id, "storageAccounts", default="<UNKNOWN_RESOURCE>"
)
requestbody = azure_parse_json_string(event.deep_get("properties", "requestbody", default=None))
resource_type = requestbody.get("properties", {}).get("publicAccess", "")
return f"Azure Storage public access allowed on [{resource_type}] " f"for [{storage_resource}]"
def alert_context(event):
context = azure_activity_alert_context(event)
return context
Rule specification
AnalysisType: rule
Filename: azure_storage_blob_anonymous_access_enabled.py
RuleID: "Azure.MonitorActivity.StorageAccount.BlobAnonymousAccessEnabled"
DisplayName: "Azure Storage Blob Anonymous Access Enabled"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: High
Description: >
Detects when Azure storage account blob anonymous access is enabled at the account level or when container public access is configured.
Enabling anonymous access allows unauthenticated users to read blob data and may lead to data exposure.
Reports:
MITRE ATT&CK:
- TA0010:T1530 # Exfiltration: Data from Cloud Storage
Tags:
- Exfiltration
- Data from Cloud Storage
Runbook: |
1. Query Azure Monitor Activity logs for all storage account operations by the callerIpAddress in the 6 hours before and after this alert to identify if this is part of a broader configuration change
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for data access patterns or blob download events from the affected storage account in the 24 hours after this configuration change to identify potential data exposure
Reference: https://hackingthe.cloud/azure/anonymous-blob-access/
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
resultTypeis one ofSuccess,Succeededany of:
operationNameisMICROSOFT.STORAGE/STORAGEACCOUNTS/WRITEall of:
operationNameis notMICROSOFT.STORAGE/STORAGEACCOUNTS/WRITEoperationNameis one ofMICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/WRITE,MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/CONTAINERS/WRITE
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE" |
operationName | in |
| field:"operationName" kind:in |
operationName | ne |
| field:"operationName" kind:ne value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all storage account operations by the callerIpAddress in the 6 hours before and after this alert to identify if this is part of a broader configuration change
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for data access patterns or blob download events from the affected storage account in the 24 hours after this configuration change to identify potential data exposure
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"location": "eastus",
"operationName": "Microsoft.Storage/storageAccounts/write",
"operationVersion": "2021-04-01",
"properties": {
"requestbody": "{\"properties\":{\"allowBlobPublicAccess\":true}}"
},
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.Storage/storageAccounts/mystorageaccount",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Storage Blob Bulk Extraction
#Detects high-volume blob extraction (50+ GetBlob operations in 15 minutes) from Azure Storage accounts. Storm-0501 rapidly extracts data using stolen credentials before ransomware deployment. Replicates Defender for Cloud alert 'Unusual amount of data extracted from a storage account'.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Collection | |
| Exfiltration |
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_resource_logs_success,
extract_resource_name_from_id,
)
def rule(event):
# Must be GetBlob operation (actual data retrieval)
operation = event.get("operationName", "").upper()
if operation != "GETBLOB":
return False
# Must be successful
return azure_resource_logs_success(event)
def extract_caller_ip(event):
"""Extract IP address from callerIpAddress field, removing port if present"""
caller_ip_address = event.get("callerIpAddress", "")
if not caller_ip_address:
return ""
# Split by colon to remove port if present
return caller_ip_address.split(":")[0] if ":" in caller_ip_address else caller_ip_address
def title(event):
caller_ip = extract_caller_ip(event) or "<UNKNOWN_IP>"
resource_id = event.get("resourceId", "")
storage_account = (
extract_resource_name_from_id(resource_id, "storageAccounts", default="<UNKNOWN_ACCOUNT>")
if resource_id
else "<UNKNOWN_ACCOUNT>"
)
return (
f"Unusual volume of blobs extracted from Azure Storage account [{storage_account}] "
f"by [{caller_ip}]"
)
def dedup(event):
"""Group by storage account and caller IP for 15-minute aggregation"""
caller_ip = extract_caller_ip(event) or "unknown"
resource_id = event.get("resourceId", "")
storage_account = (
extract_resource_name_from_id(resource_id, "storageAccounts", default="unknown")
if resource_id
else "unknown"
)
return f"{storage_account}:{caller_ip}"
def severity(event):
"""Higher severity if user agent suggests automated exfiltration"""
user_agent = event.deep_get("properties", "userAgentHeader", default="").lower()
# Scripts, curl, wget suggest malicious automation
suspicious_agents = ["python", "curl", "wget", "powershell", "bash", "script"]
if any(agent in user_agent for agent in suspicious_agents):
return "HIGH"
return "MEDIUM"
def alert_context(event):
context = azure_activity_alert_context(event)
# Add blob-specific context
context["blob_path"] = event.deep_get("properties", "objectKey", default="<UNKNOWN>")
context["user_agent"] = event.deep_get("properties", "userAgentHeader", default="<UNKNOWN>")
return context
Rule specification
AnalysisType: rule
Filename: azure_storage_blob_bulk_extraction.py
RuleID: "Azure.MonitorActivity.Storage.Blob.BulkExtraction"
DisplayName: "Azure Storage Blob Bulk Extraction"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Medium
Threshold: 50
DedupPeriodMinutes: 15
Description: >
Detects high-volume blob extraction (50+ GetBlob operations in 15 minutes) from Azure Storage accounts.
Storm-0501 rapidly extracts data using stolen credentials before ransomware deployment.
Replicates Defender for Cloud alert 'Unusual amount of data extracted from a storage account'.
Reports:
MITRE ATT&CK:
- TA0010:T1567 # Exfiltration: Exfiltration Over Web Service
- TA0009:T1530 # Collection: Data from Cloud Storage
Reference: https://learn.microsoft.com/en-us/azure/defender-for-cloud/alerts-azure-storage
Tags:
- Exfiltration
- Collection
- Storm-0501
- Defender for Cloud
- Data Theft
Runbook: |
1. Query Azure MonitorActivity logs for all GetBlob operations from the callerIpAddress in the 2 hours before and after the alert to calculate total blob count
2. Compare the blob count from step 1 to the callerIpAddress's 30-day average for this storage account to determine if this is anomalous volume
3. Find all SAS token generation operations (listAccountSas, listServiceSas) in the 6 hours before the first GetBlob to identify if stolen SAS tokens are being used
4. Check if the callerIpAddress has accessed this storage account in the past 90 days to determine if this is a new or known accessor
5. Search for authentication events from the callerIpAddress in Azure.Audit logs in the 48 hours before the alert to assess if account compromise preceded the extraction
SummaryAttributes:
- callerIpAddress
- resourceId
- properties:objectKey
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisGETBLOBproperties.metricResponseTypeisSuccess
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"GETBLOB" |
properties.metricResponseType | eq |
| field:"properties.metricResponseType" kind:eq value:"Success" |
Response runbook
1. Query Azure MonitorActivity logs for all GetBlob operations from the callerIpAddress in the 2 hours before and after the alert to calculate total blob count
2. Compare the blob count from step 1 to the callerIpAddress's 30-day average for this storage account to determine if this is anomalous volume
3. Find all SAS token generation operations (listAccountSas, listServiceSas) in the 6 hours before the first GetBlob to identify if stolen SAS tokens are being used
4. Check if the callerIpAddress has accessed this storage account in the past 90 days to determine if this is a new or known accessor
5. Search for authentication events from the callerIpAddress in Azure.Audit logs in the 48 hours before the alert to assess if account compromise preceded the extraction
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "8.8.8.8:44321",
"category": "StorageRead",
"level": "Informational",
"location": "eastus",
"operationName": "GetBlob",
"p_log_type": "Azure.MonitorActivity",
"properties": {
"accountName": "mystorageaccount",
"clientRequestId": "b1c2d3e4-f5g6-7890-hijk-lm9876543210",
"metricResponseType": "Success",
"objectKey": "/mystorageaccount/sensitive-data/financials/report_2025.xlsx",
"serverLatencyMs": 18,
"serviceType": "blob",
"tlsVersion": "TLS 1.3",
"userAgentHeader": "python-requests/2.31.0"
},
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.Storage/storageAccounts/mystorageaccount/blobServices/default",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-01-28T18:00:00.000Z"
}
Azure Storage Blob Container Permissions Modified
#Detects when permissions are modified on an Azure Storage blob container. Adversaries may modify container permissions to enable public access, grant unauthorized access, or prepare for data exfiltration. Changes to blob permissions can indicate attempts to access sensitive data, establish persistence through external access, or facilitate ransomware by modifying access controls before encryption.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment | |
| Exfiltration |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
BLOB_PERMISSIONS_OPERATIONS = [
"MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/CONTAINERS/WRITE",
]
def rule(event):
return event.get(
"operationName", ""
).upper() in BLOB_PERMISSIONS_OPERATIONS and azure_activity_success(event)
def title(event):
resource_id = event.get("resourceId", "<UNKNOWN_RESOURCE>")
container_name = extract_resource_name_from_id(
resource_id, "containers", default="<UNKNOWN_CONTAINER>"
)
storage_account_name = extract_resource_name_from_id(
resource_id, "storageAccounts", default="<UNKNOWN_STORAGE_ACCOUNT>"
)
return (
f"Azure Storage Blob Container Modified: [{container_name}] " f"in [{storage_account_name}]"
)
def alert_context(event):
context = azure_activity_alert_context(event)
return context
Rule specification
AnalysisType: rule
Filename: azure_storage_blob_permissions_modified.py
RuleID: "Azure.MonitorActivity.Storage.BlobPermissionsModified"
DisplayName: "Azure Storage Blob Container Permissions Modified"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Medium
Description: >
Detects when permissions are modified on an Azure Storage blob container. Adversaries may modify
container permissions to enable public access, grant unauthorized access, or prepare for data
exfiltration. Changes to blob permissions can indicate attempts to access sensitive data, establish
persistence through external access, or facilitate ransomware by modifying access controls before
encryption.
Reports:
MITRE ATT&CK:
- TA0010:T1567.002 # Exfiltration: Exfiltration to Cloud Storage
- TA0005:T1222 # Defense Evasion: File and Directory Permissions Modification
Tags:
- Exfiltration
- Exfiltration to Cloud Storage
- Defense Evasion
- File and Directory Permissions Modification
Runbook: |
1. Query Azure Monitor Activity logs for all storage container operations (create, modify, delete) by the callerIpAddress in the 24 hours before and after the alert to identify permission modification patterns
2. Find all blob upload and download operations for the affected storage container in the 6 hours after the permission change to identify potential data exfiltration
3. Check if the callerIpAddress has modified storage container permissions in the past 90 days to determine if this is typical administrative behavior
Reference: https://github.com/elastic/detection-rules/blob/main/rules/integrations/azure/persistence_azure_storage_blob_permissions_modified.toml
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
- location
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameis one ofMICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/CONTAINERS/WRITEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | in |
| field:"operationName" kind:in value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/CONTAINERS/WRITE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all storage container operations (create, modify, delete) by the callerIpAddress in the 24 hours before and after the alert to identify permission modification patterns
2. Find all blob upload and download operations for the affected storage container in the 6 hours after the permission change to identify potential data exfiltration
3. Check if the callerIpAddress has modified storage container permissions in the past 90 days to determine if this is typical administrative behavior
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Informational",
"location": "eastus",
"operationName": "MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/CONTAINERS/WRITE",
"operationVersion": "2021-09-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/storage-rg/providers/Microsoft.Storage/storageAccounts/prodstorageacct/blobServices/default/containers/sensitive-data",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-22T10:30:00.0000000Z"
}
Azure Storage Blob CPK Encryption Detected
#Detects when users attempt to access Azure Storage blobs that are encrypted with Customer-Provided Keys (CPK) but fail because they don't have the encryption key. This may indicate a ransomware operation that is using CPK encryption to hold data hostage, as legitimate users cannot access their own encrypted blobs without the attacker's key.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact |
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_resource_logs_failure,
extract_resource_name_from_id,
)
STORAGE_READ_CATEGORY = "STORAGEREAD"
CPK_ERROR_STATUS = "BLOBUSESCUSTOMERSPECIFIEDENCRYPTION"
def rule(event):
# Detect when users try to access CPK-encrypted blobs without the key
return (
event.get("category", "").upper() == STORAGE_READ_CATEGORY
and event.get("statusCode") == 409
and event.get("statusText", "").upper() == CPK_ERROR_STATUS
and azure_resource_logs_failure(event)
)
def title(event):
resource_id = event.get("resourceId", "<UNKNOWN_STORAGE_ACCOUNT>")
storage_account = extract_resource_name_from_id(
resource_id, "storageAccounts", default="<UNKNOWN_STORAGE_ACCOUNT>"
)
blob_path = event.deep_get("properties", "objectKey", default="<UNKNOWN_BLOB>")
return (
f"Access denied returned in storage account [{storage_account}] "
f"for CPK-encrypted blob [{blob_path}]"
)
def alert_context(event):
context = azure_activity_alert_context(event)
# Add blob-specific context
context["blob_path"] = event.deep_get("properties", "objectKey", default="<UNKNOWN>")
context["user_agent"] = event.deep_get("properties", "userAgentHeader", default="<UNKNOWN>")
context["status_code"] = event.get("statusCode", "<UNKNOWN>")
context["status_text"] = event.get("statusText", "<UNKNOWN>")
return context
Rule specification
AnalysisType: rule
Filename: azure_storage_blob_cpk_encryption_detected.py
RuleID: "Azure.MonitorActivity.Storage.Blob.CPKEncryptionDetected"
DisplayName: "Azure Storage Blob CPK Encryption Detected"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: High
Description: >
Detects when users attempt to access Azure Storage blobs that are encrypted with
Customer-Provided Keys (CPK) but fail because they don't have the encryption key.
This may indicate a ransomware operation that is using CPK encryption to hold data hostage,
as legitimate users cannot access their own encrypted blobs without the attacker's key.
Reports:
MITRE ATT&CK:
- TA0040:T1486 # Impact: Data Encrypted for Impact
- TA0040:T1490 # Impact: Inhibit System Recovery
Tags:
- Impact
- Data Encrypted for Impact
- Inhibit System Recovery
- Ransomware
Runbook: |
1. Query Azure MonitorActivity logs for all PutBlob operations on the affected resourceId in the 48 hours before the first error to identify when files were encrypted and from which callerIpAddress
2. Find all ListAccountSAS and ListKeys operations by any callerIpAddress or p_any_usernames in the 7 days before the first PutBlob operation to identify potential credential theft
3. Search for all other storage accounts accessed by the same callerIpAddress in the past 24 hours to determine the scope of the attack across the Azure environment
Reference: https://learn.microsoft.com/en-us/dotnet/api/azure.storage.blobs.models.bloberrorcode.blobusescustomerspecifiedencryption?view=azure-dotnet
SummaryAttributes:
- callerIpAddress
- resourceId
- p_any_usernames
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
categoryisSTORAGEREADstatusCodeis409statusTextisBLOBUSESCUSTOMERSPECIFIEDENCRYPTIONproperties.metricResponseTypeis notSuccess
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
category | eq |
| field:"category" kind:eq value:"STORAGEREAD" |
properties.metricResponseType | ne |
| field:"properties.metricResponseType" kind:ne value:"Success" |
statusCode | eq |
| field:"statusCode" kind:eq value:"409" |
statusText | eq |
| field:"statusText" kind:eq value:"BLOBUSESCUSTOMERSPECIFIEDENCRYPTION" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
objectKey | properties.objectKey |
Response runbook
1. Query Azure MonitorActivity logs for all PutBlob operations on the affected resourceId in the 48 hours before the first error to identify when files were encrypted and from which callerIpAddress
2. Find all ListAccountSAS and ListKeys operations by any callerIpAddress or p_any_usernames in the 7 days before the first PutBlob operation to identify potential credential theft
3. Search for all other storage accounts accessed by the same callerIpAddress in the past 24 hours to determine the scope of the attack across the Azure environment
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1:57485",
"category": "StorageRead",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"durationMs": 4,
"identity": {
"tokenHash": "key1(EXAMPLE_HASH)",
"type": "SAS"
},
"location": "westus2",
"operationName": "GetBlobMetadata",
"operationVersion": "2024-11-04",
"properties": {
"accountName": "mystorageaccount",
"metricResponseType": "ClientOtherError",
"objectKey": "/mystorageaccount/corporate-files/documents/internal_doc_8.txt.ENCRYPTED",
"serverLatencyMs": 4,
"serviceType": "blob",
"tlsVersion": "TLS 1.3",
"userAgentHeader": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/7.7.7.7 Safari/537.36"
},
"protocol": "HTTPS",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.Storage/storageAccounts/mystorageaccount/blobServices/default",
"schemaVersion": "1.0",
"statusCode": 409,
"statusText": "BlobUsesCustomerSpecifiedEncryption",
"time": "2024-12-19T19:15:07.295Z"
}
Azure Storage Blob Deletion
#Detects when blobs are deleted from Azure Storage accounts via the DeleteBlob operation. Multiple deletion events in a short time frame may indicate ransomware activity, data destruction, or malicious insider activity. This rule fires on individual deletions and can be aggregated in Panther to detect bulk deletion patterns by configuring deduplication on storage account or caller IP address.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact |
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_resource_logs_success,
extract_resource_name_from_id,
)
def rule(event):
return event.get("operationName", "").upper() == "DELETEBLOB" and azure_resource_logs_success(
event
)
def title(event):
caller = event.get("callerIpAddress", "<UNKNOWN_CALLER>")
resource_id = event.get("resourceId", "<UNKNOWN_STORAGE_ACCOUNT>")
storage_account_name = extract_resource_name_from_id(
resource_id, "storageAccounts", default="<UNKNOWN_STORAGE_ACCOUNT>"
)
return f"Azure Blobs deleted from storage account [{storage_account_name}] by caller [{caller}]"
def alert_context(event):
context = azure_activity_alert_context(event)
# Add blob-specific context
context["blob_path"] = event.deep_get("properties", "objectKey", default="<UNKNOWN>")
context["user_agent"] = event.deep_get("properties", "userAgentHeader", default="<UNKNOWN>")
return context
Rule specification
AnalysisType: rule
Filename: azure_storage_blob_bulk_deletion.py
RuleID: "Azure.MonitorActivity.Storage.Blob.BulkDeletion"
DisplayName: "Azure Storage Blob Deletion"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Medium
Threshold: 15
DedupPeriodMinutes: 15
Description: >
Detects when blobs are deleted from Azure Storage accounts via the DeleteBlob operation.
Multiple deletion events in a short time frame may indicate ransomware activity, data destruction,
or malicious insider activity. This rule fires on individual deletions and can be aggregated
in Panther to detect bulk deletion patterns by configuring deduplication on storage account
or caller IP address.
Reports:
MITRE ATT&CK:
- TA0040:T1485 # Impact: Data Destruction
- TA0040:T1490 # Impact: Inhibit System Recovery
Tags:
- Impact
- Data Destruction
- Inhibit System Recovery
- Ransomware
Runbook: |
1. Query Azure MonitorActivity logs for all DeleteBlob operations from the callerIpAddress in the 2 hours before and after the alert to identify the deletion pattern and volume
2. Find all ListAccountSAS, ListKeys, and PutBlob operations by the same callerIpAddress or p_any_usernames in the 6 hours before the first deletion to identify potential credential theft or ransomware staging
3. Check if the callerIpAddress or p_any_usernames have accessed this storage account in the past 90 days to establish if this is expected behavior
Reference: https://learn.microsoft.com/en-us/dotnet/api/azure.storage.blobs.blobcontainerclient.deleteblob?view=azure-dotnet
SummaryAttributes:
- callerIpAddress
- resourceId
- p_any_usernames
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisDELETEBLOBproperties.metricResponseTypeisSuccess
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"DELETEBLOB" |
properties.metricResponseType | eq |
| field:"properties.metricResponseType" kind:eq value:"Success" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
callerIpAddress |
Response runbook
1. Query Azure MonitorActivity logs for all DeleteBlob operations from the callerIpAddress in the 2 hours before and after the alert to identify the deletion pattern and volume
2. Find all ListAccountSAS, ListKeys, and PutBlob operations by the same callerIpAddress or p_any_usernames in the 6 hours before the first deletion to identify potential credential theft or ransomware staging
3. Check if the callerIpAddress or p_any_usernames have accessed this storage account in the past 90 days to establish if this is expected behavior
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "5.5.5.5:28915",
"category": "StorageWrite",
"level": "Informational",
"location": "eastus",
"operationName": "DeleteBlob",
"properties": {
"accountName": "mystorageaccount",
"clientRequestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"metricResponseType": "Success",
"objectKey": "/mystorageaccount/corporate-files/documents/internal_doc_13.txt",
"serverLatencyMs": 22,
"serviceType": "blob",
"tlsVersion": "TLS 1.3",
"userAgentHeader": "AZURECLI/2.79.0 (RPM) azsdk-python-storage-blob/12.16.0 Python/3.12.9 (Linux-6.6.6.6-microsoft-standard-x86_64-with-glibc2.38) cloud-shell/1.0"
},
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.Storage/storageAccounts/mystorageaccount/blobServices/default",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-19T16:37:59.255Z"
}
Azure Storage Blob Soft Delete Disabled
#Detects when Azure storage account blob soft delete is disabled. Disabling soft delete removes protection against accidental deletion and may indicate preparation for data destruction.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
azure_parse_json_string,
extract_resource_name_from_id,
)
BLOB_SERVICES_WRITE = "MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/WRITE"
def rule(event):
requestbody = azure_parse_json_string(event.deep_get("properties", "requestbody", default=None))
return all(
[
event.get("operationName", "").upper() == BLOB_SERVICES_WRITE,
requestbody.get("properties", {}).get("deleteRetentionPolicy", {}).get("enabled")
is False,
azure_activity_success(event),
]
)
def title(event):
resource_id = event.get("resourceId", "")
storage_account = extract_resource_name_from_id(
resource_id, "storageAccounts", default="<UNKNOWN_ACCOUNT>"
)
return f"Azure Storage blob soft delete disabled on [{storage_account}]"
def alert_context(event):
context = azure_activity_alert_context(event)
return context
Rule specification
AnalysisType: rule
Filename: azure_storage_blob_soft_delete_disabled.py
RuleID: "Azure.MonitorActivity.StorageAccount.BlobSoftDeleteDisabled"
DisplayName: "Azure Storage Blob Soft Delete Disabled"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: High
Description: >
Detects when Azure storage account blob soft delete is disabled.
Disabling soft delete removes protection against accidental deletion and may indicate preparation for data destruction.
Reports:
MITRE ATT&CK:
- TA0040:T1485 # Impact: Data Destruction
- TA0040:T1490 # Impact: Inhibit System Recovery
Tags:
- Impact
- Data Destruction
- Inhibit System Recovery
- Ransomware
Runbook: |
1. Query Azure Monitor Activity logs for all storage account blob service operations by the callerIpAddress in the 6 hours before and after this alert to identify patterns
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure storage account configuration changes or blob deletion events from the same user or IP in the past 7 days to assess if this is part of a data destruction campaign
Reference: https://azure.github.io/azure-storage-java/com/microsoft/azure/storage/DeleteRetentionPolicy.html
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/WRITEresultTypeis one ofSuccess,Succeeded
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/WRITE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all storage account blob service operations by the callerIpAddress in the 6 hours before and after this alert to identify patterns
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure storage account configuration changes or blob deletion events from the same user or IP in the past 7 days to assess if this is part of a data destruction campaign
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"location": "eastus",
"operationName": "Microsoft.Storage/storageAccounts/blobServices/write",
"operationVersion": "2021-04-01",
"properties": {
"requestbody": "{\"properties\":{\"deleteRetentionPolicy\":{\"enabled\":false}}}"
},
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.Storage/storageAccounts/mystorageaccount/blobServices/default",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Storage Blob Upload WITH CPK Encryption Error
#Detects potential CPK-based ransomware attacks on Azure Storage by correlating blob uploads with subsequent Customer-Provided Key (CPK) encryption errors on the same blob path. This pattern indicates an attacker has encrypted blobs using CPK and legitimate users are now unable to access their data without the attacker's encryption key. This technique allows attackers to hold data hostage while maintaining access themselves, as only they possess the customer-provided encryption key needed to decrypt the blobs.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule specification
AnalysisType: correlation_rule
RuleID: "Azure.Storage.BlobUpload.WITH.CPKEncryptionError"
DisplayName: "Azure Storage Blob Upload WITH CPK Encryption Error"
Enabled: false
Severity: High
Description: >
Detects potential CPK-based ransomware attacks on Azure Storage by correlating blob
uploads with subsequent Customer-Provided Key (CPK) encryption errors on the same blob path.
This pattern indicates an attacker has encrypted blobs using CPK and legitimate users
are now unable to access their data without the attacker's encryption key.
This technique allows attackers to hold data hostage while maintaining access themselves,
as only they possess the customer-provided encryption key needed to decrypt the blobs.
Runbook: |
1. Query Azure MonitorActivity logs for all PutBlob operations by the callerIpAddress in the 6 hours before the alert to identify the full scope of encrypted blobs
2. Check Azure AD sign-in logs for authentication events from the same callerIpAddress in the 24 hours before the first upload to determine if credentials were compromised
3. Search for LISTACCOUNTSAS or LISTKEYS operations on the affected storage_account in the 48 hours before the uploads to identify potential credential theft
Reference: https://learn.microsoft.com/en-us/dotnet/api/azure.storage.blobs.models.bloberrorcode.blobusescustomerspecifiedencryption?view=azure-dotnet
Reports:
MITRE ATT&CK:
- TA0040:T1486 # Impact: Data Encrypted for Impact
- TA0040:T1490 # Impact: Inhibit System Recovery
Detection:
- Group:
- ID: Blob Upload
RuleID: Azure.MonitorActivity.Storage.Blob.Uploaded
- ID: CPK Access Denied
RuleID: Azure.MonitorActivity.Storage.Blob.CPKEncryptionDetected
MatchCriteria:
field_name:
- GroupID: Blob Upload
Match: p_alert_context.blob_path
- GroupID: CPK Access Denied
Match: p_alert_context.blob_path
Schedule:
RateMinutes: 1440
TimeoutMinutes: 5
LookbackWindowMinutes: 1800
Stages and Predicates
Fires when the steps below all occur within 30h, correlated by p_alert_context.blob_path. Each step needs one match unless a higher minimum is shown.
Stage 1: step Blob Upload
References detection Azure Storage Blob Uploaded.
Stage 2: step CPK Access Denied
References detection Azure Storage Blob CPK Encryption Detected.
Response runbook
1. Query Azure MonitorActivity logs for all PutBlob operations by the callerIpAddress in the 6 hours before the alert to identify the full scope of encrypted blobs
2. Check Azure AD sign-in logs for authentication events from the same callerIpAddress in the 24 hours before the first upload to determine if credentials were compromised
3. Search for LISTACCOUNTSAS or LISTKEYS operations on the affected storage_account in the 48 hours before the uploads to identify potential credential theft
Azure Storage Blob Uploaded
#Tracks successful blob uploads to Azure Storage accounts.
Detection logic
from panther_azureactivity_helpers import azure_resource_logs_success
def rule(event):
return event.get("operationName", "").upper() == "PUTBLOB" and azure_resource_logs_success(
event
)
Rule specification
AnalysisType: rule
Filename: azure_storage_blob_uploaded.py
RuleID: "Azure.MonitorActivity.Storage.Blob.Uploaded"
DisplayName: "Azure Storage Blob Uploaded"
Enabled: true
CreateAlert: false
LogTypes:
- Azure.MonitorActivity
Severity: Info
Description: >
Tracks successful blob uploads to Azure Storage accounts.
SummaryAttributes:
- callerIpAddress
- resourceId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisPUTBLOBproperties.metricResponseTypeisSuccess
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"PUTBLOB" |
properties.metricResponseType | eq |
| field:"properties.metricResponseType" kind:eq value:"Success" |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "5.5.5.5:29713",
"category": "StorageWrite",
"location": "eastus",
"operationName": "PutBlob",
"operationVersion": "2025-11-05",
"properties": {
"accountName": "mystorageaccount",
"etag": "\"0x8DE3F32E714874F\"",
"metricResponseType": "Success",
"objectKey": "/mystorageaccount/test/documents/internal_doc_15.txt.ENCRYPTED",
"serverLatencyMs": 11,
"serviceType": "blob",
"tlsVersion": "TLS 1.3",
"userAgentHeader": "azsdk-python-storage-blob/12.27.1 Python/3.12.9"
},
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.Storage/storageAccounts/mystorageaccount/blobServices/default",
"statusCode": 201,
"statusText": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-19T19:14:59.091Z"
}
Azure Storage Container Soft Delete Disabled
#Detects when Azure storage account container soft delete is disabled. Disabling container soft delete removes protection against accidental deletion and may indicate preparation for data destruction.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
azure_parse_json_string,
extract_resource_name_from_id,
)
BLOB_SERVICES_WRITE = "MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/WRITE"
def rule(event):
requestbody = azure_parse_json_string(event.deep_get("properties", "requestbody", default=None))
return all(
[
event.get("operationName", "").upper() == BLOB_SERVICES_WRITE,
requestbody.get("properties", {})
.get("containerDeleteRetentionPolicy", {})
.get("enabled")
is False,
azure_activity_success(event),
]
)
def title(event):
resource_id = event.get("resourceId", "")
storage_account = extract_resource_name_from_id(
resource_id, "storageAccounts", default="<UNKNOWN_ACCOUNT>"
)
return f"Azure Storage container soft delete disabled on [{storage_account}]"
def alert_context(event):
context = azure_activity_alert_context(event)
return context
Rule specification
AnalysisType: rule
Filename: azure_storage_container_soft_delete_disabled.py
RuleID: "Azure.MonitorActivity.StorageAccount.ContainerSoftDeleteDisabled"
DisplayName: "Azure Storage Container Soft Delete Disabled"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: High
Description: >
Detects when Azure storage account container soft delete is disabled.
Disabling container soft delete removes protection against accidental deletion and may indicate preparation for data destruction.
Reports:
MITRE ATT&CK:
- TA0040:T1485 # Impact: Data Destruction
- TA0040:T1490 # Impact: Inhibit System Recovery
Tags:
- Impact
- Data Destruction
- Inhibit System Recovery
- Ransomware
Runbook: |
1. Query Azure Monitor Activity logs for all storage account blob service operations by the callerIpAddress in the 6 hours before and after this alert to identify patterns
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure storage account configuration changes or container deletion events from the same user or IP in the past 7 days to assess if this is part of a data destruction campaign
Reference: https://learn.microsoft.com/en-us/azure/storage/blobs/soft-delete-container-enable?tabs=azure-portal
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/WRITEresultTypeis one ofSuccess,Succeeded
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/WRITE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all storage account blob service operations by the callerIpAddress in the 6 hours before and after this alert to identify patterns
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure storage account configuration changes or container deletion events from the same user or IP in the past 7 days to assess if this is part of a data destruction campaign
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"location": "eastus",
"operationName": "Microsoft.Storage/storageAccounts/blobServices/write",
"operationVersion": "2021-04-01",
"properties": {
"requestbody": "{\"properties\":{\"containerDeleteRetentionPolicy\":{\"enabled\":false}}}"
},
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.Storage/storageAccounts/mystorageaccount/blobServices/default",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Storage Immutability Policy Deleted
#Detects deletion of Azure Storage immutability policies that provide WORM protection. Storm-0501 ransomware operators delete these policies before encrypting data, as WORM-protected blobs cannot be modified even by administrators. Critical pre-ransomware indicator.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment | |
| Impact |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
IMMUTABILITY_POLICY_DELETE = (
"MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/CONTAINERS/IMMUTABILITYPOLICIES/DELETE"
)
def rule(event):
return event.get(
"operationName", ""
).upper() == IMMUTABILITY_POLICY_DELETE and azure_activity_success(event)
def title(event):
resource_id = event.get("resourceId", "")
container_name = extract_resource_name_from_id(
resource_id, "containers", default="<UNKNOWN_CONTAINER>"
)
storage_account = extract_resource_name_from_id(
resource_id, "storageAccounts", default="<UNKNOWN_STORAGE_ACCOUNT>"
)
return (
f"Azure Storage immutability policy deleted on container [{container_name}] "
f"in storage account [{storage_account}]"
)
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
container_name = extract_resource_name_from_id(resource_id, "containers", default="")
if container_name:
context["container_name"] = container_name
storage_account = extract_resource_name_from_id(resource_id, "storageAccounts", default="")
if storage_account:
context["storage_account"] = storage_account
resource_group = extract_resource_name_from_id(resource_id, "resourceGroups", default="")
if resource_group:
context["resource_group"] = resource_group
return context
Rule specification
AnalysisType: rule
Filename: azure_storage_immutability_policy_deleted.py
RuleID: "Azure.MonitorActivity.Storage.ImmutabilityPolicyDeleted"
DisplayName: "Azure Storage Immutability Policy Deleted"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: High
Description: >
Detects deletion of Azure Storage immutability policies that provide WORM protection.
Storm-0501 ransomware operators delete these policies before encrypting data, as
WORM-protected blobs cannot be modified even by administrators. 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
- TA0040:T1485 # Impact: Data Destruction
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
- Data Destruction
- Ransomware
- Storm-0501
- WORM
Runbook: |
1. Query Azure Monitor Activity logs for all immutability policy deletions by the callerIpAddress and caller identity in the past 6 hours to calculate the total number of containers affected
2. Search for resource lock deletions on the same storage account by the same caller in the 2 hours before this policy deletion to identify the Storm-0501 attack pattern
3. Find all destructive operations on the affected storage account (DeleteBlob, PutBlob with CPK encryption, storage account deletion) in the 2 hours after policy deletion to assess if data destruction has begun
4. Check if the storage account has been accessed from external IPs or if bulk GetBlob operations occurred after policy deletion to identify potential data exfiltration
5. Review Azure.Audit logs for authentication events from the callerIpAddress in the 48 hours before to identify credential compromise indicators (unusual locations, failed MFA, privilege escalations)
6. Search for other immutability policy or backup-related alerts triggered by the same callerIpAddress across all storage accounts in the past 7 days to determine attack scope
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/CONTAINERS/IMMUTABILITYPOLICIES/DELETEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/CONTAINERS/IMMUTABILITYPOLICIES/DELETE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all immutability policy deletions by the callerIpAddress and caller identity in the past 6 hours to calculate the total number of containers affected
2. Search for resource lock deletions on the same storage account by the same caller in the 2 hours before this policy deletion to identify the Storm-0501 attack pattern
3. Find all destructive operations on the affected storage account (DeleteBlob, PutBlob with CPK encryption, storage account deletion) in the 2 hours after policy deletion to assess if data destruction has begun
4. Check if the storage account has been accessed from external IPs or if bulk GetBlob operations occurred after policy deletion to identify potential data exfiltration
5. Review Azure.Audit logs for authentication events from the callerIpAddress in the 48 hours before to identify credential compromise indicators (unusual locations, failed MFA, privilege escalations)
6. Search for other immutability policy or backup-related alerts triggered by the same callerIpAddress across all storage accounts in the past 7 days to determine attack scope
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "203.0.113.42",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"identity": {
"claims": {
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn": "attacker@example.com",
"name": "attacker@example.com"
}
},
"level": "Informational",
"location": "eastus",
"operationName": "MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/CONTAINERS/IMMUTABILITYPOLICIES/DELETE",
"operationVersion": "2021-09-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/data-rg/providers/Microsoft.Storage/storageAccounts/criticaldata001/blobServices/default/containers/backups/immutabilityPolicies/default",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-01-27T14:45:00.0000000Z"
}
Azure Storage SAS Token Access from External IP
#Detects SAS token usage from external public IPs by parsing the signature parameter in storage URIs. Storm-0501 uses stolen SAS tokens from external C2 infrastructure for data exfiltration. Replicates Defender for Cloud alert 'Storage.Blob_AccountSas.InternalSasUsedExternally'.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access | |
| Exfiltration |
Detection logic
import ipaddress
from urllib.parse import parse_qs, urlparse
def rule(event):
"""
Detects SAS token usage from external IP addresses.
Replicates Defender for Cloud: Storage.Blob_AccountSas.InternalSasUsedExternally
"""
# Must be storage operation
if event.get("category") not in ["StorageRead", "StorageWrite", "StorageDelete"]:
return False
# Must be successful
status_code = event.get("statusCode")
if status_code not in [200, 201, 202, 204]:
return False
# Check if SAS token was used (look for 'sig=' in URI)
uri = event.get("uri", "")
if not uri or "sig=" not in uri:
return False
# Check if IP is external (not private/RFC1918)
caller_ip = extract_caller_ip(event)
if not caller_ip or is_private_ip(caller_ip):
return False
return True
def extract_caller_ip(event):
"""Extract IP address from callerIpAddress field, removing port if present"""
caller_ip_address = event.get("callerIpAddress", "")
if not caller_ip_address:
return ""
# Split by colon to remove port if present
return caller_ip_address.split(":")[0] if ":" in caller_ip_address else caller_ip_address
def is_private_ip(ip_address):
"""Check if IP is in private ranges (RFC1918) or localhost"""
if not ip_address:
return True # Treat empty/missing IPs as private to filter them out
try:
ip_obj = ipaddress.ip_address(ip_address)
return ip_obj.is_private or ip_obj.is_loopback
except ValueError:
# Unparseable IPs are treated as external (suspicious) to avoid missing potential threats
return False
def is_permissive_sas(uri):
"""
Check if SAS token has write, delete, or add permissions.
SAS permissions in 'sp' parameter: r=read, a=add, c=create, w=write, d=delete, l=list
"""
if not uri:
return False # Unknown URIs default to non-permissive (read-only assumption)
parsed = urlparse(uri)
params = parse_qs(parsed.query)
permissions = params.get("sp", [""])[0]
# Check for dangerous permissions
return any(perm in permissions for perm in ["w", "d", "a"])
def title(event):
caller_ip = extract_caller_ip(event) or "<UNKNOWN_IP>"
storage_account = event.deep_get("properties", "accountName", default="<UNKNOWN_ACCOUNT>")
operation = event.get("operationName", "<UNKNOWN_OPERATION>")
return (
f"Azure Storage SAS token used from external IP [{caller_ip}] "
f"to access [{storage_account}] with operation [{operation}]"
)
def severity(event):
"""Higher severity for write/delete operations"""
operation = event.get("operationName", "").lower()
# Check if this is a write/delete operation or has permissive SAS
uri = event.get("uri", "")
if is_permissive_sas(uri):
return "HIGH"
# Delete operations are always high severity
if "delete" in operation:
return "HIGH"
# Write operations are medium severity
if any(op in operation for op in ["put", "write", "create", "set"]):
return "MEDIUM"
# Read-only operations from external IPs are low severity
return "LOW"
def alert_context(event):
# Start with standard Azure activity context
context = {
"caller_ip": extract_caller_ip(event) or "<UNKNOWN>",
"storage_account": event.deep_get("properties", "accountName", default="<UNKNOWN>"),
"operation": event.get("operationName", "<UNKNOWN_OPERATION>"),
"object_key": event.deep_get("properties", "objectKey", default="<UNKNOWN>"),
"user_agent": event.deep_get("properties", "userAgentHeader", default="<UNKNOWN>"),
"uri": event.get("uri", "<UNKNOWN_URI>"),
"status_code": event.get("statusCode"),
"category": event.get("category"),
}
# Extract SAS-specific parameters from URI
uri = event.get("uri", "")
if uri:
parsed = urlparse(uri)
params = parse_qs(parsed.query)
if "sp" in params:
context["sas_permissions"] = params["sp"][0]
if "se" in params:
context["sas_expiry"] = params["se"][0]
return context
def dedup(event):
"""Group alerts by storage account and external IP"""
caller_ip = extract_caller_ip(event) or "unknown"
storage_account = event.deep_get("properties", "accountName", default="unknown")
return f"{storage_account}:{caller_ip}"
Rule specification
AnalysisType: rule
Filename: azure_storage_sas_external_access.py
RuleID: "Azure.MonitorActivity.Storage.SASTokenExternalAccess"
DisplayName: "Azure Storage SAS Token Access from External IP"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Low
DedupPeriodMinutes: 60
Description: >
Detects SAS token usage from external public IPs by parsing the signature parameter in storage URIs.
Storm-0501 uses stolen SAS tokens from external C2 infrastructure for data exfiltration.
Replicates Defender for Cloud alert 'Storage.Blob_AccountSas.InternalSasUsedExternally'.
Reports:
MITRE ATT&CK:
- TA0010:T1567 # Exfiltration: Exfiltration Over Web Service
- TA0006:T1552.001 # Credential Access: Unsecured Credentials - Credentials In Files
Reference: https://learn.microsoft.com/en-us/azure/defender-for-cloud/alerts-azure-storage
Tags:
- Exfiltration
- Credential Access
- Storm-0501
- Defender for Cloud
- SAS Token
Runbook: |
1. Query Azure Monitor Activity logs for SAS token generation operations (Microsoft.Storage/storageAccounts/listAccountSas/action) by the same identity in the 24 hours before this access to identify when the token was created and by whom
2. Find all storage operations (GetBlob, ListBlobs, DeleteBlob) from the callerIpAddress in the 6 hours before and after the alert to assess if this is isolated access or part of bulk exfiltration
3. Check if the callerIpAddress has accessed this storage account in the past 90 days to establish if this external IP is expected
4. Review Azure Audit logs for authentication events from the callerIpAddress in the 48 hours before the alert to identify potential account compromise or credential theft
5. Search for other alerts with the same callerIpAddress across all storage accounts in the past 7 days to identify if this is targeted or widespread
SummaryAttributes:
- callerIpAddress
- properties:accountName
- operationName
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
categoryis one ofStorageRead,StorageWrite,StorageDeletestatusCodeis one of200,201,202,204uriis presenturicontainssig=
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
uri | contains | sig= | excludes:uri field:"uri" value:"sig=" |
uri | is_null | excludes:uri |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
category | in |
| field:"category" kind:in |
statusCode | in |
| field:"statusCode" kind:in |
uri | contains |
| field:"uri" kind:contains value:"sig=" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
storage_account | properties.accountName |
operation | operationName |
object_key | properties.objectKey |
user_agent | properties.userAgentHeader |
uri | |
status_code | statusCode |
category |
Response runbook
1. Query Azure Monitor Activity logs for SAS token generation operations (Microsoft.Storage/storageAccounts/listAccountSas/action) by the same identity in the 24 hours before this access to identify when the token was created and by whom
2. Find all storage operations (GetBlob, ListBlobs, DeleteBlob) from the callerIpAddress in the 6 hours before and after the alert to assess if this is isolated access or part of bulk exfiltration
3. Check if the callerIpAddress has accessed this storage account in the past 90 days to establish if this external IP is expected
4. Review Azure Audit logs for authentication events from the callerIpAddress in the 48 hours before the alert to identify potential account compromise or credential theft
5. Search for other alerts with the same callerIpAddress across all storage accounts in the past 7 days to identify if this is targeted or widespread
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "8.8.8.8:45123",
"category": "StorageRead",
"operationName": "GetBlob",
"p_log_type": "Azure.MonitorActivity",
"properties": {
"accountName": "criticaldata001",
"metricResponseType": "Success",
"objectKey": "/criticaldata001/backups/data.zip",
"userAgentHeader": "python-requests/2.31.0"
},
"statusCode": 200,
"time": "2025-01-28T15:30:00.0000000Z",
"uri": "https://criticaldata001.blob.core.windows.net/backups/data.zip?sv=2021-12-02&ss=b&srt=sco&sp=rl&se=2025-12-31T23:59:59Z&sig=SIGNATURE_HERE"
}
Azure User Elevated to User Access Administrator Role
#Detects when a user elevates their permissions to the "User Access Administrator" role in Azure, which grants full control over access management for Azure resources. The User Access Administrator role is one of the most powerful privileged roles in Azure, allowing the holder to manage user access to all Azure resources, assign roles to other users including administrative roles, and effectively control the entire Azure subscription's permission structure.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Detection logic
def rule(event):
operation_name = event.get("operationName", "")
result = event.get("result", "").lower()
return all(
[
"User has elevated their access to User Access Administrator" in operation_name,
result == "success",
]
)
def title(event):
actor = event.deep_get(
"properties", "initiatedBy", "user", "userPrincipalName", default="<UNKNOWN_ACTOR>"
)
return f"User Elevated to User Access Administrator Role: [{actor}]"
def alert_context(event):
context = {}
context["tenantId"] = event.get("tenantId", "<NO_TENANTID>")
context["operation_name"] = event.get("operationName", "<NO_OPERATION>")
context["category"] = event.get("category", "<NO_CATEGORY>")
context["result"] = event.get("result", "<NO_RESULT>")
# Initiator details
context["initiator_user_id"] = event.deep_get(
"properties", "initiatedBy", "user", "id", default="<NO_USER_ID>"
)
context["initiator_display_name"] = event.deep_get(
"properties", "initiatedBy", "user", "displayName", default="<NO_DISPLAY_NAME>"
)
context["initiator_ip"] = event.deep_get(
"properties", "initiatedBy", "user", "ipAddress", default="<NO_IP>"
)
return context
Rule specification
AnalysisType: rule
Filename: azure_elevate_user_access_admin.py
RuleID: "Azure.Audit.ElevateUserAccessAdministrator"
DisplayName: "Azure User Elevated to User Access Administrator Role"
Enabled: true
LogTypes:
- Azure.Audit
Severity: High
Description: >
Detects when a user elevates their permissions to the "User Access Administrator" role in Azure,
which grants full control over access management for Azure resources. The User Access Administrator
role is one of the most powerful privileged roles in Azure, allowing the holder to manage user access
to all Azure resources, assign roles to other users including administrative roles, and effectively
control the entire Azure subscription's permission structure.
Tags:
- Persistence
- Account Manipulation
Reports:
MITRE ATT&CK:
- TA0004:T1098
- TA0004:T1098.003
Runbook: |
1. Query Azure.Audit logs for all role assignment operations and permission changes by properties:initiatedBy:user:userPrincipalName in the 24 hours after the elevation to identify what privileged actions the user performed with the elevated access
2. Verify with the user or their manager whether this elevation was authorized and required for a specific operational task, incident response activity, or break-glass scenario documented in your organization's procedures
3. Review Azure.Audit logs for role assignments made during the elevated access period to identify if the user granted themselves or others persistent administrative roles that should be revoked, and check if the User Access Administrator role was properly removed after the task completion
Reference: https://github.com/elastic/detection-rules/blob/main/rules/integrations/azure/privilege_escalation_entra_id_elevate_to_user_administrator_access.toml
SummaryAttributes:
- properties:initiatedBy:user:userPrincipalName
- properties:initiatedBy:user:ipAddress
- tenantId
Stages and Predicates
Fires on Azure.Audit events when all of the conditions below hold.
Condition
operationNamecontainsUser has elevated their access to User Access Administratorresultissuccess
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | contains |
| field:"operationName" kind:contains value:"User has elevated their access to User Access Administrator" |
result | eq |
| field:"result" kind:eq value:"success" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
userPrincipalName | properties.initiatedBy.user.userPrincipalName |
Response runbook
1. Query Azure.Audit logs for all role assignment operations and permission changes by properties:initiatedBy:user:userPrincipalName in the 24 hours after the elevation to identify what privileged actions the user performed with the elevated access
2. Verify with the user or their manager whether this elevation was authorized and required for a specific operational task, incident response activity, or break-glass scenario documented in your organization's procedures
3. Review Azure.Audit logs for role assignments made during the elevated access period to identify if the user granted themselves or others persistent administrative roles that should be revoked, and check if the User Access Administrator role was properly removed after the task completion
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"Level": "4",
"callerIpAddress": "2.2.2.2",
"category": "AuditLogs",
"correlationId": "elevate-access-001",
"durationMs": 0,
"operationName": "User has elevated their access to User Access Administrator for their Azure Resources",
"operationVersion": "1.0",
"p_event_time": "2025-01-15 08:45:30.123",
"p_log_type": "Azure.Audit",
"properties": {
"activityDateTime": "2025-01-15T08:45:30.1234567Z",
"additionalDetails": [
{
"key": "Action",
"value": "Microsoft.Authorization/elevateAccess/action"
},
{
"key": "Scope",
"value": "/subscriptions/sub-123"
}
],
"initiatedBy": {
"user": {
"displayName": "Senior Administrator",
"id": "user-admin-789",
"ipAddress": "2.2.2.2",
"userPrincipalName": "denethor@lotr.com"
}
},
"loggedByService": "Microsoft.Authorization",
"operationType": "Assign"
},
"resourceId": "/subscriptions/sub-123/providers/Microsoft.Authorization",
"result": "SUCCESS",
"tenantId": "tenant-123",
"time": "2025-01-15 08:45:30.123"
}
Azure Virtual Machine Deleted
#Detects when an Azure Virtual Machine is deleted. VM deletion may indicate normal deprovisioning or could be part of a larger attack pattern to disrupt services.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | Activity Log event Microsoft.Compute/virtualMachines/delete: Deletes the virtual machine |
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
VIRTUAL_MACHINE_DELETE = "MICROSOFT.COMPUTE/VIRTUALMACHINES/DELETE"
def rule(event):
return event.get(
"operationName", ""
).upper() == VIRTUAL_MACHINE_DELETE and azure_activity_success(event)
def title(event):
resource_id = event.get("resourceId", "")
vmname = extract_resource_name_from_id(resource_id, "virtualMachines", default="<UNKNOWN_VM>")
return f"Azure Virtual Machine [{vmname}] deleted"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
resource_group = extract_resource_name_from_id(resource_id, "resourceGroups", default="")
if resource_group:
context["resource_group"] = resource_group
return context
Rule specification
AnalysisType: rule
Filename: azure_vm_deleted.py
RuleID: "Azure.MonitorActivity.VirtualMachine.Deleted"
DisplayName: "Azure Virtual Machine Deleted"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Info
Status: Experimental
Description: >
Detects when an Azure Virtual Machine is deleted.
VM deletion may indicate normal deprovisioning or could be part of a larger attack pattern to disrupt services.
Reports:
MITRE ATT&CK:
- TA0040:T1485 # Impact: Data Destruction
- TA0040:T1489 # Impact: Service Stop
Tags:
- Impact
- Data Destruction
- Service Stop
Runbook: |
1. Query Azure Monitor Activity logs for all compute resource operations by the callerIpAddress in the 24 hours before and after this alert to identify if multiple VMs are being deleted
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure resource deletions or service disruption activities from the same user or IP in the past 7 days to assess the scope of potential impact
Reference: https://learn.microsoft.com/en-us/rest/api/compute/virtual-machines/delete?view=rest-compute-2025-04-01&tabs=HTTP
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.COMPUTE/VIRTUALMACHINES/DELETEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.COMPUTE/VIRTUALMACHINES/DELETE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all compute resource operations by the callerIpAddress in the 24 hours before and after this alert to identify if multiple VMs are being deleted
2. Check if the source IP is associated with known cloud providers, VPN services, or corporate network ranges using threat intelligence
3. Search for other Azure resource deletions or service disruption activities from the same user or IP in the past 7 days to assess the scope of potential impact
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"location": "",
"operationName": "Microsoft.Compute/virtualMachines/delete",
"operationVersion": "2021-07-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.Compute/virtualMachines/myvm",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure Virtual Network Deleted
#Detects when an Azure Virtual Network (VNet) is deleted. VNet deletion removes the entire network infrastructure and disconnects all resources within it, causing significant service disruption. This may indicate ransomware activity, sabotage, or unauthorized infrastructure destruction.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | Activity Log event Microsoft.Network/virtualNetworks/delete: Deletes a virtual network |
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
VNET_DELETE = "MICROSOFT.NETWORK/VIRTUALNETWORKS/DELETE"
def rule(event):
return event.get("operationName", "").upper() == VNET_DELETE and azure_activity_success(event)
def title(event):
resource_id = event.get("resourceId", "")
vnet = extract_resource_name_from_id(resource_id, "virtualNetworks", default="<UNKNOWN_VNET>")
return f"Azure Virtual Network [{vnet}] deleted"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
vnet_name = extract_resource_name_from_id(resource_id, "virtualNetworks", default="")
if vnet_name:
context["vnet_name"] = vnet_name
resource_group = extract_resource_name_from_id(resource_id, "resourceGroups", default="")
if resource_group:
context["resource_group"] = resource_group
return context
Rule specification
AnalysisType: rule
Filename: azure_virtual_network_deleted.py
RuleID: "Azure.MonitorActivity.Network.VNetDeleted"
DisplayName: "Azure Virtual Network Deleted"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: High
Description: >
Detects when an Azure Virtual Network (VNet) is deleted.
VNet deletion removes the entire network infrastructure and disconnects all resources within it, causing significant service disruption.
This may indicate ransomware activity, sabotage, or unauthorized infrastructure destruction.
Reports:
MITRE ATT&CK:
- TA0040:T1485 # Impact: Data Destruction
- TA0040:T1499 # Impact: Endpoint Denial of Service
Runbook: |
1. Find all Azure Monitor Activity resource deletion operations by the callerIpAddress in the 24 hours before and after the alert to identify if multiple resources are being destroyed
2. Query for Azure Monitor Activity events related to the deleted VNet resourceId in the 6 hours before deletion to identify connected resources and assess service impact
3. Check if the callerIpAddress is associated with known VPN services or corporate IP ranges and compare to the caller's authentication patterns in the past 30 days
Reference: https://learn.microsoft.com/en-us/azure/virtual-network/manage-virtual-network
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.NETWORK/VIRTUALNETWORKS/DELETEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.NETWORK/VIRTUALNETWORKS/DELETE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Find all Azure Monitor Activity resource deletion operations by the callerIpAddress in the 24 hours before and after the alert to identify if multiple resources are being destroyed
2. Query for Azure Monitor Activity events related to the deleted VNet resourceId in the 6 hours before deletion to identify connected resources and assess service impact
3. Check if the callerIpAddress is associated with known VPN services or corporate IP ranges and compare to the caller's authentication patterns in the past 30 days
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"location": "",
"operationName": "Microsoft.Network/virtualNetworks/delete",
"operationVersion": "2021-04-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/myresourcegroup/providers/Microsoft.Network/virtualNetworks/myvnet",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2024-12-17T10:30:00.0000000Z"
}
Azure VM Command Executed
#Detects when commands are executed on Azure virtual machines through multiple execution methods including RunCommand, VM extensions (CustomScriptExtension, DSC), gallery applications, AKS command invoke, VMSS run commands, and serial console access. Adversaries may abuse these capabilities to execute unauthorized commands, deploy malware, establish persistence, or move laterally within the environment.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Execution |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
# AZT301.1 - RunCommand
VM_RUN_COMMAND = "MICROSOFT.COMPUTE/VIRTUALMACHINES/RUNCOMMAND/ACTION"
# AZT301.2 & AZT301.3 - CustomScriptExtension and DSC
VM_EXTENSIONS_WRITE = "MICROSOFT.COMPUTE/VIRTUALMACHINES/EXTENSIONS/WRITE"
# AZT301.4 - Compute Gallery Application
GALLERY_APP_WRITE = "MICROSOFT.COMPUTE/GALLERIES/APPLICATIONS/VERSIONS/WRITE"
# AZT301.5 - AKS Command Invoke
AKS_RUN_COMMAND = "MICROSOFT.CONTAINERSERVICE/MANAGEDCLUSTERS/RUNCOMMAND/ACTION"
# AZT301.6 - VMSS Run Command
VMSS_RUN_COMMAND = "MICROSOFT.COMPUTE/VIRTUALMACHINESCALESETS/VIRTUALMACHINES/RUNCOMMAND/ACTION"
# AZT301.7 - Serial Console
SERIAL_CONSOLE_CONNECT = "MICROSOFT.SERIALCONSOLE/SERIALPORTS/CONNECT/ACTION"
AZT301_OPERATIONS = [
VM_RUN_COMMAND,
VM_EXTENSIONS_WRITE,
GALLERY_APP_WRITE,
AKS_RUN_COMMAND,
VMSS_RUN_COMMAND,
SERIAL_CONSOLE_CONNECT,
]
def rule(event):
operation = event.get("operationName", "").upper()
return all([operation in AZT301_OPERATIONS, azure_activity_success(event)])
# Map operations to (resource_type_key, display_name, technique_name)
OPERATION_METADATA = {
VM_RUN_COMMAND: ("virtualMachines", "Virtual Machine", "RunCommand"),
VM_EXTENSIONS_WRITE: ("virtualMachines", "VM Extension", "Extension"),
GALLERY_APP_WRITE: ("applications", "Gallery Application", "Gallery Application"),
AKS_RUN_COMMAND: ("managedClusters", "AKS Cluster", "AKS Command"),
VMSS_RUN_COMMAND: ("virtualMachineScaleSets", "VM Scale Set", "VMSS RunCommand"),
SERIAL_CONSOLE_CONNECT: ("virtualMachines", "Serial Console", "Serial Console"),
}
def title(event):
resource_id = event.get("resourceId", "")
operation = event.get("operationName", "").upper()
if operation in OPERATION_METADATA:
resource_type_key, _, technique = OPERATION_METADATA[operation]
resource_name = extract_resource_name_from_id(
resource_id, resource_type_key, default="<UNKNOWN_RESOURCE>"
)
else:
technique = "Command"
resource_name = "<UNKNOWN_RESOURCE>"
return f"Azure VM {technique} Executed on [{resource_name}]"
def alert_context(event):
context = azure_activity_alert_context(event)
return context
Rule specification
AnalysisType: rule
Filename: azure_vm_command_executed.py
RuleID: "Azure.MonitorActivity.Compute.VMCommandExecuted"
DisplayName: "Azure VM Command Executed"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Info
Description: >
Detects when commands are executed on Azure virtual machines through multiple execution methods
including RunCommand, VM extensions (CustomScriptExtension, DSC), gallery applications, AKS command
invoke, VMSS run commands, and serial console access. Adversaries may abuse these capabilities to
execute unauthorized commands, deploy malware, establish persistence, or move laterally within the environment.
Reports:
MITRE ATT&CK:
- TA0002:T1651 # Execution: Cloud Administration Command
Tags:
- AZT301
- AZT301.1
- AZT301.2
- AZT301.3
- AZT301.4
- AZT301.5
- AZT301.6
- AZT301.7
- Execution
- Cloud Administration Command
Runbook: |
1. Query Azure Monitor Activity logs for all VM-related command execution operations (RunCommand, extensions, AKS commands, VMSS commands, serial console) by the callerIpAddress in the 24 hours before and after the alert to identify the full scope of activity
2. Check if the same callerIpAddress has performed other suspicious activities such as reconnaissance (reading VMs, IPs, NSGs) or privilege escalation in the 6 hours before and after the alert
3. Verify if the callerIpAddress and caller identity have a history of legitimate VM administration in the past 90 days to distinguish between authorized admin activity and potential compromise
Reference: https://microsoft.github.io/Azure-Threat-Research-Matrix/Execution/AZT301/AZT301/
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameis one ofMICROSOFT.COMPUTE/VIRTUALMACHINES/RUNCOMMAND/ACTION,MICROSOFT.COMPUTE/VIRTUALMACHINES/EXTENSIONS/WRITE,MICROSOFT.COMPUTE/GALLERIES/APPLICATIONS/VERSIONS/WRITE,MICROSOFT.CONTAINERSERVICE/MANAGEDCLUSTERS/RUNCOMMAND/ACTION,MICROSOFT.COMPUTE/VIRTUALMACHINESCALESETS/VIRTUALMACHINES/RUNCOMMAND/ACTIONresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | in |
| field:"operationName" kind:in |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all VM-related command execution operations (RunCommand, extensions, AKS commands, VMSS commands, serial console) by the callerIpAddress in the 24 hours before and after the alert to identify the full scope of activity
2. Check if the same callerIpAddress has performed other suspicious activities such as reconnaissance (reading VMs, IPs, NSGs) or privilege escalation in the 6 hours before and after the alert
3. Verify if the callerIpAddress and caller identity have a history of legitimate VM administration in the past 90 days to distinguish between authorized admin activity and potential compromise
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Informational",
"location": "eastus",
"operationName": "MICROSOFT.COMPUTE/VIRTUALMACHINES/RUNCOMMAND/ACTION",
"operationVersion": "2021-11-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/compute-rg/providers/Microsoft.Compute/virtualMachines/prod-vm-01",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-22T10:30:00.0000000Z"
}
Azure VM Disk SAS URI Generated
#Detects when a Shared Access Signature (SAS) URI is generated for an Azure VM disk. SAS URIs provide time-limited, unauthenticated access to download disk contents directly from Azure Storage. Adversaries can generate SAS URIs to exfiltrate entire virtual machine disks, including operating systems, applications, and all data. This allows offline analysis to extract credentials, secrets, and sensitive data without detection. This is a critical indicator of data exfiltration and should be investigated immediately.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Collection | |
| Exfiltration |
Telemetry coverage
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
DISK_SAS_OPERATIONS = [
"MICROSOFT.COMPUTE/DISKS/BEGINGETACCESS/ACTION",
]
def rule(event):
return event.get("operationName", "").upper() in DISK_SAS_OPERATIONS and azure_activity_success(
event
)
def title(event):
resource_id = event.get("resourceId", "<UNKNOWN_RESOURCE>")
disk_name = extract_resource_name_from_id(resource_id, "disks", default="<UNKNOWN_DISK>")
return f"Azure VM Disk SAS URI Generated: [{disk_name}]"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
disk_name = extract_resource_name_from_id(resource_id, "disks", default="")
if disk_name:
context["disk_name"] = disk_name
resource_group = extract_resource_name_from_id(resource_id, "resourceGroups", default="")
if resource_group:
context["resource_group"] = resource_group
return context
Rule specification
AnalysisType: rule
Filename: azure_vm_disk_sas_uri_generated.py
RuleID: "Azure.MonitorActivity.Compute.DiskSASURIGenerated"
DisplayName: "Azure VM Disk SAS URI Generated"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: High
Description: >
Detects when a Shared Access Signature (SAS) URI is generated for an Azure VM disk.
SAS URIs provide time-limited, unauthenticated access to download disk contents directly from Azure Storage.
Adversaries can generate SAS URIs to exfiltrate entire virtual machine disks, including operating systems,
applications, and all data. This allows offline analysis to extract credentials, secrets, and sensitive data
without detection. This is a critical indicator of data exfiltration and should be investigated immediately.
Reports:
MITRE ATT&CK:
- TA0010:T1567.002 # Exfiltration: Exfiltration Over Web Service - Exfiltration to Cloud Storage
- TA0009:T1530 # Collection: Data from Cloud Storage
Tags:
- AZT701.1
- Exfiltration
- Collection
- Exfiltration Over Web Service
- Exfiltration to Cloud Storage
- Data from Cloud Storage
Runbook: |
1. Find all disk and storage operations by the callerIpAddress in the 24 hours before and after this alert to identify if multiple disks are being exfiltrated
2. Query for the VM associated with this disk to determine what applications and data are stored on the disk
3. Check if the callerIpAddress has performed similar disk export operations in the past 90 days to determine if this is expected backup behavior
Reference: https://microsoft.github.io/Azure-Threat-Research-Matrix/Impact/AZT701/AZT701-1
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameis one ofMICROSOFT.COMPUTE/DISKS/BEGINGETACCESS/ACTIONresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | in |
| field:"operationName" kind:in value:"MICROSOFT.COMPUTE/DISKS/BEGINGETACCESS/ACTION" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Find all disk and storage operations by the callerIpAddress in the 24 hours before and after this alert to identify if multiple disks are being exfiltrated
2. Query for the VM associated with this disk to determine what applications and data are stored on the disk
3. Check if the callerIpAddress has performed similar disk export operations in the past 90 days to determine if this is expected backup behavior
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Informational",
"location": "eastus",
"operationName": "Microsoft.Compute/disks/beginGetAccess/action",
"operationVersion": "2021-04-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/vm-rg/providers/Microsoft.Compute/disks/vm-prod-osdisk",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-23T10:30:00.0000000Z"
}
Azure VM Snapshot Deleted
#Detects when an Azure disk snapshot is deleted. Snapshots serve critical functions for backup, disaster recovery, and forensic analysis. Adversaries may target snapshots to prevent data recovery, destroy forensic evidence, or undermine backup strategies before launching ransomware or destructive operations.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | Activity Log event Microsoft.Compute/snapshots/delete: Delete a Snapshot |
Detection logic
from panther_azureactivity_helpers import (
azure_activity_alert_context,
azure_activity_success,
extract_resource_name_from_id,
)
SNAPSHOT_DELETE = "MICROSOFT.COMPUTE/SNAPSHOTS/DELETE"
def rule(event):
return event.get("operationName", "").upper() == SNAPSHOT_DELETE and azure_activity_success(
event
)
def title(event):
resource_id = event.get("resourceId", "")
snapshot_name = extract_resource_name_from_id(
resource_id, "snapshots", default="<UNKNOWN_SNAPSHOT>"
)
return f"Azure VM Snapshot [{snapshot_name}] deleted"
def alert_context(event):
context = azure_activity_alert_context(event)
resource_id = event.get("resourceId", "")
snapshot_name = extract_resource_name_from_id(resource_id, "snapshots", default="")
if snapshot_name:
context["snapshot_name"] = snapshot_name
resource_group = extract_resource_name_from_id(resource_id, "resourceGroups", default="")
if resource_group:
context["resource_group"] = resource_group
return context
Rule specification
AnalysisType: rule
Filename: azure_vm_snapshot_deleted.py
RuleID: "Azure.MonitorActivity.Compute.SnapshotDeleted"
DisplayName: "Azure VM Snapshot Deleted"
Enabled: true
LogTypes:
- Azure.MonitorActivity
Severity: Low
Description: >
Detects when an Azure disk snapshot is deleted. Snapshots serve critical functions for backup,
disaster recovery, and forensic analysis. Adversaries may target snapshots to prevent data
recovery, destroy forensic evidence, or undermine backup strategies before launching ransomware
or destructive operations.
Reports:
MITRE ATT&CK:
- TA0040:T1485 # Impact: Data Destruction
- TA0040:T1490 # Impact: Inhibit System Recovery
Tags:
- Impact
- Data Destruction
- Inhibit System Recovery
- Ransomware
Runbook: |
1. Query Azure Monitor Activity logs for all backup and recovery operations (snapshot deletions, restore point deletions, disk deletions) by the callerIpAddress in the 24 hours before and after the alert
2. Find all snapshot deletions across all subscriptions in the past 6 hours to determine if this is part of a pre-ransomware attack pattern
3. Check if the callerIpAddress has deleted snapshots in the past 90 days to establish if this is normal maintenance activity
Reference: https://github.com/elastic/detection-rules/blob/main/rules/integrations/azure/impact_azure_compute_vm_snapshot_deletion.toml
SummaryAttributes:
- resourceId
- callerIpAddress
- correlationId
Stages and Predicates
Fires on Azure.MonitorActivity events when all of the conditions below hold.
Condition
operationNameisMICROSOFT.COMPUTE/SNAPSHOTS/DELETEresultTypeis one ofSuccess,Succeeded
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operationName | eq |
| field:"operationName" kind:eq value:"MICROSOFT.COMPUTE/SNAPSHOTS/DELETE" |
resultType | in |
| field:"resultType" kind:in |
Response runbook
1. Query Azure Monitor Activity logs for all backup and recovery operations (snapshot deletions, restore point deletions, disk deletions) by the callerIpAddress in the 24 hours before and after the alert
2. Find all snapshot deletions across all subscriptions in the past 6 hours to determine if this is part of a pre-ransomware attack pattern
3. Check if the callerIpAddress has deleted snapshots in the past 90 days to establish if this is normal maintenance activity
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"callerIpAddress": "1.1.1.1",
"category": "Administrative",
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"level": "Informational",
"location": "eastus",
"operationName": "MICROSOFT.COMPUTE/SNAPSHOTS/DELETE",
"operationVersion": "2021-12-01",
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/backup-rg/providers/Microsoft.Compute/snapshots/vm-backup-snapshot-20251222",
"resultSignature": "200",
"resultType": "Success",
"tenantId": "87654321-4321-4321-4321-111111111111",
"time": "2025-12-22T10:30:00.0000000Z"
}
Azure VS Code OAuth Phishing
#Detects OAuth authorization flows where Visual Studio Code successfully authenticates to Microsoft Graph. While legitimate for developers, this pattern is commonly abused in phishing campaigns where attackers use the trusted VS Code client ID to trick users into granting OAuth tokens.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access |
Telemetry coverage
Detection logic
from panther_azuresignin_helpers import (
actor_user,
azure_signin_alert_context,
azure_signin_success,
is_sign_in_event,
)
# Visual Studio Code first-party application ID
VSCODE_APP_ID = "aebc6443-996d-45c2-90f0-388ff96faa56"
# Microsoft Graph resource ID
MS_GRAPH_RESOURCE_ID = "00000003-0000-0000-c000-000000000000"
def rule(event):
if not is_sign_in_event(event) or not azure_signin_success(event):
return False
# Check if Visual Studio Code application is being used
app_id = event.deep_get("properties", "appId", default="")
user_agent = event.deep_get("properties", "userAgent", default="").lower()
is_vscode = app_id == VSCODE_APP_ID or "visual studio code" in user_agent
# Check if accessing Microsoft Graph
resource_id = event.deep_get("properties", "resourceId", default="")
resource_name = event.deep_get("properties", "resourceDisplayName", default="").lower()
accessing_graph = resource_id == MS_GRAPH_RESOURCE_ID or "microsoft graph" in resource_name
# Alert on VS Code OAuth to Microsoft Graph
return is_vscode and accessing_graph
def title(event):
principal = actor_user(event)
if principal is None:
principal = "<NO_PRINCIPALNAME>"
ip_address = event.deep_get("properties", "ipAddress", default="<UNKNOWN_IP>")
return f"VS Code OAuth to Microsoft Graph: [{principal}] from [{ip_address}]"
def alert_context(event):
context = azure_signin_alert_context(event)
# Add OAuth phishing-specific context
context["app_id"] = event.deep_get("properties", "appId", default="<NO_APP_ID>")
context["app_display_name"] = event.deep_get("properties", "appDisplayName", default="<NO_APP>")
context["user_agent"] = event.deep_get("properties", "userAgent", default="<NO_USER_AGENT>")
context["authentication_protocol"] = event.deep_get(
"properties", "authenticationProtocol", default="<NO_PROTOCOL>"
)
context["token_issuer_type"] = event.deep_get(
"properties", "tokenIssuerType", default="<NO_ISSUER_TYPE>"
)
context["is_interactive"] = event.deep_get("properties", "isInteractive", default=None)
return context
Rule specification
AnalysisType: rule
Filename: azure_vscode_oauth_phishing.py
RuleID: "Azure.Audit.VSCodeOAuthPhishing"
DisplayName: "Azure VS Code OAuth Phishing"
Enabled: true
Status: Experimental
LogTypes:
- Azure.Audit
Severity: Medium
Description: >
Detects OAuth authorization flows where Visual Studio Code successfully authenticates to
Microsoft Graph. While legitimate for developers, this pattern is commonly abused in
phishing campaigns where attackers use the trusted VS Code client ID to trick users into
granting OAuth tokens.
Reports:
MITRE ATT&CK:
- TA0001:T1566
- TA0006:T1528
Runbook: |
1. Query Azure.Audit sign-in logs for all VS Code OAuth events by properties:userPrincipalName in the 24 hours before and after the alert to identify usage patterns
2. Check if callerIpAddress is associated with known VPN services or matches the user's typical geographic locations and corporate network ranges
3. Find other OAuth consent grants or application authentications for this user in the past 7 days to determine if multiple suspicious OAuth flows are occurring
Reference: https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-token-protection
SummaryAttributes:
- properties:userPrincipalName
- callerIpAddress
- properties:appDisplayName
- properties:resourceDisplayName
- properties:userAgent
Stages and Predicates
Fires on Azure.Audit events when all of the conditions below hold.
Condition
operationNameisSign-in activityresultSignatureisSUCCESSany of:
properties.appIdisaebc6443-996d-45c2-90f0-388ff96faa56properties.userAgentcontainsvisual studio code
any of:
properties.resourceIdis00000003-0000-0000-c000-000000000000properties.resourceDisplayNamecontainsmicrosoft graph
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
operationName | ne | Sign-in activity | excludes:operationName field:"operationName" value:"Sign-in activity" |
resultSignature | ne | SUCCESS | excludes:resultSignature field:"resultSignature" value:"SUCCESS" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
properties.appId | eq |
| field:"properties.appId" kind:eq value:"aebc6443-996d-45c2-90f0-388ff96faa56" |
properties.resourceDisplayName | contains |
| field:"properties.resourceDisplayName" kind:contains value:"microsoft graph" |
properties.resourceId | eq |
| field:"properties.resourceId" kind:eq value:"00000003-0000-0000-c000-000000000000" |
properties.userAgent | contains |
| field:"properties.userAgent" kind:contains value:"visual studio code" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
ipAddress | properties.ipAddress |
Response runbook
1. Query Azure.Audit sign-in logs for all VS Code OAuth events by properties:userPrincipalName in the 24 hours before and after the alert to identify usage patterns
2. Check if callerIpAddress is associated with known VPN services or matches the user's typical geographic locations and corporate network ranges
3. Find other OAuth consent grants or application authentications for this user in the past 7 days to determine if multiple suspicious OAuth flows are occurring
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"Level": "4",
"callerIpAddress": "9.9.9.9",
"category": "NonInteractiveUserSignInLogs",
"correlationId": "vscode-123-456-789",
"durationMs": 200,
"location": "US",
"operationName": "Sign-in activity",
"operationVersion": "1.0",
"p_event_time": "2025-01-15 14:30:25.123",
"p_log_type": "Azure.Audit",
"properties": {
"appDisplayName": "Visual Studio Code",
"appId": "aebc6443-996d-45c2-90f0-388ff96faa56",
"authenticationProtocol": "oAuth2",
"clientAppUsed": "Mobile Apps and Desktop clients",
"conditionalAccessStatus": "notApplied",
"correlationId": "vscode-123-456-789",
"createdDateTime": "2025-01-15T14:30:25.1234567Z",
"ipAddress": "9.9.9.9",
"isInteractive": false,
"location": {
"city": "Seattle",
"countryOrRegion": "US",
"geoCoordinates": {
"latitude": 47.6062,
"longitude": -122.3321
},
"state": "Washington"
},
"resourceDisplayName": "Microsoft Graph",
"resourceId": "00000003-0000-0000-c000-111111111111",
"status": {
"errorCode": 0
},
"tokenIssuerType": "AzureAD",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Visual Studio Code/1.75.0 Chrome/102.0.5005.167 Electron/19.1.9 Safari/537.36",
"userId": "user-abc-123",
"userPrincipalName": "sam@lotr.com"
},
"resourceId": "/tenants/tenant-123/providers/Microsoft.aadiam",
"resultSignature": "SUCCESS",
"resultType": "0",
"tenantId": "tenant-123",
"time": "2025-01-15 14:30:25.123"
}