Today’s article will be about the newly introduced /analyzedEmails Graph API endpoint, which in a nutshell is a lightweight Threat explorer implementation. While the new API fails to measure up to the robust tool that Threat explorer is, this is not to say it has no merit, as it does address a long-time ask – providing a Graph API endpoint for fetching data about received and send messages, analogous to the message trace functionality. In addition, the new API gives us threat analysis details as well as some remediation capabilities. Let’s dig right in!
The need for message trace reporting
Even though Exchange Online was one of the first workloads within the Office 365 suite to get reporting capabilities, a lot of customers wanted even more insight into their mail flow. Common questions unaddressed by the built-in reporting include: breakdown of the number/size of messages per domain, external vs internal breakdown, messages sent/received by a given smtp alias (as opposed to the primary SMTP address), as well as some more obscure reports such as breakdown by size or number of attachments. In addition, the built-in reports do not include data for all recipient type, thus in many cases you have to build your own solution.
For years, the to go tool for building such reports was the Get-MessageTrace cmdlet and its siblings. In fact, one of the most popular scripts over at the TechNet Gallery back in the day was Alan Byrne’s Office 365 Mail Traffic Statistics by User, which you can still get thanks to the Internet Time Machine project! Reports based on said script and similar solutions were de facto the standard and continue to be used nowadays.
Sadly, PowerShell-based solutions have their own challenges and limitations, and when a more robust solution was needed, organizations opted to use the Reporting Web Service instead, which exposed Message trace data via one of its endpoints. In fact, the service is still up an running today, although Microsoft has long stopped updating it with new features (apart from bringing support for OAuth back in 2022). With that in mind, many of the organizations that leveraged said web service have been asking for a similar solution based on the Graph API, but Microsoft turned a blind eye to such requests.
Things got even more frustrating when the Threat explorer tool was introduced, as the data it exposed in the “details area” resembled message trace data, enriched with some additional properties. And, similar to message trace, if offered “near real time” experience. While the Threat explorer tool was (and still is) a great addition, frustration arose from the fact that the underlying APIs were not exposed to the public. We did get a private preview of a MessageTrace-like API, but this effort did not materialize as something every tenant can use.
Meet the Analyzed Email API!
Well, at long last, Microsoft is now introducing a Graph API based solution that you can not only use to obtain near real-time mail flow information, similar to what the message trace does, but also fetch additional threat-related information and even act on (“remediate”) messages. Dubbed as Analyzed email, the new API currently has three methods exposed under the /beta/security/collaboration/analyzedEmails endpoint: GET, LIST and REMEDIATE. While the official documentation is currently light on some details, it is sufficient to get us started, so let’s test the new API!
First things first, we need to cover the required permissions. An important bit is that all three methods exposed for this new endpoint are currently only available via application permissions, which is luckily what we want to use for a fully automated solution. Two permissions are available: the read-only SecurityAnalyzedMessage.Read.All, which is what you want to use for any “reporting” scenarios, and the SecurityAnalyzedMessage.ReadWrite.All one, used for remediation. As with all other things Graph, add the relevant permission to your app and grant admin consent before attempting to use the API!
As mentioned above, the API only has a single endpoint, available under /beta/security/collaboration/analyzedEmails. The LIST method is what you want to use for our “message trace report” scenario. Interestingly, Microsoft has introduced two “non-standard” query parameters to use with said method, namely startTime and endTime. Both are in fact optional, and if you run the query without providing them, data from the past 24 hours will be returned. By leveraging said parameters, we can be more specific on the time frame to cover, with the maximum supported value being 30 days. Note that the parameters accept a date value, so it’s not necessary to use a full datetime one.
#Return data for the past 24 hours GET https://graph.microsoft.com/beta/security/collaboration/analyzedEmails #Get data for specific period GET https://graph.microsoft.com/beta/security/collaboration/analyzedEmails?startTime=2024-07-05T11:15:48Z&endTime=2024-07-06T11:15:48Z #You can also use a date value GET https://graph.microsoft.com/beta/security/collaboration/analyzedEmails?startTime=2024-07-05&endTime=2024-07-06
NOTE: do not prefix the startDate and endData parameters with the $ character, doing so will cause the API to ignore them.
Similar to other Graph API endpoints, the LIST query against /beta/security/collaboration/analyzedEmails returns results in pages, with the default size of a page being 50. You can control the page size via the $top query parameter, and maximum page size seems to be 999 (although you have to use $top=1000 to get 999 results, and similarly for any lower value). The example below shows how you can get the full set of results for a specific timeframe, by leveraging the @odata.nextLink hint.
#Fetch all results for a specific timeframe (multiple pages) $results = @() $uri = "https://graph.microsoft.com/beta/security/collaboration/analyzedEmails?startTime=2024-07-06&endTime=2024-08-05&`$top=1000" do { $response = Invoke-RestMethod -Method GET -Uri $uri -Headers @{"Authorization" = "Bearer $($Token.AccessToken)"} $uri = $response.'@odata.nextLink' $results += @($response.value) Write-Host $uri } while ($uri)
Unfortunately, the $count operator does not work as expected and the corresponding @odata.count property seems to only be returned when you run the LIST query without any parameters. This in turn makes it impossible to quickly get the full count of events within a specific timeframe, which one would usually do by combining $top=1 with $count. Instead, you are left with fetching the full set of events and counting them client-side.
Fetching additional threat processing details
In addition to the LIST method, we also have the GET one, /security/collaboration/analyzedEmails/{id}. Compared to the data you get via the LIST method, the GET one exposes the full list of attachments and urls, if any. More importantly, any threat-related details for processing such entities and the message as a whole are also returned. As the LIST method does not support the $expand operator, this is the only method to obtain said data, in case you are interested in it. For each of these entities you can get additional details as to the file/url itself, any detected threats, as well as the results of any detonation, if performed.
The screenshot below illustrates how a event retrieved via the GET method looks like. I have highlighted the properties that map to data exposed in (detailed) message trace, although some property names might differ. For example, Message Trace ID (which you can filter on via –MessageTraceId) maps to networkMessageId in the API. The rest of the properties are what the /analyzedEmails API adds to the mix – details about Defender’s processing of the message. This is indeed the primary value you get from the new API, along with the ability to take actions on messages via the REMEDIATE method.
Comparison with message trace
The screenshot below compares message data received from both methods, with message trace output on the right. As you can see, both methods return the same basic “mail flow” data, apart from delay of few seconds in reporting the message’s timestamp, in favor of the message trace method. Interestingly, some other properties do not match in value as well. For example both methods might show different size for the message, and different delivery location.
There are other places where both methods differ. First, the default output for the /analyzedEmails endpoint (i.e. without using any parameters) returns data from the past 24 hours only, whereas the message trace one includes 48 hours worth of entries. Using additional parameters, you can fetch up to 30 days via the new API, whereas message trace gives you maximum of 10 days in synchronous mode (up to 90 if you run it async via Start-HistoricalSearch). The number of entries returned by default also differs, with the output capped at 50 for the /analyzedEmails endpoint, and up to 1000 for the Get-MessageTrace method.
The biggest difference however comes down to filtering capabilities. While in theory the /analyzedEmails endpoint supports the $filter parameter, it looks like you can only filter by the the networkMessageId property. The only way to obtain the value for said property is via the API (or message trace), so this is practically useless. The documentation currently lacks any description as to which other properties are filterable, and the only other example we get is for recipientEmailAddress. In my tests however, a filter on recipientEmailAddress only works when combined with networkMessageId, so again, zero value.
What does work is filtering based on datetime value, which technically is not part of $filter but enabled via the startDate and endDate parameters. And that’s it. You cannot filter by sender or recipient, at least not without combining it with message id. You cannot filter on domain, you cannot filter on directionality, delivery status, IP or even on internetMessageId. And, much like with the message trace, you cannot filter by subject either. In effect, you will end up having to fetch all the entries in a specific timeframe first, before applying any client-side filter. This is in stark contrast with the Threat explorer, which is often praised for its robust filtering capabilities.
Example on remediation
The limitations mentioned above in turn make it hard to work with individual messages, as you either have to fetch the full list and sift through it, or use other methods (such as message trace, the advanced hunting API or Threat explorer) in order to get the id of the message. Only once you have the id you can use the GET or REMEDIATE methods. The latter allows you to perform actions such as deleting the message or moving it to the junk folder, and is what you would use for incident response type of scenarios. The action is executed via POST request against the /analyzedEmails/remediate endpoint, with JSON payload featuring the following parameters:
- displayName – give the action a name. Required.
- description – an optional description.
- severity – specify the severity of the operation, either low, medium or high. Optional?
- action – the action to perform. Currently, only delete and move actions are supported. Available values include: moveToJunk, moveToInbox, hardDelete, softDelete, moveToDeletedItems. Required.
- remediateSendersCopy – optional parameter, used to specify whether to also process the stored copy of a sent message.
- analyzedEmails – an array of networkMessageId identifiers and recipientEmailAddress values, used to designate the messages to process in the respective mailbox. Both values are required.
Here is an example of how a REMEDIATE action might look like. In this case, we will be deleting a message delivered to a mailbox. As mentioned above, the hardest part of the process is getting the networkMessageId, due to the limited filtering capabilities of the API. Once you have that, we can prepare the payload and issue the request. This operation will require the SecurityAnalyzedMessage.ReadWrite.All scope, so make sure you’ve granted that!
#Soft-delete a message { "analyzedEmails": [ { "recipientEmailAddress": "vasil@michev.info", "networkMessageId": "db0377e9-bd35-4443-af18-08dcb4387b1d" } ], "displayName": "Delete junk message", "action": "softDelete", "severity": "medium" } POST https://graph.microsoft.com/beta/security/collaboration/analyzedEmails/remediate
Successful execution is indicated via 202 Accepted status code. To check the actual status of the action, you have to use the Action center in the Security portal, and you can get a deep link to the action in question by looking at the response headers:
Location https://security.microsoft.com/action-center/history?filters=%7B%22bulkId%22:%5B%2203d3cf7b-bdc9-4dc4-bddc-14e4870423f2%22%5D%7D&tid=923712ba-352a-4eda-bece-09d0684d0cfb
While you can process multiple items, the documentation currently does not specify what the supported limits on that are. In addition, only move and delete actions are supported, so if you want to trigger investigation or a message submission, you’re out of luck.
Conclusion
In summary, we reviewed the newly introduced /analyzedEmails endpoint and the methods it supports. While nice to have, the new API pales in comparison to the feature-rich Threat explorer and can only be used for relatively basic scenarios. Still, it is a nice addition to the service, and can be considered a worthy replacement for the message trace functionality, at least when it comes to reporting around received/sent messages. More importantly, it can enrich your message trace data with details around Defender’s processing of the message and any files and urls found within it.
The other functionality introduced via the API is remediation, i.e. its ability to perform actions against individual messages or groups of items. The limited filtering capabilities will however present some challenges for incident response scenarios, as getting the message ID will likely require you to use different methods, such as advanced hunting API, Threat explorer, or good old message trace. At the end of the day you might end up using multiple APIs/endpoints… but this is probably a cheap price to pay for the opportunity to have a fully automated solution.
As of ~2024-11:
These APIs are only available for Defender for Office 365 Plan 2 or Microsoft 365 A5/E5/F5/G5 Security plans.
Ref: https://learn.microsoft.com/en-us/graph/api/resources/security-api-overview?view=graph-rest-beta#email-and-collaboration-protection
If you don’t have at least 1 of these licenses/subscriptions, will get following error
“`
{
“error”: {
“code”: “UnknownError”,
“message”: “{\”@
odata.context\”:\”https://nam11.dataservice.protection.outlook.com/DataInsights/mdo/security/collaboration/$metadata#Edm.String\”,
\”value\”:\”Invalid subscription\”}”,
}
}
“`