Detection rules › Kusto
Power Apps - Multiple apps deleted
Identifies mass delete activity where multiple Power Apps are deleted, matching a predefined threshold of total apps deleted or app delete events across multiple Power Platform environments.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact | T1485 Data Destruction |
| Impact | T0826 Loss of Availability |
Rule body kusto
id: ed88638d-8627-4c20-ba08-67c13807a9b1
kind: Scheduled
name: Power Apps - Multiple apps deleted
description: Identifies mass delete activity where multiple Power Apps are deleted,
matching a predefined threshold of total apps deleted or app delete events across
multiple Power Platform environments.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: PowerPlatformAdmin
dataTypes:
- PowerPlatformAdminActivity
queryFrequency: 1h
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1485
- T0826
query: |
let total_app_mass_delete_threshold = 25;
let cross_environment_delete_threshold = 10;
let query_frequency = 1h;
let app_delete_events = materialize(
PowerPlatformAdminActivity
| where TimeGenerated >= ago (query_frequency)
| where EventOriginalType == "DeletePowerApp"
| extend Properties = tostring(PropertyCollection)
| extend AppId = extract(@'"powerplatform.analytics.resource.power_app.id","Value":"([^"]+)"', 1, Properties)
| extend AppId = tolower(replace_string(AppId, '/providers/Microsoft.PowerApps/apps/', ''))
| extend EnvironmentId = extract(@'"powerplatform.analytics.resource.environment.id","Value":"([^"]+)"', 1, Properties)
);
app_delete_events
| summarize AppCount = count(), EnvCount = dcount(EnvironmentId) by ActorName
| where AppCount >= total_app_mass_delete_threshold or EnvCount >= cross_environment_delete_threshold
| join kind=inner app_delete_events on ActorName
| summarize
Apps = make_set(AppId, 1000),
Environments = make_set(EnvironmentId, 1000),
StartTime = min(TimeGenerated)
by AppCount, EnvCount, ActorName
| extend
PowerAppsEntityId = 27593,
DataverseId = 32780,
AccountName = tostring(split(ActorName, '@')[0]),
UPNSuffix = tostring(split(ActorName, '@')[1])
| project
StartTime,
ActorName,
AppCount,
Apps,
EnvCount,
Environments,
PowerAppsEntityId,
DataverseId,
AccountName,
UPNSuffix
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: CloudApplication
fieldMappings:
- identifier: AppId
columnName: PowerAppsEntityId
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: UPNSuffix
alertDetailsOverride:
alertDisplayNameFormat: Power Apps - mass deletion of apps
alertDescriptionFormat: '{{AppCount}} apps were deleted in {{EnvCount}} environments
by {{ActorName}} , exceeding the mass delete threshold.'
customDetails:
EnvironmentsImpacted: Environments
AppsDeleted: Apps
AppDeleteCount: AppCount
EnvironmentsCount: EnvCount
version: 3.2.0
Stages and Predicates
Parameters
let total_app_mass_delete_threshold = 25;
let cross_environment_delete_threshold = 10;
let query_frequency = 1h;
The stages below define let app_delete_events (the rule's main pipeline source).
Stage 1: source
PowerPlatformAdminActivity
Stage 2: where
| where TimeGenerated >= ago (query_frequency)
Stage 3: where
| where EventOriginalType == "DeletePowerApp"
Stage 4: extend (4 consecutive steps)
| extend Properties = tostring(PropertyCollection)
| extend AppId = extract(@'"powerplatform.analytics.resource.power_app.id","Value":"([^"]+)"', 1, Properties)
| extend AppId = tolower(replace_string(AppId, '/providers/Microsoft.PowerApps/apps/', ''))
| extend EnvironmentId = extract(@'"powerplatform.analytics.resource.environment.id","Value":"([^"]+)"', 1, Properties)
The stages below run on app_delete_events (the outer pipeline).
Stage 5: summarize
app_delete_events
| summarize AppCount = count(), EnvCount = dcount(EnvironmentId) by ActorName
Stage 6: where
| where AppCount >= total_app_mass_delete_threshold or EnvCount >= cross_environment_delete_threshold
Stage 7: join
| join kind=inner app_delete_events on ActorName
Stage 8: summarize
| summarize
Apps = make_set(AppId, 1000),
Environments = make_set(EnvironmentId, 1000),
StartTime = min(TimeGenerated)
by AppCount, EnvCount, ActorName
Stage 9: extend
| extend
PowerAppsEntityId = 27593,
DataverseId = 32780,
AccountName = tostring(split(ActorName, '@')[0]),
UPNSuffix = tostring(split(ActorName, '@')[1])
Stage 10: project
| project
StartTime,
ActorName,
AppCount,
Apps,
EnvCount,
Environments,
PowerAppsEntityId,
DataverseId,
AccountName,
UPNSuffix
Indicators
Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.
| Field | Kind | Values |
|---|---|---|
AppCount | ge |
|
EnvCount | ge |
|
EventOriginalType | eq |
|
Output fields
Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.
| Field | Source |
|---|---|
AccountName | project |
ActorName | project |
AppCount | project |
Apps | project |
DataverseId | project |
EnvCount | project |
Environments | project |
PowerAppsEntityId | project |
StartTime | project |
UPNSuffix | project |