Detection rules › Kusto
Suspicious access of BEC related documents
This query looks for users with suspicious spikes in the number of files accessed that relate to topics commonly accessed as part of Business Email Compromise (BEC) attacks. The query looks for access to files in storage that relate to topics such as invoices or payments, and then looks for users accessing these files in significantly higher numbers than in the previous 14 days. Incidents raised by this analytic should be investigated to see if the user accessing these files should be accessing them, and if the volume they accessed them at was related to a legitimate business need. This query contains thresholds to reduce the chance of false positives, these can be adjusted to suit individual environments. In addition false positives could be generated by legitimate, scheduled actions that occur less often than every 14 days, additional exclusions can be added for these actions on username or IP address entities. This query uses the imFileEvent schema from ASIM, you will first need to ensure you have ASIM deployed in your environment. Ref https://learn.microsoft.com/azure/sentinel/normalization-about-parsers
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Collection | T1530 Data from Cloud Storage |
Event coverage
| Provider | Event | Title |
|---|---|---|
| Sysmon | Event ID 11 | FileCreate |
| Sysmon | Event ID 23 | FileDelete (File Delete archived) |
| Sysmon | Event ID 26 | FileDeleteDetected (File Delete logged) |
| Security-Auditing | Event ID 4663 | An attempt was made to access an object. |
Rule body kusto
id: cd8d946d-10a4-40a9-bac1-6d0a6c847d65
name: Suspicious access of BEC related documents
description: |
'This query looks for users with suspicious spikes in the number of files accessed that relate to topics commonly accessed as part of Business Email Compromise (BEC) attacks.
The query looks for access to files in storage that relate to topics such as invoices or payments, and then looks for users accessing these files in significantly higher numbers than in the previous 14 days. Incidents raised by this analytic should be investigated to see if the user accessing these files should be accessing them, and if the volume they accessed them at was related to a legitimate business need.
This query contains thresholds to reduce the chance of false positives, these can be adjusted to suit individual environments. In addition false positives could be generated by legitimate, scheduled actions that occur less often than every 14 days, additional exclusions can be added for these actions on username or IP address entities. This query uses the imFileEvent schema from ASIM, you will first need to ensure you have ASIM deployed in your environment. Ref https://learn.microsoft.com/azure/sentinel/normalization-about-parsers'
severity: Medium
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Collection
relevantTechniques:
- T1530
eventGroupingSettings:
aggregationKind: SingleAlert
query: |
let BEC_Keywords = dynamic([ 'invoice','payment','paycheck','transfer','bank statement','bank details','closing','funds','bank account','account details','remittance','purchase','deposit',"PO#","Zahlung","Rechnung","Paiement", "virement bancaire","Bankuberweisung",'hacked','phishing']);
// Adjust this threshold based on your environment
let sensitivity = 2.5;
let Events = materialize(imFileEvent
| where TimeGenerated between(startofday(ago(14d))..endofday(ago(0d)))
| where User !~ "app@sharepoint"
| where EventType =~ "FileAccessed"
| extend OriginalEvent = column_ifexists("EventOriginalType","Unknown")
| where OriginalEvent !~ "FileSyncDownloadedFull"
| where EventProduct in ("SharePoint 365", "Azure File Storage", "OneDrive" , "SharePoint")
| where FilePath has_any(BEC_Keywords)
| extend _AuthDetails = column_ifexists("AuthorizationDetails", "None")
| extend SPuser = case(gettype(_AuthDetails) == "array", tostring(todynamic(_AuthDetails)[0].principals[0].id), "Unknown")
| extend User = case(isnotempty(User), User, SPuser)
| where isnotempty(User));
Events
| summarize dcount(FileName) by User, bin(startofday(TimeGenerated), 1d)
| summarize CountOfDocs = make_list(dcount_FileName, 10000), TimeStamp = make_list(TimeGenerated, 10000) by User
| extend (Anomalies, Score, Baseline) = series_decompose_anomalies(CountOfDocs, sensitivity, -1, 'linefit')
| mv-expand CountOfDocs to typeof(double), TimeStamp to typeof(datetime), Anomalies to typeof(double), Score to typeof(double), Baseline to typeof(long)
| where Anomalies > 0
| project TimeStamp, CountOfDocs, Baseline, Score, Anomalies, User
| join kind=inner(Events | extend TimeStamp = startofday(TimeGenerated)) on TimeStamp, User
| extend IpAddr = column_ifexists("IpAddr", SrcIpAddr)
| extend Name = iif(User contains "@", split(User, "@")[0], split(User, "\\")[1])
| extend UPNSuffix = iif(User contains "@", split(User, "@")[1], "")
| extend NTDomain = iif(User contains "@", split(User, "\\")[0], "")
| project-reorder TimeGenerated, User, EventType, EventResult, EventProduct, FilePath, HttpUserAgent, IpAddr, CountOfDocs, Baseline, Score
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: User
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: User
- identifier: Name
columnName: Name
- identifier: NTDomain
columnName: NTDomain
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: User
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpAddr
- entityType: File
fieldMappings:
- identifier: Name
columnName: FilePath
customDetails:
Type: EventType
Result: EventResult
Product: EventProduct
UserAgent: HttpUserAgent
alertDetailsOverride:
alertDisplayNameFormat: Suspicious access of {{CountOfDocs}} BEC related documents by {{User}}
alertDescriptionFormat: |
This query looks for users (in this case {{User}}) with suspicious spikes in the number of files accessed (in this case {{CountOfDocs}} events) that relate to topics commonly accessed as part of Business Email Compromise (BEC) attacks. The query looks for access to files in storage that relate to topics such as invoices or payments, and then looks for users accessing these files in significantly higher numbers than in the previous 14 days. Incidents raised by this analytic should be investigated to see if the user accessing these files should be accessing them, and if the volume they accessed them at was related to a legitimate business need.
This query contains thresholds to reduce the chance of false positives, these can be adjusted to suit individual environments. In addition false positives could be generated by legitimate, scheduled actions that occur less often than every 14 days, additional exclusions can be added for these actions on username or IP address entities. This query uses the imFileEvent schema from ASIM, you will first need to ensure you have ASIM deployed in your environment. Ref https://learn.microsoft.com/azure/sentinel/normalization-about-parsers
version: 1.0.5
kind: Scheduled
Stages and Predicates
Stage 0: let
let BEC_Keywords = dynamic([ 'invoice','payment','paycheck','transfer','bank statement','bank details','closing','funds','bank account','account details','remittance','purchase','deposit',"PO#","Zahlung","Rechnung","Paiement", "virement bancaire","Bankuberweisung",'hacked','phishing']);
let sensitivity = 2.5;
let Events = materialize(<inlined as stages below>);
Stage 1: source time_window=1209600s
imFileEvent
Stage 2: where
| where TimeGenerated between(startofday(ago(14d))..endofday(ago(0d)))
Stage 3: where
| where User !~ "app@sharepoint"
Stage 4: where
| where EventType =~ "FileAccessed"
Stage 5: extend
| extend OriginalEvent = column_ifexists("EventOriginalType","Unknown")
Stage 6: where
| where OriginalEvent !~ "FileSyncDownloadedFull"
Stage 7: where
| where EventProduct in ("SharePoint 365", "Azure File Storage", "OneDrive" , "SharePoint")
Stage 8: where
| where FilePath has_any(BEC_Keywords)
Stage 9: extend
| extend _AuthDetails = column_ifexists("AuthorizationDetails", "None")
Stage 10: extend
| extend SPuser = case(gettype(_AuthDetails) == "array", tostring(todynamic(_AuthDetails)[0].principals[0].id), "Unknown")
Stage 11: extend
| extend User = case(isnotempty(User), User, SPuser)
Stage 12: where
| where isnotempty(User)
Stage 13: summarize
| summarize dcount(FileName) by User, bin(startofday(TimeGenerated), 1d)
Stage 14: summarize
| summarize CountOfDocs = make_list(dcount_FileName, 10000), TimeStamp = make_list(TimeGenerated, 10000) by User
Stage 15: extend
| extend (Anomalies, Score, Baseline) = series_decompose_anomalies(CountOfDocs, sensitivity, -1, 'linefit')
Stage 16: mv-expand
| mv-expand CountOfDocs to typeof(double), TimeStamp to typeof(datetime), Anomalies to typeof(double), Score to typeof(double), Baseline to typeof(long)
Stage 17: where
| where Anomalies > 0
Stage 18: project
| project TimeStamp, CountOfDocs, Baseline, Score, Anomalies, User
Stage 19: join
| join kind=inner(Events | extend TimeStamp = startofday(TimeGenerated)) on TimeStamp, User
Stage 20: extend
| extend IpAddr = column_ifexists("IpAddr", SrcIpAddr)
Stage 21: extend
| extend Name = iif(User contains "@", split(User, "@")[0], split(User, "\\")[1])
Stage 22: extend
| extend UPNSuffix = iif(User contains "@", split(User, "@")[1], "")
Stage 23: extend
| extend NTDomain = iif(User contains "@", split(User, "\\")[0], "")
Stage 24: project-reorder
| project-reorder TimeGenerated, User, EventType, EventResult, EventProduct, FilePath, HttpUserAgent, IpAddr, CountOfDocs, Baseline, Score
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 |
|---|---|---|
Anomalies | gt |
|
EventProduct | in |
|
EventType | eq |
|
FilePath | match |
|
OriginalEvent | ne |
|
User | is_not_null | |
User | ne |
|
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 |
|---|---|
Anomalies | project |
Baseline | project |
CountOfDocs | project |
Score | project |
TimeStamp | project |
User | project |
IpAddr | extend |
Name | extend |
UPNSuffix | extend |
NTDomain | extend |