Fear of configuring the outbound anti-spam policy [with KQL]
What is the out-bound spam policy
Exchange Online Protection allows us to define an outbound spam policy. In many environments I encounter, the outbound spam policy is often left at its default settings. This means that any email address can potentially send unlimited emails externally and internally (restricted to 0 an hour), and might also be able to automatically forward* emails to another mailbox.
*Auto forwarding is by default on “system managed” state and Microsoft will by default block auto-forwarding.
The risk of “no limit”
A breached employee’s mailbox or a compromised app secret that can send emails can significantly impact your reputation and breach scope. To mitigate this risk and safeguard your company’s reputation, it’s advisable to limit the number of emails that can be sent, both externally and within your organization.
The outbound spam policy will create a threshold for emails than can be send for every hour.
Why is it not configured?
The hardest part of setting this up is getting the right information to make smart choices. The current reports in Exchange Online don’t give us the details we need to make those choices. Administrators struggle to find the information they need to prevent problems based on the limits they set.
Let’s keep things simple and start by writing some straightforward KQL.”
Use Advanced Hunting
If you are new to using KQL or don’t know where to start. Go to security.microsoft.com > Advanced hunting. There you will be able to past all of the queries I wrote to follow along and verify your datasets.
We are going to use the EmailEvents table, which is by default in your advanced hunting as it part of Defender for Office 365.
Find the max send
The following KQL will give you an overview of the max send emails per hour in the last 24h’s for each sender. This dataset is scoped to 24h’s to start with a limited scope and validate my dataset.
EmailEvents
| where Timestamp >= ago(24h)
| where EmailDirection == "Intra-org" // or Outbound
| extend hour = datetime_part("Hour", Timestamp)
| summarize emailCount = count(NetworkMessageId) by SenderFromAddress, hour
| summarize maxEmailCount = max(emailCount) by SenderFromAddress
| order by maxEmailCount desc
| render columnchart // remove this line to see raw data
Calculate the hourly average
This KQL will calculate the average amount of email send per hour. Of course averages don’t show the impact of the a sudden peak of in email send. You can use this query to identity senders that deviate to much from your other users.
EmailEvents
| where EmailDirection == "Outbound" // or "Intra-org"
| extend hour = datetime_part("Hour", Timestamp)
| summarize emailCount = count(NetworkMessageId) by SenderFromAddress, hour
| summarize avgEmailsPerHour = avg(emailCount) by SenderFromAddress
| order by avgEmailsPerHour desc
Find the peak
The following KQL allows you to identify at what hour of the day a specific account performs his highest peak of sending emails. Keep in mind that is important as the hourly peak will determine our threshold as this reset each hour.
EmailEvents
| where Timestamp >= ago(24h)
| where EmailDirection == "Outbound" // or "Intra-org"
| where SenderFromAddress == "" //fill in the sender
| extend hour = datetime_part("Hour", Timestamp)
| summarize emailCount = count(NetworkMessageId) by hour
| order by hour asc
| render linechart
Max send over multiple days for a specific account
Not every weekday has the same amount of email activity. It’s possible that at the beginning of the week or month, more emails are sent than at the end, or that the emails are saved until a specific point in time and then sent. That’s why we also need to analyze the traffic over a couple of days. You can use this query to zoom in on a specific sender.
EmailEvents
| where Timestamp >= ago(10d)
| where EmailDirection == "Outbound" // or "Intra-org"
| where SenderFromAddress == "" //replace with specific email address
| extend day = format_datetime(Timestamp, 'MM/dd/yyyy')
| extend hour = datetime_part("Hour", Timestamp)
| summarize emailCount = count(NetworkMessageId) by SenderFromAddress, day, hour
| order by emailCount
In the screenshot above, I identified that for a specific email address, the peak of the emails occurs each day at 21:00.
The highest send emails per hour in the last 10 days
This is the most comprehensive query that allows you to identify which thresholds you can set. This KQL displays the highest number of sent emails in the last 10 days at a specific hour of each day. I
EmailEvents
| where Timestamp >= ago(10d)
| where EmailDirection == "Outbound" // or "Intra-org"
| extend day = format_datetime(Timestamp, 'MM/dd/yyyy')
| extend hour = datetime_part("Hour", Timestamp)
| summarize emailCount = count(NetworkMessageId) by SenderFromAddress, day, hour
| summarize maxEmailCount = arg_max(emailCount, *) by SenderFromAddress
| order by maxEmailCount desc
Now go and configure
Now that you have the necessary insights, I can only suggest going ahead and configure. I often find myself creating a policy for service accounts with a high volume of emails and setting the default anti-spam policy to a threshold that matches for regular users.
Make sure that you perform the exercise for outbound & intra-org mail traffic, as these have separate thresholds.
Don’t be afraid if somebody hits your threshold because you might identify business processes that shouldn’t be running via a regular user account.