Detection rules › Kusto
Suspicious access of BEC related documents in AWS S3 buckets
'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 AWS S3 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.'
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Collection | T1530 Data from Cloud Storage |
Rules detecting the same action
Other rules on this platform that filter on the same API call or operation.
- AWS Exfiltration via Anomalous GetObject API Activity (Splunk)
- AWSCloudTrail - S3 bucket suspicious ransomware activity (Kusto)
- AWSCloudTrail - S3 Object Exfiltration from Anonymous User (Kusto)
- AWSCloudTrail - Successful brute force attack on S3 Bucket (Kusto)
- S3 Access Via VPC Endpoint From External IP (Panther)
Rule body kusto
id: f3e2d35f-1202-4215-995c-4654ef07d1d8
name: Suspicious access of BEC related documents in AWS S3 buckets
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 AWS S3 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.'
severity: Medium
requiredDataConnectors:
- connectorId: AWS
dataTypes:
- AWSCloudTrail
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(AWSCloudTrail
| where TimeGenerated between (ago(14d)..ago(0d))
| where UserIdentityAccountId != "anonymous"
| where EventSource startswith "s3."
| where EventName =~ "GetObject"
| extend FilePath = tostring(parse_json(RequestParameters).key)
| where FilePath has_any(BEC_Keywords)
);
Events
| summarize dcount(FilePath) by UserIdentityPrincipalid, bin(startofday(TimeGenerated), 1d)
| summarize CountOfDocs = make_list(dcount_FilePath, 10000), TimeStamp = make_list(TimeGenerated, 10000) by UserIdentityPrincipalid
| 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, UserIdentityPrincipalid
| join kind=inner(Events | extend TimeStamp = startofday(TimeGenerated)) on TimeStamp, UserIdentityPrincipalid
| extend Name = iif(UserIdentityUserName contains "@", split(UserIdentityUserName, "@")[0], UserIdentityUserName)
| extend UPNSuffix = iif(UserIdentityUserName contains "@", split(UserIdentityUserName, "@")[1], "")
| project-reorder TimeGenerated, UserIdentityType, UserIdentityPrincipalid, UserIdentityUserName, FilePath, EventName, UserAgent, SourceIpAddress, CountOfDocs, Baseline, Score
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserIdentityUserName
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIpAddress
- entityType: File
fieldMappings:
- identifier: Name
columnName: FilePath
customDetails:
UserType: UserIdentityType
Event: EventName
UserAgent: UserAgent
alertDetailsOverride:
alertDisplayNameFormat: Suspicious access of {{CountOfDocs}} BEC related documents in AWS S3 buckets by {{UserIdentityUserName}}
alertDescriptionFormat: |
This query looks for users (in this case {{UserIdentityUserName}}) with suspicious spikes in the number of files accessed (in this case {{CountOfDocs}})that relate to topics commonly accessed as part of Business Email Compromise (BEC) attacks. The query looks for access to files in AWS S3 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.
version: 1.0.4
kind: Scheduled
Stages and Predicates
Parameters
let sensitivity = 2.5;
Let binding: BEC_Keywords
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']);
The stages below define let Events (the rule's main pipeline source).
Stage 1: source
AWSCloudTrail
Stage 2: where
| where TimeGenerated between (ago(14d)..ago(0d))
Stage 3: where
| where UserIdentityAccountId != "anonymous"
Stage 4: where
| where EventSource startswith "s3."
Stage 5: where
| where EventName =~ "GetObject"
Stage 6: extend
| extend FilePath = tostring(parse_json(RequestParameters).key)
Stage 7: where
| where FilePath has_any(BEC_Keywords)
References BEC_Keywords (defined above).
The stages below run on Events (the outer pipeline).
Stage 8: summarize
Events
| summarize dcount(FilePath) by UserIdentityPrincipalid, bin(startofday(TimeGenerated), 1d)
Stage 9: summarize
| summarize CountOfDocs = make_list(dcount_FilePath, 10000), TimeStamp = make_list(TimeGenerated, 10000) by UserIdentityPrincipalid
The stages below score time-series anomalies (make-series, series_decompose_anomalies).
Stage 10: extend
| extend (Anomalies, Score, Baseline) = series_decompose_anomalies(CountOfDocs, sensitivity, -1, 'linefit')
Stage 11: 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 12: where
| where Anomalies > 0
Stage 13: project
| project TimeStamp, CountOfDocs, Baseline, Score, Anomalies, UserIdentityPrincipalid
Stage 14: join
| join kind=inner(Events | extend TimeStamp = startofday(TimeGenerated)) on TimeStamp, UserIdentityPrincipalid
Stage 15: extend
| extend Name = iif(UserIdentityUserName contains "@", split(UserIdentityUserName, "@")[0], UserIdentityUserName)
Stage 16: extend
| extend UPNSuffix = iif(UserIdentityUserName contains "@", split(UserIdentityUserName, "@")[1], "")
Stage 17: project-reorder
| project-reorder TimeGenerated, UserIdentityType, UserIdentityPrincipalid, UserIdentityUserName, FilePath, EventName, UserAgent, SourceIpAddress, 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 |
|
EventName | eq |
|
EventSource | starts_with |
|
FilePath | match |
|
UserIdentityAccountId | 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 |
UserIdentityPrincipalid | project |
Name | extend |
UPNSuffix | extend |