Detection rules › Kusto
Unusual Volume of file deletion by users
This query looks for users performing file deletion activities. Spikes in file deletion observed from risky sign-in sessions are flagged here. This applies to SharePoint and OneDrive users. Audit event and Cloud application identifier references. Reference - https://learn.microsoft.com/microsoft-365/compliance/audit-log-activities?view=o365-worldwide Reference - https://learn.microsoft.com/azure/sentinel/entities-reference#cloud-application-identifiers
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact | T1485 Data Destruction |
Rule body kusto
id: e5f8e196-3544-4a8b-96a9-17c1b6a49710
name: Unusual Volume of file deletion by users
description: |
This query looks for users performing file deletion activities. Spikes in file deletion observed from risky sign-in sessions are flagged here.
This applies to SharePoint and OneDrive users.
Audit event and Cloud application identifier references.
Reference - https://learn.microsoft.com/microsoft-365/compliance/audit-log-activities?view=o365-worldwide
Reference - https://learn.microsoft.com/azure/sentinel/entities-reference#cloud-application-identifiers
severity: High
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- CloudAppEvents
- AADSignInEventsBeta
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1485
query: |
let relevantOperations = pack_array("FileDeleted", "FileRecycled", "FileDeletedFirstStageRecycleBin", "FileDeletedSecondStageRecycleBin", "FileVersionsAllMinorsRecycled", "FileVersionRecycled", "FileVersionsAllRecycled");
let relevantAppIds = pack_array(int(20892), int(15600)); // App Ids for SharePoint and OneDrive
let timeWindow = 7d;
let timeNow = now();
//
let riskyUsers= // Look for users with risky sign-ins
SigninLogs
| where CreatedDateTime between ((timeNow - timeWindow) .. (timeNow))
| where isnotempty(UserId) and isnotempty(OriginalRequestId)
| where ResultType == '0'
| where RiskLevelDuringSignIn == 'high'
| project RiskLevelDuringSignIn, UserId, CreatedDateTime, SessionId=OriginalRequestId
;
let hasUsers = isnotempty(toscalar(riskyUsers));
//
let deleteEvents = // look for file deletion activity and scope it to risky users
CloudAppEvents
| where hasUsers
| where TimeGenerated between ((timeNow - timeWindow) .. (timeNow))
| where ApplicationId in (relevantAppIds)
| where isnotempty(AccountObjectId)
| where AccountObjectId in (riskyUsers)
| where ActionType in (relevantOperations)
| extend SessionId= tostring(RawEventData.AppAccessContext.AADSessionId)
| where isnotempty(SessionId)
| project UserId=AccountObjectId, AccountDisplayName, ApplicationId, SessionId, ActionType, TimeGenerated, ReportId
;
//
deleteEvents
| join kind=leftsemi riskyUsers on UserId, SessionId
| summarize Count=count() , (Timestamp, ReportId)=arg_min(TimeGenerated, ReportId) by UserId, AccountDisplayName, ApplicationId, ActionType, Time=bin(TimeGenerated, 5m)
// look for only those scoped users who have generated an increase in file deletion activity.
| summarize TotalCount= countif(Count > 50), (Timestamp, ReportId)=arg_min(Timestamp, ReportId) by UserId, AccountDisplayName, ApplicationId
| where TotalCount >= 3
| project UserId, AccountDisplayName, ApplicationId, TotalCount, ReportId, Timestamp
| extend NTDomain = tostring(split(AccountDisplayName,'\\',0)[0]), Name = tostring(split(AccountDisplayName,'\\',1)[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: UserId
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Name
- identifier: NTDomain
columnName: NTDomain
- entityType: CloudApplication
fieldMappings:
- identifier: AppId
columnName: ApplicationId
customDetails:
Count: TotalCount
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let relevantAppIds = pack_array(int(20892), int(15600));
let timeWindow = 7d;
let timeNow = now();
let hasUsers = isnotempty(toscalar(riskyUsers));
Let binding: relevantOperations
let relevantOperations = pack_array("FileDeleted", "FileRecycled", "FileDeletedFirstStageRecycleBin", "FileDeletedSecondStageRecycleBin", "FileVersionsAllMinorsRecycled", "FileVersionRecycled", "FileVersionsAllRecycled");
Let binding: riskyUsers
let riskyUsers = SigninLogs
| where CreatedDateTime between ((timeNow - timeWindow) .. (timeNow))
| where isnotempty(UserId) and isnotempty(OriginalRequestId)
| where ResultType == '0'
| where RiskLevelDuringSignIn == 'high'
| project RiskLevelDuringSignIn, UserId, CreatedDateTime, SessionId=OriginalRequestId;
Derived from timeWindow, timeNow.
The stages below define let deleteEvents (the rule's main pipeline source).
Stage 1: source
CloudAppEvents
Stage 2: where
| where hasUsers
Stage 3: where
| where TimeGenerated between ((timeNow - timeWindow) .. (timeNow))
Stage 4: where
| where ApplicationId in (relevantAppIds)
Stage 5: where
| where isnotempty(AccountObjectId)
Stage 6: where
| where AccountObjectId in (riskyUsers)
References riskyUsers (defined above).
Stage 7: where
| where ActionType in (relevantOperations)
References relevantOperations (defined above).
Stage 8: extend
| extend SessionId= tostring(RawEventData.AppAccessContext.AADSessionId)
Stage 9: where
| where isnotempty(SessionId)
Stage 10: project
| project UserId=AccountObjectId, AccountDisplayName, ApplicationId, SessionId, ActionType, TimeGenerated, ReportId
The stages below run on deleteEvents (the outer pipeline).
Stage 11: join
deleteEvents
| join kind=leftsemi riskyUsers on UserId, SessionId
Stage 12: summarize
| summarize Count=count() , (Timestamp, ReportId)=arg_min(TimeGenerated, ReportId) by UserId, AccountDisplayName, ApplicationId, ActionType, Time=bin(TimeGenerated, 5m)
Stage 13: summarize
| summarize TotalCount= countif(Count > 50), (Timestamp, ReportId)=arg_min(Timestamp, ReportId) by UserId, AccountDisplayName, ApplicationId
Stage 14: where
| where TotalCount >= 3
Stage 15: project
| project UserId, AccountDisplayName, ApplicationId, TotalCount, ReportId, Timestamp
Stage 16: extend
| extend NTDomain = tostring(split(AccountDisplayName,'\\',0)[0]), Name = tostring(split(AccountDisplayName,'\\',1)[0])
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 |
|---|---|---|
AccountObjectId | in |
|
AccountObjectId | is_not_null | |
ActionType | in |
|
ApplicationId | in |
|
OriginalRequestId | is_not_null | |
ResultType | eq |
|
RiskLevelDuringSignIn | eq |
|
SessionId | is_not_null | |
TotalCount | ge |
|
UserId | is_not_null |
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 |
|---|---|
AccountDisplayName | project |
ApplicationId | project |
ReportId | project |
Timestamp | project |
TotalCount | project |
UserId | project |
NTDomain | extend |
Name | extend |