Detection rules › Panther

Panther rules: azure

RuleSeverity
Azure Action Groups Deletedinformational
Azure Advisor Security Recommendation Availableinformational
Azure Alert Rules Deletedmedium
Azure Alert Suppression Rule Created or Modifiedlow
Azure Authentication Methods Policy OIDC Discovery URL Changedhigh
Azure Automation Account Createdinformational
Azure Automation Runbook Created or Modifiedinformational
Azure Automation Runbook Deletedinformational
Azure Automation Schedule Created or Modifiedinformational
Azure Automation Webhook Createdinformational
Azure Device Code Authentication with Broker Clientmedium
Azure Diagnostic Settings Deletedmedium
Azure Disk Deletedinformational
Azure Domain Federation Settings Modifiedmedium
Azure Event Hub Deletedhigh
Azure Excessive Account Lockoutshigh
Azure Excessive IP and VM Discoverymedium
Azure Excessive Network Security Group Readmedium
Azure Firewall Policy Deletedhigh
Azure High-Risk Sign-Inhigh
Azure Invite External Userslow
Azure Key Vault Certificate Accessedinformational
Azure Key Vault Deletedmedium
Azure Key Vault Key Accessed or Recoveredmedium
Azure Key Vault Key Permanently Purgedinformational
Azure Key Vault Permanently Purgedlow
Azure Key Vault Secret Accessed or Recoveredmedium
Azure Kubernetes RoleBinding or ClusterRoleBinding Createdmedium
Azure Log Analytics Workspace Deletedmedium
Azure Many Failed SignInsmedium
Azure MFA Disabledhigh
Azure Microsoft Graph Single Session from Multiple IP Addressesmedium
Azure Network Packet Capture Enabledmedium
Azure Network Security Configuration Modified or Deletedmedium
Azure Network Watcher Deletedmedium
Azure Policy Changedlow
Azure Policy DeployIfNotExists Action Triggeredmedium
Azure Policy Violation Detectedinformational
Azure Privileged or Elevated Role Assignmentmedium
Azure Protection Multiple Alerts for Userhigh
Azure Recovery Services Protection Container Deletedhigh
Azure Resource Group Deletedmedium
Azure Resource Lock Deletedhigh
Azure Restore Point Collection Deletedmedium
Azure RiskLevel Passthroughmedium
Azure Role Changed PIMmedium
Azure ROPC Login Attempt Without MFAmedium
Azure Serverless Script Executioninformational
Azure Service Principal Credentials Addedmedium
Azure SignIn via Legacy Authentication Protocolmedium
Azure SQL Server Deletedinformational
Azure Storage Account Blob Versioning Disabledhigh
Azure Storage Account Deletedhigh
Azure Storage Account HTTPS-Only Traffic Disabledhigh
Azure Storage Account Key Regeneratedinformational
Azure Storage Account Keys Listedmedium
Azure Storage Account Public Network Access Enabledhigh
Azure Storage Account Shared Key Access Enabledmedium
Azure Storage Blob Anonymous Access Enabledhigh
Azure Storage Blob Bulk Extractionmedium
Azure Storage Blob Container Permissions Modifiedmedium
Azure Storage Blob CPK Encryption Detectedhigh
Azure Storage Blob Deletionmedium
Azure Storage Blob Soft Delete Disabledhigh
Azure Storage Blob Upload WITH CPK Encryption Errorhigh
Azure Storage Blob Uploadedinformational
Azure Storage Container Soft Delete Disabledhigh
Azure Storage File Share Created or Modifiedinformational
Azure Storage Immutability Policy Deletedhigh
Azure Storage SAS Token Access from External IPlow
Azure User Elevated to User Access Administrator Rolehigh
Azure Virtual Machine Deletedinformational
Azure Virtual Network Deletedhigh
Azure VM Command Executedinformational
Azure VM Disk SAS URI Generatedhigh
Azure VM Snapshot Deletedlow
Azure VS Code OAuth Phishingmedium

Azure Action Groups Deleted

#
Severity
informational
Log types
Azure.MonitorActivity
Tags
Defense Evasion, Impair Defenses, Disable Cloud Logs
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

Detects when Azure action groups are deleted, which could disable alert notifications to security teams to prevent incident response.

MITRE ATT&CK coverage

Telemetry coverage

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

  • operationName is MICROSOFT.INSIGHTS/ACTIONGROUPS/DELETE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.INSIGHTS/ACTIONGROUPS/DELETE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.INSIGHTS/ACTIONGROUPS/DELETE"
resultTypein
  • Succeeded
  • Success
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

#
Status
Experimental
Severity
informational
Log types
Azure.MonitorActivity
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

  • operationName is MICROSOFT.ADVISOR/RECOMMENDATIONS/AVAILABLE/ACTION (case-insensitive)
  • category is Recommendation
  • properties.recommendationCategory is Security

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
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

#
Severity
medium
Log types
Azure.MonitorActivity
Tags
Defense Evasion, Impair Defenses, Disable Cloud Logs
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

Detects when Azure alert rules are deleted. Deleting alert rules disables security notifications and is a common defense evasion technique.

MITRE ATT&CK coverage

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

  • operationName is one of MICROSOFT.INSIGHTS/ALERTRULES/DELETE, MICROSOFT.INSIGHTS/METRICALERTS/DELETE
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNamein
  • MICROSOFT.INSIGHTS/ALERTRULES/DELETE
  • MICROSOFT.INSIGHTS/METRICALERTS/DELETE
field:"operationName" kind:in
resultTypein
  • Succeeded
  • Success
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

#
Severity
low
Log types
Azure.MonitorActivity
Tags
Defense Evasion, Impair Defenses
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
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

  • operationName is MICROSOFT.SECURITY/ALERTSSUPPRESSIONRULES/WRITE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.SECURITY/ALERTSSUPPRESSIONRULES/WRITE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.SECURITY/ALERTSSUPPRESSIONRULES/WRITE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
high
Log types
Azure.Audit
Tags
Persistence, Modify Authentication Process
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

Telemetry coverage

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

  • operationName contains authentication methods policy update (case-insensitive)

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.

