Sync Defender for Cloud Alerts with Sentinel Incidents
When working with Defender for Cloud and Microsoft Sentinel the two product greatly integrate into each other. If integration is enabled each Defender for Cloud alert will generate an Sentinel incidents which contains the entities, description, the title and more information of the DfC alert. Also there is a direct link to the alert and if bi-directional alert synchronization is enabled it keeps the alerts, you guessed it, in sync.
But there is one caveat in this integration.
Defender for Cloud alerts and Sentinel incidents are not kept in sync.
This is clearly stated in the Microsoft Sentinel documentation but many people would expect otherwise.
Incident vs. alert
The root cause for this behavior is, that Sentinel incidents are not the same as an Sentinel alert. Let’s look under the hood.
Alerts
All alerts are stored in the SecurityAlert
table and contain a multitude of different information like the source product name (Defender for Cloud aka. Azure Security Center), entities, display name and more. But most importantly for us currently is the property SystemAlertId
.
Incidents
Incidents on the other hand are stored in the SecurityIncident
table and contain by far less information. There is no property for entities or remediation steps. But there is a property called AlertIds
which contains an array of one or more alert ids, referencing the security alert that is stored in the SecurityAlert
table.
That way Sentinel can group multiple alerts into one incident if they belong together.
In the case of Defender for Cloud Alerts this incident creation is done by the Microsoft Analytics rule “Create incidents based on Microsoft Defender for Cloud” and the relationship between alert and incident is 1:1 (there are edge cases).
When enabling the data connector for Defender for Cloud you must manually activate the incident creation otherwise the security alerts would be synchronized but never surfaced as incidents.
Alert update logic
Both of those tables can contain multiple versions for the same alert or incident, created whenever a change is made to the respective type.
When an alert is first ingested from Defender for Cloud the status of the alert and the related incident is as follows.
Sentinel to Defender for Cloud
If now the SOC analyst would take over and assign the incident to themselves and set the incident state to active this will result in two new entries in the SecurityIncident
table reflecting those changes.
But this does not change anything at the alert level.
When you now resolve the incident in Sentinel, this will trigger a more complete flow. The Sentinel incident is solved, the Sentinel alert is resolved and the Defender for Cloud alert is resolved.
In the SecurityAlerts table a new entry is created, reflecting this change.
This is mostly as expected and documented, but now let’s have a look at what is currently missing.
Defender for Cloud to Sentinel
When you change the alert in Defender for Cloud to in progress this will create a second entry in the SecurityAlert
table reflecting that the subscription owner is working on this alert. As the Microsoft docs state, this is not reflected in the SecurityIncident
table since the incident is not part of the sync.
And this is also the case if the alert was resolved by the subscription owner. In Sentinel you can only see this change when looking at the particular alert itself, but not the incident.
Behavior summary
Initiator | Action | DfC alert | Sentinel alert | Sentinel incident |
---|---|---|---|---|
Defender for Cloud | New alert | Active | New | New |
Defender for Cloud | Set alert to In progress | In Progress | InProgress | New |
Defender for Cloud | Dismiss alert | Dismissed | Dismissed | New |
Defender for Cloud | Resolve alert | Resolved | Resolved | New |
Sentinel | Change incident status to active | Active | New | Active |
Sentinel | Resolve incident | Resolved | Resolved | Resolved |
Is this a problem?
In many cases, the missing sync between Defender for Cloud alert changes and the Sentinel incident is just a nuisance, but because the SOC team will investigate the alert regardless what the subscription owner does it does not matter.
But if you want to enable a feedback loop and the subscription owner is part of solving the incident this missing sync is really annoying. Either you grant the subscription owner access to Sentinel to close the incident themselves or your SOC analyst checks for changes to the alerts. Both options are not particular optimal in many cases.
Solution
Since the Defender for Cloud alert changes are replicated to Sentinel alerts it is not that complicated to create Logic App that solves the missing link.
Basic logic
Basically the logic app will check for any changes to the SecurityAlert
table and compare the current SecurityIncident
state to the alert state. This way if the incident is not reflecting the current alert status the logic app can make this change.
I decided for my environment that
- A dismissed Defender for Cloud alert will be handled as a benign positive
- A resolved Defender for Cloud alert will be handled as a true positive
Since Defender for Cloud does not offer any comment functionality the closure text will be a static text and not include something dynamic.
KQL query
Because building advanced logic in Logic Apps is something I try to avoid like the cat avoids the water my “brain” of this operation is built into this KQL query.
I added a lot of comments in the query itself, but still let’s do a quick rundown what I want to achieve with this query.
Get all security incidents from the last 30 days that are not yet closed. Check if the related alerts are all in a resolved or dismissed state or if the related alert is in progress. Avoid incidents with more than one alert if one of them is still under investigation.
SecurityIncident
| where TimeGenerated > ago(30d)
// Limit results to incidents that are from Defender for Cloud
| where AdditionalData.alertProductNames has "Azure Security Center"
// Get only the latest incident status
| summarize arg_max(TimeGenerated,*) by IncidentNumber
// Only return incidents that are not closed
| where Status != "Closed"
// Join all alerts based on the AlertId with the latest alert event
| mv-expand AlertIds
| extend SystemAlertId = tostring(AlertIds)
| join kind=inner ( SecurityAlert | summarize arg_max(TimeGenerated,*) by SystemAlertId | project SystemAlertId, Status ) on SystemAlertId
| extend AlertStatus = Status1
| extend IncidentStatus = Status
// Summarize the results to filter out any incidents where there are more than one alert and one of them is either in status "New" or "InProgress"
// to avoid prematurely closing the whole incident.
// Normally there should always be a 1:1 mapping, but a SOC analyst might have added related alerts
// https://learn.microsoft.com/en-us/azure/sentinel/relate-alerts-to-incidents
| summarize by IncidentName, IncidentStatus, AlertStatus
| summarize AlertStatus = make_set(AlertStatus) by IncidentName, IncidentStatus
| where not (AlertStatus has_any ("New", "InProgress") and array_length(AlertStatus) > 1) and not (AlertStatus has_any ("New"))
| mv-expand AlertStatus
// Remove all incidents that already are in status active, when the alert status is inProgress
| where not ( AlertStatus == "InProgress" and IncidentStatus == "Active" )
Logic App
The logic app itself uses a managed identity to send the query to the Sentinel Log Analytics workspace and loops through the results.
Depending on the alert status it will either close the incident as true positive, benign positive or set the incident to active.
Deploy
If you want to implement this solution yourself I created a (actually two) neat little ARM template for the job. It’s a all in one package that will deploy the needed logic app, the Sentinel connector, set the correct permissions (Microsoft Sentinel Responder) for the managed identity and depending on the option you choose uses either an system managed identity or an user managed identity.
System managed identity
Using a system managed identity has the benefit of not needing to worry about anything left behind if you remove the Logic App in the future.
You just define the following parameters, deploy the template and enable the Logic App.
- Subscription and resource group to deploy the Logic App and Sentinel connection
- Enter your Sentinel workspace name
- Change the logic app name or connection name to your liking
- If the Sentinel workspace is not in the same resource group as the Logic App you are going to deploy please enter the correct resource group name
User managed identity
The only difference when using this deployment method is that it will create a user managed identity and use that for all connections to the Sentinel workspace. Other than that it’s exactly the same.
Known issues
Deploy to a different subscription
It’s not supported to deploy the ARM template to another subscription as the Sentinel workspace is deployed to. A different resource group is fine, but another subscription was too much of an hassle which is why I choose not to do it.
InsufficientAccessError
Currently it can happen that the first time the logic app is run the KQL query will fail with an InsufficientAccessError
. I’m not sure if this because I’m to impatient or some other caching issue, but you can run the Logic App immediately afterwards again and it will just work.
{
"error": {
"message": "The provided credentials have insufficient access to perform the requested operation",
"code": "InsufficientAccessError",
"correlationId": "b6c48692-cabf-449a-ba8d-fed52ad6d174"
}
}