LoFP LoFP / infrastructure-as-code, ci/cd, and platform automation (for example crossplane, terraform, arm/bicep, aks control plane) commonly delete resource groups during environment teardown. the first occurrence per principal will alert; baseline expected service principals and exclude them if the activity is authorized.

Sample rules

Azure Resource Group Deleted by Unusual Principal

Description

Identifies Azure resource group deletions by a principal that has not performed this operation in the prior 7 days. Deletion of a resource group is permanent and removes all resources in the group. This rule uses an ES|QL first-seen (new terms) approach keyed on the acting principal so routine infrastructure-as-code teardown is suppressed after the first occurrence, while still surfacing first-time or unusual identities performing the delete.

Detection logic

FROM logs-azure.activitylogs-*, filebeat-*
| WHERE data_stream.dataset == "azure.activitylogs"
    AND event.action == "MICROSOFT.RESOURCES/SUBSCRIPTIONS/RESOURCEGROUPS/DELETE"
    AND event.outcome IN ("success", "Success")
    AND azure.activitylogs.identity.authorization.evidence.principal_id IS NOT NULL
| STATS Esql.first_time_seen = MIN(@timestamp),
        Esql.last_time_seen = MAX(@timestamp),
        Esql.event_count = COUNT(*),
        Esql.resource_group_values = VALUES(azure.resource.group),
        Esql.resource_name_values = VALUES(azure.resource.name),
        Esql.resource_id_values = VALUES(azure.resource.id),
        Esql.principal_type_values = VALUES(azure.activitylogs.identity.authorization.evidence.principal_type),
        Esql.initiated_by_user_values = VALUES(azure.activitylogs.identity.claims_initiated_by_user.name),
        Esql.appid_values = VALUES(azure.activitylogs.identity.claims.appid),
        Esql.session_id_values = VALUES(azure.activitylogs.identity.claims.sid),
        Esql.unique_token_id_values = VALUES(azure.activitylogs.identity.claims.uti),
        Esql.correlation_id_values = VALUES(azure.correlation_id),
        Esql.role_values = VALUES(azure.activitylogs.identity.authorization.evidence.role),
        Esql.role_assignment_id_values = VALUES(azure.activitylogs.identity.authorization.evidence.role_assignment_id),
        Esql.source_ip_values = VALUES(source.ip),
        Esql.source_country_values = VALUES(source.geo.country_name),
        Esql.source_as_number_values = VALUES(source.`as`.number),
        Esql.subscription_id_values = VALUES(azure.subscription_id)
        BY azure.activitylogs.identity.authorization.evidence.principal_id
// new terms emulation: fire when this principal's first_seen falls in the current interval
// (schedule + ingest-lag buffer), i.e. it was absent from the rest of the 7-day window
| EVAL Esql.recent_minutes = DATE_DIFF("minute", Esql.first_time_seen, NOW())
| WHERE Esql.recent_minutes <= 10
| KEEP azure.*, Esql.*

Azure Diagnostic Settings Deleted

Description

Identifies deletion of Azure diagnostic settings for a resource group that has not had this operation in the prior 7 days. Diagnostic settings send platform logs and metrics to Log Analytics, Event Hubs, or storage; removing them blinds monitoring and detection. This rule uses an ES|QL first-seen (new terms) approach keyed on the resource group so groups that constantly churn logging configuration are suppressed after the first occurrence, while a newly seen group still alerts.

Detection logic

FROM logs-azure.activitylogs-*
| WHERE data_stream.dataset == "azure.activitylogs"
    AND event.action == "MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE"
    AND event.outcome IN ("success", "Success")
    AND azure.resource.group IS NOT NULL
| STATS Esql.first_time_seen = MIN(@timestamp),
        Esql.last_time_seen = MAX(@timestamp),
        Esql.event_count = COUNT(*),
        Esql.event_action_values = VALUES(event.action),
        Esql.resource_name_values = VALUES(azure.resource.name),
        Esql.resource_id_values = VALUES(azure.resource.id),
        Esql.resource_provider_values = VALUES(azure.resource.provider),
        Esql.principal_id_values = VALUES(azure.activitylogs.identity.authorization.evidence.principal_id),
        Esql.principal_type_values = VALUES(azure.activitylogs.identity.authorization.evidence.principal_type),
        Esql.initiated_by_user_values = VALUES(azure.activitylogs.identity.claims_initiated_by_user.name),
        Esql.appid_values = VALUES(azure.activitylogs.identity.claims.appid),
        Esql.session_id_values = VALUES(azure.activitylogs.identity.claims.sid),
        Esql.unique_token_id_values = VALUES(azure.activitylogs.identity.claims.uti),
        Esql.correlation_id_values = VALUES(azure.correlation_id),
        Esql.role_values = VALUES(azure.activitylogs.identity.authorization.evidence.role),
        Esql.role_assignment_id_values = VALUES(azure.activitylogs.identity.authorization.evidence.role_assignment_id),
        Esql.role_assignment_scope_values = VALUES(azure.activitylogs.identity.authorization.evidence.role_assignment_scope),
        Esql.source_ip_values = VALUES(source.ip),
        Esql.source_country_values = VALUES(source.geo.country_name),
        Esql.source_as_number_values = VALUES(source.`as`.number),
        Esql.subscription_id_values = VALUES(azure.subscription_id)
        BY azure.resource.group
// new terms emulation: fire when this resource group's first_seen falls in the current interval
// (schedule + ingest-lag buffer), i.e. it was absent from the rest of the 7-day window
| EVAL Esql.recent_minutes = DATE_DIFF("minute", Esql.first_time_seen, NOW())
| WHERE Esql.recent_minutes <= 10
| KEEP azure.*, Esql.*