Contents

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.

/en/sync-defender-cloud-alerts-sentinel-incidents/images/DefenderForCloudAlertInMicrosoftSentinel.png
Defender for Cloud incident in Microsoft Sentinel

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.

/en/sync-defender-cloud-alerts-sentinel-incidents/images/MicrosoftDocs.png
Changing the status of an alert in Defender for Cloud will not affect the status of any Microsoft Sentinel incidents that contain the Microsoft Sentinel alert, only that of the alert itself.

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.

/en/sync-defender-cloud-alerts-sentinel-incidents/images/SentinelSecurityAlertTable.png
SystemAlertId is used as an anchor by the Sentinel incident

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.

/en/sync-defender-cloud-alerts-sentinel-incidents/images/SentinelSecurityIncidentTable.png
One incident can relate to multiple alerts

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.

/en/sync-defender-cloud-alerts-sentinel-incidents/images/DfCEnableIncidentCreation.png
A essential step to get incidents from Defender for Cloud. Enable the incident creation.

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.

/en/sync-defender-cloud-alerts-sentinel-incidents/images/AlertStateNew.png
New Defender for Cloud alert synched to Sentinel

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.

/en/sync-defender-cloud-alerts-sentinel-incidents/images/SentinelAssignAndStartWork.png
Each change in represented in the SecurityIncident table.

But this does not change anything at the alert level.

/en/sync-defender-cloud-alerts-sentinel-incidents/images/SentinelAssignAndStartWork-Alert.png
O Changes, Where Art Thou?

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.

/en/sync-defender-cloud-alerts-sentinel-incidents/images/SentinelIncidentSolved.png
The sentinel incident status is synced to the alert.

In the SecurityAlerts table a new entry is created, reflecting this change.

/en/sync-defender-cloud-alerts-sentinel-incidents/images/SecurityAlertResolved.png
Now we are talking. Change accepted.

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.

/en/sync-defender-cloud-alerts-sentinel-incidents/images/SecurityAlertInProgress.png
Synchronization works as advertised

/en/sync-defender-cloud-alerts-sentinel-incidents/images/SecurityIncidentNoChange.png
Nothing to see here

/en/sync-defender-cloud-alerts-sentinel-incidents/images/AlertStateInProgress.png
Defender for Cloud changes are not synchronized to the Sentinel incident...

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.

/en/sync-defender-cloud-alerts-sentinel-incidents/images/AlertStateResolved.png
...even if the DfC alert is resolved.

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.

/en/sync-defender-cloud-alerts-sentinel-incidents/images/ProcessLogic.png
Simple logic based on alert state

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

/en/sync-defender-cloud-alerts-sentinel-incidents/images/deploytoazure.png /en/sync-defender-cloud-alerts-sentinel-incidents/images/deploytoazuregov.png /en/sync-defender-cloud-alerts-sentinel-incidents/images/visualizebutton.png

Inspect ARM template

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

/en/sync-defender-cloud-alerts-sentinel-incidents/images/DeploySystemManagedIdentity.png
Deploy using a system managed identity

User managed identity

/en/sync-defender-cloud-alerts-sentinel-incidents/images/deploytoazure.png /en/sync-defender-cloud-alerts-sentinel-incidents/images/deploytoazuregov.png /en/sync-defender-cloud-alerts-sentinel-incidents/images/visualizebutton.png

Inspect ARM template

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.

/en/sync-defender-cloud-alerts-sentinel-incidents/images/DeployUserManagedIdentity.png
Deploy using a user managed identity

Info
Personally I would recommend using a system managed identity.

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"
  }
}