FieldKindValuesSearch
operationNamecontains
  • authentication methods policy update transforms: tolower
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.

FieldSource
userPrincipalNameproperties.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

#
Severity
informational
Log types
Azure.MonitorActivity
Tags
Persistence, Defense Evasion, Valid Accounts
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
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

  • operationName is MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WRITE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WRITE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WRITE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
informational
Log types
Azure.MonitorActivity
Tags
AZT302, AZT302.1, AZT302.2, AZT302.3, AZT601, AZT601.5, AZT602, AZT605.2, Persistence, Execution, Cloud Accounts, Command and Scripting Interpreter
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is one of MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/DRAFT/WRITE, MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/WRITE, MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/PUBLISH/ACTION (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNamein
  • MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/DRAFT/WRITE transforms: toupper
  • MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/PUBLISH/ACTION transforms: toupper
  • MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/WRITE transforms: toupper
field:"operationName" kind:in
resultTypein
  • Succeeded
  • Success
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

#
Severity
informational
Log types
Azure.MonitorActivity
Tags
Defense Evasion, Indicator Removal
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
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

  • operationName is MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/DELETE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/DELETE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/RUNBOOKS/DELETE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
informational
Log types
Azure.MonitorActivity
Tags
AZT505, Persistence, Privilege Escalation, Scheduled Task, Exploitation for Privilege Escalation
Reference
microsoft.github.io
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is one of MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/SCHEDULES/WRITE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNamein
  • MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/SCHEDULES/WRITE transforms: toupper
field:"operationName" kind:in value:"MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/SCHEDULES/WRITE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
informational
Log types
Azure.MonitorActivity
Tags
AZT502, AZT503, AZT503.3, Persistence, Resource Development, Event Triggered Execution, Stage Capabilities
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
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

  • operationName is one of MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WEBHOOKS/ACTION, MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WEBHOOKS/WRITE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNamein
  • MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WEBHOOKS/ACTION transforms: toupper
  • MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/WEBHOOKS/WRITE transforms: toupper
field:"operationName" kind:in
resultTypein
  • Succeeded
  • Success
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

#
Severity
medium
Log types
Azure.Audit
Tags
Initial Access, Phishing, Use Alternate Authentication Material, Valid Accounts
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is Sign-in activity
  • resultSignature is SUCCESS
  • properties.authenticationProtocol is devicecode
  • any of:
    • properties.conditionalAccessAudiences.applicationId contains 29d9ed98-a469-4536-ade2-f981bc1d605e
    • properties.conditionalAccessAudiences.applicationId is 29d9ed98-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.

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.

FieldSource
userPrincipalNameproperties.userPrincipalName
ipAddressproperties.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

#
Severity
medium
Log types
Azure.MonitorActivity
Tags
Defense Evasion, Impair Defenses, Disable Cloud Logs
Reference
docs.datadoghq.com
Source
github.com/panther-labs/panther-analysis

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

Telemetry coverage

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

  • operationName is MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE"
resultTypein
  • Succeeded
  • Success
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

#
Status
Experimental
Severity
informational
Log types
Azure.MonitorActivity
Tags
Impact, Data Destruction, Inhibit System Recovery, Ransomware
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

Telemetry coverage

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

  • operationName is MICROSOFT.COMPUTE/DISKS/DELETE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.COMPUTE/DISKS/DELETE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.COMPUTE/DISKS/DELETE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
medium
Log types
Azure.Audit
Tags
Persistence, Modify Authentication Process
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

Telemetry coverage

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:
    • operationName contains set federation settings on domain (case-insensitive)
    • all of:
      • operationName contains set domain authentication (case-insensitive)
      • properties.targetResources.modifiedProperties.displayName contains LiveType

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.

FieldKindValuesSearch
operationNamecontains
  • set domain authentication transforms: tolower
  • set federation settings on domain transforms: tolower
field:"operationName" kind:contains
properties.targetResources.modifiedProperties.displayNamecontains
  • LiveType
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.

FieldSource
userPrincipalNameproperties.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

#
Severity
high
Log types
Azure.MonitorActivity
Tags
Defense Evasion, Impair Defenses, Disable Cloud Logs
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is MICROSOFT.EVENTHUB/NAMESPACES/EVENTHUBS/DELETE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.EVENTHUB/NAMESPACES/EVENTHUBS/DELETE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.EVENTHUB/NAMESPACES/EVENTHUBS/DELETE"
resultTypein
  • Succeeded
  • Success
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

#
Status
Experimental
Severity
high
Log types
Azure.Audit
Tags
Credential Access, Brute Force
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

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

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.errorCode is 50053
  • properties.autonomousSystemNumber is not one of 8075, 8068, 8069, 8070, 12076
  • properties.userPrincipalName is present
Alert cadence
alerts after 20 matches within 1h

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
properties.autonomousSystemNumberin12076, 8068, 8069, 8070, 8075excludes: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.

FieldSource
userPrincipalNameproperties.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

#
Severity
medium
Log types
Azure.MonitorActivity
Tags
AZT102, Discovery, Reconnaissance, Network Service Discovery, Remote System Discovery, Active Scanning, Vulnerability Scanning
Reference
microsoft.github.io
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is one of MICROSOFT.NETWORK/PUBLICIPADDRESSES/READ, MICROSOFT.COMPUTE/VIRTUALMACHINES/READ
  • resultType is one of Success, Succeeded
Alert cadence
alerts after 50 matches within 1h

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNamein
  • MICROSOFT.COMPUTE/VIRTUALMACHINES/READ
  • MICROSOFT.NETWORK/PUBLICIPADDRESSES/READ
field:"operationName" kind:in
resultTypein
  • Succeeded
  • Success
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

#
Severity
medium
Log types
Azure.MonitorActivity
Tags
AZT101, Discovery, Reconnaissance, Network Service Discovery, Active Scanning, Vulnerability Scanning
Reference
microsoft.github.io
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is MICROSOFT.NETWORK/NETWORKSECURITYGROUP/READ (case-insensitive)
  • resultType is one of Success, Succeeded
Alert cadence
alerts after 50 matches within 1h

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.NETWORK/NETWORKSECURITYGROUP/READ transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.NETWORK/NETWORKSECURITYGROUP/READ"
resultTypein
  • Succeeded
  • Success
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

#
Severity
high
Log types
Azure.MonitorActivity
Tags
Defense Evasion, Impair Defenses, Disable or Modify System Firewall
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
Defense Impairment

Telemetry coverage

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

  • operationName is MICROSOFT.NETWORK/FIREWALLPOLICIES/DELETE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.NETWORK/FIREWALLPOLICIES/DELETE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.NETWORK/FIREWALLPOLICIES/DELETE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
high
Log types
Azure.Audit
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
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

  • operationName is Sign-in activity
  • properties.riskState is not one of dismissed, remediated
  • any of:
    • properties.riskLevelDuringSignIn is high
    • properties.riskLevelAggregated is high

Exclusions

The rule actively suppresses these predicates.

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.

FieldSource
ipAddressproperties.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

#
Severity
low
Log types
Azure.Audit
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

This detection looks for a Azure users inviting external users

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Telemetry coverage

PlatformRecord / event type
AzureInvite external user

Rules detecting the same action

These rules filter on the same operation.

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.result is success
  • operationName is Invite 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.

FieldKindExcluded valuesSearch
operationNameneInvite external userexcludes:operationName field:"operationName" value:"Invite external user"
properties.resultnesuccessexcludes:properties.result field:"properties.result" value:"success"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
operationName
categoryproperties.category
actor_idproperties.initiatedBy.user.id
actor_upnproperties.initiatedBy.user.userPrincipalName
source_ip_addressproperties.initiatedBy.user.ipAddress
target_idproperties.targetResources.id
target_nameproperties.targetResources.displayName
valueproperties.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

#
Severity
informational
Log types
Azure.MonitorActivity
Tags
AZT604, AZT604.2, Credential Access, Collection, Credentials from Password Stores, Data from Cloud Storage
Reference
microsoft.github.io
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is MICROSOFT.KEYVAULT/VAULTS/CERTIFICATES/READ
  • resultType is one of Success, Succeeded

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
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

#
Severity
medium
Log types
Azure.MonitorActivity
Tags
Impact, Data Destruction, Inhibit System Recovery, Ransomware
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

Telemetry coverage

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

  • operationName is MICROSOFT.KEYVAULT/VAULTS/DELETE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.KEYVAULT/VAULTS/DELETE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.KEYVAULT/VAULTS/DELETE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
medium
Log types
Azure.MonitorActivity
Tags
AZT604, AZT604.3, AZT704, AZT704.1, Credential Access, Discovery, Credentials from Password Stores, Account Discovery, Cloud Account
Reference
microsoft.github.io
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is one of MICROSOFT.KEYVAULT/VAULTS/KEYS/READ/ACTION, MICROSOFT.KEYVAULT/VAULTS/KEYS/RESTORE/ACTION, MICROSOFT.KEYVAULT/VAULTS/KEYS/RECOVER/ACTION
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNamein
  • MICROSOFT.KEYVAULT/VAULTS/KEYS/READ/ACTION
  • MICROSOFT.KEYVAULT/VAULTS/KEYS/RECOVER/ACTION
  • MICROSOFT.KEYVAULT/VAULTS/KEYS/RESTORE/ACTION
field:"operationName" kind:in
resultTypein
  • Succeeded
  • Success
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

#
Severity
informational
Log types
Azure.MonitorActivity
Tags
Impact, Data Destruction, Inhibit System Recovery, Ransomware
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is MICROSOFT.KEYVAULT/VAULTS/KEYS/PURGE/ACTION (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.KEYVAULT/VAULTS/KEYS/PURGE/ACTION transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.KEYVAULT/VAULTS/KEYS/PURGE/ACTION"
resultTypein
  • Succeeded
  • Success
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

#
Severity
low
Log types
Azure.MonitorActivity
Tags
Impact, Data Destruction, Inhibit System Recovery, Ransomware
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is MICROSOFT.KEYVAULT/LOCATIONS/DELETEDVAULTS/PURGE/ACTION (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.KEYVAULT/LOCATIONS/DELETEDVAULTS/PURGE/ACTION transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.KEYVAULT/LOCATIONS/DELETEDVAULTS/PURGE/ACTION"
resultTypein
  • Succeeded
  • Success
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

#
Severity
medium
Log types
Azure.MonitorActivity
Tags
AZT604.1, AZT704.3, Credential Access, Collection, Credentials from Password Stores, Data from Cloud Storage
Reference
blog.pwnedlabs.io
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is one of MICROSOFT.KEYVAULT/VAULTS/SECRETS/GETSECRET/ACTION, MICROSOFT.KEYVAULT/VAULTS/SECRETS/RECOVER/ACTION, MICROSOFT.KEYVAULT/VAULTS/SECRETS/RESTORE/ACTION
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNamein
  • MICROSOFT.KEYVAULT/VAULTS/SECRETS/GETSECRET/ACTION
  • MICROSOFT.KEYVAULT/VAULTS/SECRETS/RECOVER/ACTION
  • MICROSOFT.KEYVAULT/VAULTS/SECRETS/RESTORE/ACTION
field:"operationName" kind:in
resultTypein
  • Succeeded
  • Success
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

#
Severity
medium
Log types
Azure.MonitorActivity
Tags
Privilege Escalation, Persistence, Valid Accounts, Cloud Accounts, Account Manipulation
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is one of MICROSOFT.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/WRITE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNamein
  • MICROSOFT.CONTAINERSERVICE/MANAGEDCLUSTERS/RBAC.AUTHORIZATION.K8S.IO/CLUSTERROLEBINDINGS/WRITE transforms: toupper
  • MICROSOFT.CONTAINERSERVICE/MANAGEDCLUSTERS/RBAC.AUTHORIZATION.K8S.IO/ROLEBINDINGS/WRITE transforms: toupper
  • MICROSOFT.KUBERNETES/CONNECTEDCLUSTERS/RBAC.AUTHORIZATION.K8S.IO/CLUSTERROLEBINDINGS/WRITE transforms: toupper
  • MICROSOFT.KUBERNETES/CONNECTEDCLUSTERS/RBAC.AUTHORIZATION.K8S.IO/ROLEBINDINGS/WRITE transforms: toupper
field:"operationName" kind:in
resultTypein
  • Succeeded
  • Success
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

#
Severity
medium
Log types
Azure.MonitorActivity
Tags
Defense Evasion, Impact, Impair Defenses, Disable Cloud Logs, Data Destruction
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is MICROSOFT.OPERATIONALINSIGHTS/WORKSPACES/DELETE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.OPERATIONALINSIGHTS/WORKSPACES/DELETE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.OPERATIONALINSIGHTS/WORKSPACES/DELETE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
medium
Log types
Azure.Audit
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

This detection looks for a number of failed sign-ins for the same ServicePrincipalName or UserPrincipalName

MITRE ATT&CK coverage

TacticTechniques
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

  • operationName is Sign-in activity
  • properties.status.errorCode is greater than 0
Alert cadence
alerts after 10 matches within 10m

Indicators

These rows show field, operator, and value matches.

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

#
Severity
high
Log types
Azure.Audit
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

This detection looks for MFA being disabled in conditional access policy

MITRE ATT&CK coverage

Telemetry coverage

PlatformRecord / event type
AzureUpdate conditional access policy

Rules detecting the same action

These rules filter on the same operation.

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

  • operationName is Update 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.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
operationName
categoryproperties.category
actor_idproperties.initiatedBy.user.id
actor_upnproperties.initiatedBy.user.userPrincipalName
source_ip_addressproperties.initiatedBy.user.ipAddress
target_idproperties.targetResources.id
target_nameproperties.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

#
Status
Experimental
Severity
medium
Group by
properties.sessionId, properties.userPrincipalName
Log types
Azure.Audit
Tags
Initial Access, Valid Accounts, Use Alternate Authentication Material
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

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.resourceDisplayName contains Microsoft Graph
  • properties.appId is not one of 00000003-0000-0ff1-ce00-000000000000, 00000006-0000-0ff1-ce00-000000000000, 00b41c95-dab0-4487-9791-b9d2c32c80f2, d3590ed6-52b3-4102-aeff-aad2292ab01c, 1fec8e78-bce4-4aaf-ab1b-5451cc387264 (+29 more values, see Indicators below)
  • properties.isInteractive is present
  • category is not NonInteractiveUserSignInLogs
  • properties.sessionId is present
  • properties.userPrincipalName is present
  • properties.ipAddress is present
  • resultSignature is SUCCESS
Alert cadence
alerts after 2 matches within 1h

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
properties.resourceDisplayNamecontainsMicrosoft Graphexcludes:properties.resourceDisplayName field:"properties.resourceDisplayName" value:"Microsoft Graph"
properties.appIdin00000003-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-307022471b92excludes:properties.appId
categoryeqNonInteractiveUserSignInLogsexcludes:category field:"category" value:"NonInteractiveUserSignInLogs"
properties.isInteractiveis_null(no value, null check)excludes:properties.isInteractive
properties.ipAddressis_null(no value, null check)excludes:properties.ipAddress
properties.sessionIdis_null(no value, null check)excludes:properties.sessionId
properties.userPrincipalNameis_null(no value, null check)excludes:properties.userPrincipalName
resultSignatureneSUCCESSexcludes:resultSignature field:"resultSignature" value:"SUCCESS"

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.

FieldSource
userPrincipalNameproperties.userPrincipalName
sessionIdproperties.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

#
Severity
medium
Log types
Azure.MonitorActivity
Tags
Credential Access, Network Sniffing
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
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:
    • operationName contains MICROSOFT.NETWORK/NETWORKWATCHERS/STARTPACKETCAPTURE/ACTION
    • operationName contains MICROSOFT.NETWORK/NETWORKWATCHERS/VPNCONNECTIONS/STARTPACKETCAPTURE/ACTION
    • operationName contains MICROSOFT.NETWORK/NETWORKWATCHERS/PACKETCAPTURES/WRITE
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNamecontains
  • MICROSOFT.NETWORK/NETWORKWATCHERS/PACKETCAPTURES/WRITE
  • MICROSOFT.NETWORK/NETWORKWATCHERS/STARTPACKETCAPTURE/ACTION
  • MICROSOFT.NETWORK/NETWORKWATCHERS/VPNCONNECTIONS/STARTPACKETCAPTURE/ACTION
field:"operationName" kind:contains
resultTypein
  • Succeeded
  • Success
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

#
Status
Experimental
Severity
medium
Log types
Azure.MonitorActivity
Tags
AZT506, Defense Evasion, Impact, Impair Defenses, Disable or Modify Cloud Firewall, Data Destruction
Reference
attack.mitre.org
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is one of MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/WRITE, MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/DELETE, MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/SECURITYRULES/WRITE, MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/SECURITYRULES/DELETE, MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/JOIN/ACTION (+1 more values, see Indicators below) (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNamein
  • MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/DELETE transforms: toupper
  • MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/JOIN/ACTION transforms: toupper
  • MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/PROVIDERS/MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/WRITE transforms: toupper
  • MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/SECURITYRULES/DELETE transforms: toupper
  • MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/SECURITYRULES/WRITE transforms: toupper
  • MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/WRITE transforms: toupper
field:"operationName" kind:in
resultTypein
  • Succeeded
  • Success
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

#
Severity
medium
Log types
Azure.MonitorActivity
Tags
Defense Evasion, Impair Defenses, Disable or Modify Tools
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
Defense Impairment

Telemetry coverage

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

  • operationName is MICROSOFT.NETWORK/NETWORKWATCHERS/DELETE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.NETWORK/NETWORKWATCHERS/DELETE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.NETWORK/NETWORKWATCHERS/DELETE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
low
Log types
Azure.Audit
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

This detection looks for policy changes in AuditLogs

MITRE ATT&CK coverage

TacticTechniques
Discovery

Telemetry coverage

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.result is success
  • operationName ends with policy
  • operationName does not start with Add
  • operationName does not start with Added
Alert deduplication
repeat matches within 10m group into one alert

Exclusions

The rule actively suppresses these predicates.

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.

FieldSource
operationName
categoryproperties.category
actor_idproperties.initiatedBy.user.id
actor_upnproperties.initiatedBy.user.userPrincipalName
source_ip_addressproperties.initiatedBy.user.ipAddress
target_idproperties.targetResources.id
target_nameproperties.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

#
Severity
medium
Log types
Azure.MonitorActivity
Tags
AZT508, Persistence, Defense Evasion, Valid Accounts, Cloud Accounts, Hide Artifacts
Reference
microsoft.github.io
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is one of MICROSOFT.AUTHORIZATION/POLICIES/DEPLOYIFNOTEXISTS/ACTION (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNamein
  • MICROSOFT.AUTHORIZATION/POLICIES/DEPLOYIFNOTEXISTS/ACTION transforms: toupper
field:"operationName" kind:in value:"MICROSOFT.AUTHORIZATION/POLICIES/DEPLOYIFNOTEXISTS/ACTION"
resultTypein
  • Succeeded
  • Success
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

#
Status
Experimental
Severity
informational
Log types
Azure.MonitorActivity
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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.

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

  • operationName is one of MICROSOFT.AUTHORIZATION/POLICIES/AUDIT/ACTION, MICROSOFT.AUTHORIZATION/POLICIES/AUDITIFNOTEXISTS/ACTION (case-insensitive)
  • category is Policy
  • level is Warning

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
categoryeq
  • Policy
field:"category" kind:eq value:"Policy"
leveleq
  • Warning
field:"level" kind:eq value:"Warning"
operationNamein
  • MICROSOFT.AUTHORIZATION/POLICIES/AUDIT/ACTION transforms: toupper
  • MICROSOFT.AUTHORIZATION/POLICIES/AUDITIFNOTEXISTS/ACTION transforms: toupper
field:"operationName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
entityproperties.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

#
Severity
medium
Log types
Azure.MonitorActivity
Tags
AZT402, Persistence, Defense Evasion, Account Manipulation, Add Office 365 Global Administrator Role, Valid Accounts, Cloud Accounts
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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.value is present
      • operationName.value is MICROSOFT.AUTHORIZATION/ELEVATEACCESS/ACTION
      • status.value is Succeeded
    • all of:
      • operationName is MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE
      • resultType is one of Success, 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.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
nameidentity.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

#
Severity
high
Group by
properties.userPrincipalName
Log types
Azure.Audit
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
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

  • operationName is Sign-in activity
  • resultSignature is SUCCESS
  • properties.riskState is present
  • properties.riskState is one of atrisk, confirmedcompromised (case-insensitive)
  • any of:
    • properties.riskLevelDuringSignIn is one of high, medium
    • properties.riskLevelAggregated is one of high, medium
  • properties.userPrincipalName is present
Alert cadence
alerts after 3 matches within 15m

Exclusions

The rule actively suppresses these predicates.

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.

FieldSource
userPrincipalNameproperties.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

#
Severity
high
Log types
Azure.MonitorActivity
Tags
Impact, Inhibit System Recovery, Data Destruction, Defense Evasion, Impair Defenses, Ransomware, Storm-0501, Backup
Reference
www.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is MICROSOFT.RECOVERYSERVICES/VAULTS/BACKUPFABRICS/PROTECTIONCONTAINERS/DELETE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.RECOVERYSERVICES/VAULTS/BACKUPFABRICS/PROTECTIONCONTAINERS/DELETE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.RECOVERYSERVICES/VAULTS/BACKUPFABRICS/PROTECTIONCONTAINERS/DELETE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
medium
Log types
Azure.MonitorActivity
Tags
Impact, Data Destruction
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
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

  • operationName is MICROSOFT.RESOURCES/SUBSCRIPTIONS/RESOURCEGROUPS/DELETE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.RESOURCES/SUBSCRIPTIONS/RESOURCEGROUPS/DELETE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.RESOURCES/SUBSCRIPTIONS/RESOURCEGROUPS/DELETE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
high
Log types
Azure.MonitorActivity
Tags
Defense Evasion, Impair Defenses, Inhibit System Recovery, Ransomware, Storm-0501
Reference
www.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
Defense Impairment
Impact

Telemetry coverage

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

  • operationName is MICROSOFT.AUTHORIZATION/LOCKS/DELETE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.AUTHORIZATION/LOCKS/DELETE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.AUTHORIZATION/LOCKS/DELETE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
medium
Log types
Azure.MonitorActivity
Tags
Impact, Data Destruction, Inhibit System Recovery, Ransomware
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is MICROSOFT.COMPUTE/RESTOREPOINTCOLLECTIONS/DELETE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.COMPUTE/RESTOREPOINTCOLLECTIONS/DELETE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.COMPUTE/RESTOREPOINTCOLLECTIONS/DELETE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
medium
Log types
Azure.Audit
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
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

  • operationName is Sign-in activity
  • properties.riskState is not one of dismissed, remediated (case-insensitive)
  • any of:
    • properties is one of low, medium, high (case-insensitive)
    • properties is one of low, medium, high (case-insensitive)

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Alert deduplication
repeat matches within 40m group into one alert

Exclusions

The rule actively suppresses these predicates.

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • Sign-in activity
field:"operationName" kind:eq value:"Sign-in activity"
propertiesin
  • high transforms: tolower
  • low transforms: tolower
  • medium transforms: tolower
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

#
Severity
medium
Group by
p_row_id
Log types
Azure.Audit
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

This detection looks for a change in member's PIM roles in EntraID

MITRE ATT&CK coverage

TacticTechniques
Resource Development

Telemetry coverage

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.result is success
  • operationName contains Add member to role in PIM completed
Alert deduplication
repeat matches within 5m group into one alert

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.

FieldSource
operationName
categoryproperties.category
actor_idproperties.initiatedBy.user.id
actor_upnproperties.initiatedBy.user.userPrincipalName
source_ip_addressproperties.initiatedBy.user.ipAddress
target_idproperties.targetResources.id
target_nameproperties.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

#
Status
Experimental
Severity
medium
Log types
Azure.Audit
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
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

  • operationName is Sign-in activity
  • resultSignature is SUCCESS
  • properties.authenticationProtocol is ropc
  • properties.authenticationRequirement is singlefactorauthentication
  • any of:
    • properties.userType is empty
    • properties.userType is member

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
operationNameneSign-in activityexcludes:operationName field:"operationName" value:"Sign-in activity"
resultSignatureneSUCCESSexcludes:resultSignature field:"resultSignature" value:"SUCCESS"
properties.userTypeis_not_null(no value, null check)excludes:properties.userType
properties.userTypenememberexcludes:properties.userType field:"properties.userType" value:"member"

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.

FieldSource
userPrincipalNameproperties.userPrincipalName
ipAddressproperties.ipAddress
appDisplayNameproperties.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

#
Severity
informational
Log types
Azure.MonitorActivity
Tags
AZT404, AZT404.3, AZT302, AZT302.1, AZT302.2, AZT302.3, AZT302.4, Execution, Command and Scripting Interpreter, Cloud Administration Command
Reference
microsoft.github.io
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is one of MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/JOBS/WRITE, MICROSOFT.WEB/SITES/HOSTRUNTIME/HOST/ACTION
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNamein
  • MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/JOBS/WRITE
  • MICROSOFT.WEB/SITES/HOSTRUNTIME/HOST/ACTION
field:"operationName" kind:in
resultTypein
  • Succeeded
  • Success
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

#
Severity
medium
Log types
Azure.Audit
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

Telemetry coverage

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:
    • operationName contains add service principal credentials
    • properties.activityDisplayName contains add service principal credentials

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.

FieldSource
tenantId
operation_nameoperationName
activity_display_nameproperties.activityDisplayName
category
resultproperties.result
actor_userproperties.initiatedBy.user.userPrincipalName
initiator_user_idproperties.initiatedBy.user.id
initiator_display_nameproperties.initiatedBy.user.displayName
source_ipproperties.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

#
Severity
medium
Log types
Azure.Audit
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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.

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

  • operationName is Sign-in activity
  • properties.userAgent is one of BAV2ROPC, CBAInPROD
  • properties.status.errorCode is 0

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Alert deduplication
repeat matches within 10m group into one alert

Indicators

These rows show field, operator, and value matches.

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

#
Severity
informational
Log types
Azure.MonitorActivity
Tags
Impact, Data Destruction, Inhibit System Recovery
Reference
docs.datadoghq.com
Source
github.com/panther-labs/panther-analysis

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

Telemetry coverage

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

  • operationName is MICROSOFT.SQL/SERVERS/DELETE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.SQL/SERVERS/DELETE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.SQL/SERVERS/DELETE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
high
Log types
Azure.MonitorActivity
Tags
Impact, Data Destruction
Reference
www.azadvertizer.net
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
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

  • operationName is MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/WRITE (case-insensitive)
  • resultType is one of Success, 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.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/WRITE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/WRITE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
high
Log types
Azure.MonitorActivity
Tags
Impact, Data Destruction, Inhibit System Recovery, Ransomware
Reference
www.trendmicro.com
Source
github.com/panther-labs/panther-analysis

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

Telemetry coverage

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

  • operationName is MICROSOFT.STORAGE/STORAGEACCOUNTS/DELETE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.STORAGE/STORAGEACCOUNTS/DELETE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/DELETE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
high
Log types
Azure.MonitorActivity
Tags
Command and Control, Application Layer Protocol
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
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

  • operationName is MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE (case-insensitive)
  • resultType is one of Success, 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.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
informational
Log types
Azure.MonitorActivity
Tags
Persistence, Account Manipulation
Reference
www.elastic.co
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
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

  • operationName is MICROSOFT.STORAGE/STORAGEACCOUNTS/REGENERATEKEY/ACTION (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.STORAGE/STORAGEACCOUNTS/REGENERATEKEY/ACTION transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/REGENERATEKEY/ACTION"
resultTypein
  • Succeeded
  • Success
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

#
Status
Experimental
Severity
medium
Log types
Azure.MonitorActivity
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
Reference
microsoft.github.io
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
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

  • operationName is one of MICROSOFT.STORAGE/STORAGEACCOUNTS/LISTKEYS/ACTION (case-insensitive)
  • resultType is one of Success, Succeeded
Alert cadence
alerts after 50 matches within 1h

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNamein
  • MICROSOFT.STORAGE/STORAGEACCOUNTS/LISTKEYS/ACTION transforms: toupper
field:"operationName" kind:in value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/LISTKEYS/ACTION"
resultTypein
  • Succeeded
  • Success
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

#
Severity
high
Log types
Azure.MonitorActivity
Tags
Exfiltration, Exfiltration Over Web Service
Reference
www.mitiga.io
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
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

  • operationName is MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE (case-insensitive)
  • resultType is one of Success, 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.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
medium
Log types
Azure.MonitorActivity
Tags
Persistence, Account Manipulation
Reference
orca.security
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
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

  • operationName is MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE (case-insensitive)
  • resultType is one of Success, 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.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
high
Log types
Azure.MonitorActivity
Tags
Exfiltration, Data from Cloud Storage
Reference
hackingthe.cloud
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
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

  • resultType is one of Success, Succeeded
  • any of:
    • operationName is MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE
    • all of:
      • operationName is not MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE
      • operationName is one of MICROSOFT.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.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE
field:"operationName" kind:eq value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE"
operationNamein
  • MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/CONTAINERS/WRITE
  • MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/WRITE
field:"operationName" kind:in
operationNamene
  • MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE
field:"operationName" kind:ne value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
medium
Log types
Azure.MonitorActivity
Tags
Exfiltration, Collection, Storm-0501, Defender for Cloud, Data Theft
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is GETBLOB
  • properties.metricResponseType is Success
Alert cadence
alerts after 50 matches within 15m

Indicators

These rows show field, operator, and value matches.

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

#
Severity
medium
Log types
Azure.MonitorActivity
Tags
Exfiltration, Exfiltration to Cloud Storage, Defense Evasion, File and Directory Permissions Modification
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is one of MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/CONTAINERS/WRITE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNamein
  • MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/CONTAINERS/WRITE transforms: toupper
field:"operationName" kind:in value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/CONTAINERS/WRITE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
high
Log types
Azure.MonitorActivity
Tags
Impact, Data Encrypted for Impact, Inhibit System Recovery, Ransomware
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

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

  • category is STORAGEREAD (case-insensitive)
  • statusCode is 409
  • statusText is BLOBUSESCUSTOMERSPECIFIEDENCRYPTION (case-insensitive)
  • properties.metricResponseType is not Success

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.

FieldSource
objectKeyproperties.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

#
Severity
medium
Log types
Azure.MonitorActivity
Tags
Impact, Data Destruction, Inhibit System Recovery, Ransomware
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is DELETEBLOB (case-insensitive)
  • properties.metricResponseType is Success
Alert cadence
alerts after 15 matches within 15m

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
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

#
Severity
high
Log types
Azure.MonitorActivity
Tags
Impact, Data Destruction, Inhibit System Recovery, Ransomware
Reference
azure.github.io
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/WRITE (case-insensitive)
  • resultType is one of Success, 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.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/WRITE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/WRITE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
high
Time window
30h
Match by
p_alert_context.blob_path
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

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

#
Severity
informational
Log types
Azure.MonitorActivity
Source
github.com/panther-labs/panther-analysis

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

  • operationName is PUTBLOB (case-insensitive)
  • properties.metricResponseType is Success

Indicators

These rows show field, operator, and value matches.

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

#
Severity
high
Log types
Azure.MonitorActivity
Tags
Impact, Data Destruction, Inhibit System Recovery, Ransomware
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/WRITE (case-insensitive)
  • resultType is one of Success, 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.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/WRITE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/WRITE"
resultTypein
  • Succeeded
  • Success
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 File Share Created or Modified

#
Severity
informational
Log types
Azure.MonitorActivity
Tags
Exfiltration, Exfiltration Over Alternative Protocol, Collection, Data from Cloud Storage, AZT702, AZT702.1, File Share Mounting, Storage Account File Share NFS/SMB Mount
Reference
microsoft.github.io
Source
github.com/panther-labs/panther-analysis

Detects when an Azure Storage file share is created or modified. File shares can be mounted as network drives using SMB or NFS protocols, providing persistent access to storage. Adversaries may create or modify file shares to establish data exfiltration channels or mount shares to local systems for easier data transfer. While file share operations are common in legitimate scenarios, monitoring these activities helps establish baselines and identify unusual patterns that may indicate data exfiltration.

MITRE ATT&CK coverage

Telemetry coverage

Detection logic

from panther_azureactivity_helpers import (
    azure_activity_alert_context,
    azure_activity_success,
    extract_resource_name_from_id,
)

FILESHARE_OPERATIONS = [
    "MICROSOFT.STORAGE/STORAGEACCOUNTS/FILESERVICES/SHARES/WRITE",
]


def rule(event):
    return event.get(
        "operationName", ""
    ).upper() in FILESHARE_OPERATIONS and azure_activity_success(event)


def title(event):
    resource_id = event.get("resourceId", "<UNKNOWN_RESOURCE>")
    share_name = extract_resource_name_from_id(resource_id, "shares", default="<UNKNOWN_SHARE>")
    return f"Azure Storage File Share Created or Modified: [{share_name}]"


def alert_context(event):
    context = azure_activity_alert_context(event)

    resource_id = event.get("resourceId", "")

    share_name = extract_resource_name_from_id(resource_id, "shares", default="")
    if share_name:
        context["share_name"] = share_name
    return context

Rule specification

AnalysisType: rule
Filename: azure_storage_fileshare_modified.py
RuleID: "Azure.MonitorActivity.Storage.FileShareModified"
DisplayName: "Azure Storage File Share Created or Modified"
Enabled: true
LogTypes:
  - Azure.MonitorActivity
Severity: Info
Description: >
  Detects when an Azure Storage file share is created or modified. File shares can be mounted as
  network drives using SMB or NFS protocols, providing persistent access to storage. Adversaries may
  create or modify file shares to establish data exfiltration channels or mount shares to local systems
  for easier data transfer. While file share operations are common in legitimate scenarios, monitoring
  these activities helps establish baselines and identify unusual patterns that may indicate data exfiltration.
Reports:
  MITRE ATT&CK:
    - TA0010:T1048 # Exfiltration: Exfiltration Over Alternative Protocol
    - TA0009:T1530 # Collection: Data from Cloud Storage
Tags:
  - Exfiltration
  - Exfiltration Over Alternative Protocol
  - Collection
  - Data from Cloud Storage
  - AZT702
  - AZT702.1
  - File Share Mounting
  - Storage Account File Share NFS/SMB Mount
Runbook: |
  1. Find all storage account operations by the callerIpAddress in the 6 hours before and after this alert to identify if this is part of a data staging or exfiltration pattern
  2. Query for the storage account configuration to determine if the file share has public access or is restricted to specific networks
  3. Check if the callerIpAddress has created or modified file shares in the past 30 days to determine if this is normal administrative behavior
Reference: https://microsoft.github.io/Azure-Threat-Research-Matrix/Impact/AZT702/AZT702-1
SummaryAttributes:
  - resourceId
  - callerIpAddress
  - correlationId

Stages and Predicates

Fires on Azure.MonitorActivity events when all of the conditions below hold.

Condition

  • operationName is one of MICROSOFT.STORAGE/STORAGEACCOUNTS/FILESERVICES/SHARES/WRITE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNamein
  • MICROSOFT.STORAGE/STORAGEACCOUNTS/FILESERVICES/SHARES/WRITE transforms: toupper
field:"operationName" kind:in value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/FILESERVICES/SHARES/WRITE"
resultTypein
  • Succeeded
  • Success
field:"resultType" kind:in

Response runbook

1. Find all storage account operations by the callerIpAddress in the 6 hours before and after this alert to identify if this is part of a data staging or exfiltration pattern

2. Query for the storage account configuration to determine if the file share has public access or is restricted to specific networks

3. Check if the callerIpAddress has created or modified file shares in the past 30 days to determine if this is normal 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/fileServices/shares/write",
  "operationVersion": "2021-09-01",
  "resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/storage-rg/providers/Microsoft.Storage/storageAccounts/mystorageaccount/fileServices/default/shares/data-share",
  "resultSignature": "201",
  "resultType": "Success",
  "tenantId": "87654321-4321-4321-4321-111111111111",
  "time": "2025-12-23T10:30:00.0000000Z"
}

Azure Storage Immutability Policy Deleted

#
Severity
high
Log types
Azure.MonitorActivity
Tags
Defense Evasion, Impair Defenses, Inhibit System Recovery, Data Destruction, Ransomware, Storm-0501, WORM
Reference
www.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/CONTAINERS/IMMUTABILITYPOLICIES/DELETE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/CONTAINERS/IMMUTABILITYPOLICIES/DELETE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.STORAGE/STORAGEACCOUNTS/BLOBSERVICES/CONTAINERS/IMMUTABILITYPOLICIES/DELETE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
low
Group by
properties.accountName
Log types
Azure.MonitorActivity
Tags
Exfiltration, Credential Access, Storm-0501, Defender for Cloud, SAS Token
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

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

  • category is one of StorageRead, StorageWrite, StorageDelete
  • statusCode is one of 200, 201, 202, 204
  • uri is present
  • uri contains sig=

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.

FieldKindExcluded valuesSearch
uricontainssig=excludes:uri field:"uri" value:"sig="
uriis_null(no value, null check)excludes:uri

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
categoryin
  • StorageDelete
  • StorageRead
  • StorageWrite
field:"category" kind:in
statusCodein
  • 200 transforms: number
  • 201 transforms: number
  • 202 transforms: number
  • 204 transforms: number
field:"statusCode" kind:in
uricontains
  • sig=
field:"uri" kind:contains value:"sig="

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
storage_accountproperties.accountName
operationoperationName
object_keyproperties.objectKey
user_agentproperties.userAgentHeader
uri
status_codestatusCode
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

#
Severity
high
Log types
Azure.Audit
Tags
Persistence, Account Manipulation
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

Telemetry coverage

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

  • operationName contains User has elevated their access to User Access Administrator
  • result is success

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.

FieldSource
userPrincipalNameproperties.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

#
Status
Experimental
Severity
informational
Log types
Azure.MonitorActivity
Tags
Impact, Data Destruction, Service Stop
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

Telemetry coverage

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

  • operationName is MICROSOFT.COMPUTE/VIRTUALMACHINES/DELETE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.COMPUTE/VIRTUALMACHINES/DELETE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.COMPUTE/VIRTUALMACHINES/DELETE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
high
Log types
Azure.MonitorActivity
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

Telemetry coverage

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

  • operationName is MICROSOFT.NETWORK/VIRTUALNETWORKS/DELETE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.NETWORK/VIRTUALNETWORKS/DELETE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.NETWORK/VIRTUALNETWORKS/DELETE"
resultTypein
  • Succeeded
  • Success
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

#
Severity
informational
Log types
Azure.MonitorActivity
Tags
AZT301, AZT301.1, AZT301.2, AZT301.3, AZT301.4, AZT301.5, AZT301.6, AZT301.7, Execution, Cloud Administration Command
Reference
microsoft.github.io
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
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

  • operationName is one of MICROSOFT.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/ACTION (+1 more values, see Indicators below)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNamein
  • MICROSOFT.COMPUTE/GALLERIES/APPLICATIONS/VERSIONS/WRITE
  • MICROSOFT.COMPUTE/VIRTUALMACHINES/EXTENSIONS/WRITE
  • MICROSOFT.COMPUTE/VIRTUALMACHINES/RUNCOMMAND/ACTION
  • MICROSOFT.COMPUTE/VIRTUALMACHINESCALESETS/VIRTUALMACHINES/RUNCOMMAND/ACTION
  • MICROSOFT.CONTAINERSERVICE/MANAGEDCLUSTERS/RUNCOMMAND/ACTION
  • MICROSOFT.SERIALCONSOLE/SERIALPORTS/CONNECT/ACTION
field:"operationName" kind:in
resultTypein
  • Succeeded
  • Success
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

#
Severity
high
Log types
Azure.MonitorActivity
Tags
AZT701.1, Exfiltration, Collection, Exfiltration Over Web Service, Exfiltration to Cloud Storage, Data from Cloud Storage
Reference
microsoft.github.io
Source
github.com/panther-labs/panther-analysis

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

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

  • operationName is one of MICROSOFT.COMPUTE/DISKS/BEGINGETACCESS/ACTION (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNamein
  • MICROSOFT.COMPUTE/DISKS/BEGINGETACCESS/ACTION transforms: toupper
field:"operationName" kind:in value:"MICROSOFT.COMPUTE/DISKS/BEGINGETACCESS/ACTION"
resultTypein
  • Succeeded
  • Success
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

#
Severity
low
Log types
Azure.MonitorActivity
Tags
Impact, Data Destruction, Inhibit System Recovery, Ransomware
Reference
github.com
Source
github.com/panther-labs/panther-analysis

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

Telemetry coverage

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

  • operationName is MICROSOFT.COMPUTE/SNAPSHOTS/DELETE (case-insensitive)
  • resultType is one of Success, Succeeded

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationNameeq
  • MICROSOFT.COMPUTE/SNAPSHOTS/DELETE transforms: toupper
field:"operationName" kind:eq value:"MICROSOFT.COMPUTE/SNAPSHOTS/DELETE"
resultTypein
  • Succeeded
  • Success
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

#
Status
Experimental
Severity
medium
Log types
Azure.Audit
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

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

TacticTechniques
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

  • operationName is Sign-in activity
  • resultSignature is SUCCESS
  • any of:
    • properties.appId is aebc6443-996d-45c2-90f0-388ff96faa56
    • properties.userAgent contains visual studio code
  • any of:
    • properties.resourceId is 00000003-0000-0000-c000-000000000000
    • properties.resourceDisplayName contains microsoft graph

Exclusions

The rule actively suppresses these predicates.

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.

FieldSource
ipAddressproperties.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"
}