How to get Azure Sentinel Incidents via the Sentinel API

At Info Support, we use an on-premise SIEM in our security monitoring setup. We also use Azure Sentinel as Cloud SIEM for a few customers, to further monitor their resources in Azure. Because keeping an eye at lots of dashboards at once is not feasible, we had one strong requirement: We need a single dashboard, for all the alerts occurring in any of the monitored environments, ours, and those of our customers.

Because we use our own SIEM to monitor our own environment, we do not want all the logs from the Azure-resources of our customers in our on-prem SIEM (the key component of a SIEM is log-correllation, which would not benefit us anyway).

To achieve our requirement, we just want to get incident notifications from Azure Sentinel only to our on-prem SIEM. To achieve this, we had to create a connection between Azure Sentinel and our SIEM, with only the needed information of the incidents.

Our first angle of approach, was to use a Playbook to send the incident information to an Azure Event Hub, and a connector to connect the Event Hub to our SIEM. A big pro for this method was that most of this solution was already made available by Azure and our SIEM vendor. But…. We hit a big con: you can’t use Playbooks with Microsoft’s predefined Azure Sentinel rules, one of which was a rule that generates incidents from an Azure Security Center alarm.

Moving to our next angle of approach. This time, we focussed on using the Azure Sentinel Management API to extract the needed information from Azure Sentinel. Hence: if you only need to trigger an action with custom rules, you can use the Sentinel Playbook approach off course. However, with the Azure Sentinel Management API approach, we can query all incidents including the ones that are triggered by Azure Security Center Alerts. Nice!

Playbooks

Azure Sentinel gives you the option to trigger a Playbook when an analytics-rule is hit. A Playbook is in fact an Azure Logic App with an Azure Sentinel function as trigger. After the trigger, you can send your data to almost anything you want. In our case we use an Azure Event Hub.

NOTE: I will not cover the how to use/setup of these Playbooks in-depth, in this blogpost.

When creating an Azure Sentinel Playbook, just create a Logic App with the trigger “When a response to an Azure Sentinel alert is triggered”.  For the ease of use, we’ve added some other actions to gather some entity data (“Alert – Get… “-actions), however this isn’t strictly necessary for sending the information to another system

After we gathered all the data, we send that event to our Azure Event Hub namespace. You can use any format you want, the format in the screenshot below is the format our processor understands, so we didn’t need an extra translation.

Because pictures say more than words the playbook we used:
Playbook Azure Sentinel

Azure Sentinel Management API

The Azure Sentinel Management API is, sadly enough, a not (yet) well/completly documented feature of Azure Sentinel. But it’s possible to do almost everything with Azure Sentinel via the API. You can find some documentation at https://docs.microsoft.com/en-us/rest/api/securityinsights/, but in this blogpost I’m covering an in-depth how-to get the incident information, including the entity information via the Sentinel Management API.

Note: the code snippets are simplified for reading purposes and do not contain error handling/checking! They also parse more output response than needed for the snippets. Of course it’s a good practice to check the output of the commands and log the necessary information about the status.

Permissions and authentication

To call the API and get the Sentinel Incidents, we need read permissions on the Log Analytics workspace used by Azure Sentinel.

If you want to collect the incidents with an unattended script/tool, just like us, the best option is to create an “App registration” in the Azure AD of your tenant, with a Secret and the proper permissions set up for this App Registration on the Log Analytics Workspace.

Setup App Registration

To create an App Registration you can use the guide Microsoft published: https://docs.microsoft.com/nl-nl/azure/active-directory/develop/howto-create-service-principal-portal#register-an-application-with-azure-ad-and-create-a-service-principal.

Next you need to setup a client secret, see option 2 at the Microsoft docs for step-by-step guide: https://docs.microsoft.com/nl-nl/azure/active-directory/develop/howto-create-service-principal-portal#authentication-two-options

When the App is registered, you need to setup an API permission “Log Analytics API / Data.Read”. In our case, we used an application secret to authenticate. You can create one easily via the Azure Portal, see https://docs.microsoft.com/nl-nl/azure/active-directory/develop/howto-create-service-principal-portal#assign-a-role-to-the-application

After configuring the App registration, you also need to setup some permissions on the Log Analytics workspace, which Sentinel runs on. You need to assign the “Security Reader” and “LogAnalytics Reader” role in the “Log Analytics workspace” to your app registration you’ve just created.

To do this, you can follow Microsoft’s documentation: https://docs.microsoft.com/nl-nl/azure/active-directory/develop/howto-create-service-principal-portal#assign-a-role-to-the-application

Get the authentication token

When all is set up, we can call the authentication API to get the access token to call the Sentinel API. For example, you can use the code snippet below.

$authUrl = "https://login.microsoftonline.com/" + $tenantId + "/oauth2/token";

$postParams = @{ 
    resource = "https://management.azure.com"
    grant_type = "client_credentials"
    client_id = $client_id
    client_secret = $client_secret
}

$authResponse = Invoke-RestMethod -Method POST -Uri $authurl -Body $postParams

$sessionExpires = $authResponse.expires_on
$apiToken = $authResponse.access_token

Authentication via credentials

You can also use your credentials to authenticate to the API if you need. However, when proper authentication solutions with MFA are enabled, this will not be suitable for unattended use, because you need to confirm your MFA-challenge again on a token refresh.

The easiest way to do this is via the cmd-lets in the Az PowerShell module (https://docs.microsoft.com/nl-nl/powershell/azure/new-azureps-module-az?view=azps-5.5.0),

First you need to connect to Azure via the “Connect-AzAccount” cmd-let. This cmd-let will prompt for your credentials and will challenge the MFA (if applicable). Next you can get your  AccessToken via the PowerShell snippet below.

$context = Get-AzContext
$profile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile
$profileClient = New-Object -TypeName Microsoft.Azure.Commands.ResourceManager.Common.RMProfileClient -ArgumentList ($profile)
$token = $profileClient.AcquireAccessToken($context.Subscription.TenantId)

From now on, you have the token to access the API in $token.AccessToken, in $token.Expired you can check if your token needs a refresh (which can be done by the same snippet).

A note of warning: if you have multiple connected accounts / cached account’s this snippet will not work properly. You first need to call the “Disonnect-AzAccount” and the “Clear-AzContext” cmd-lets to clear you cache.

Get (new) incidents

With a simple GET-request we can get the incidents from Sentinel. For this request, we need the “subscriptionId”, “resource group name”, and the “workspace name“ of the log analytics workspace used for Azure Sentinel.

$workspace = '/subscriptions/'+$subscriptionId+'/resourcegroups/'+$workspaceResourceGroup+'/providers/microsoft.operationalinsights/workspaces/'+$workspaceName.

The $workspace variable will be used in all the calls to the Sentinel Management API. Adding the authentication header to all our calls is also vital:

$headers = @{
    'Authorization' = "Bearer $apiToken "
    'Content-Type'  = 'application/json'
}
# Used single qoutes because we need to treat the $ in the URL as text, not as variable as they are part of the oData implementation.
$URI = 'https://management.azure.com' + $workspace + '/providers/Microsoft.SecurityInsights/incidents?api-version=2020-01-01

Filtering / ordering / limiting

Off course you can also filter, order, or limit the query via the filter query string, which is in fact an oData implementation.

If you want, for example, to order on IncidentNumber and limit to 1000 incidents (default is 20), you can append the following to your URI.

“&$orderby=properties/incidentNumber desc&$top=1000” 

You can also create a filter, for example to query only incidents with an IncidentNumber greater than X (for example 10) to do this, just append the desired filter URI. Off course you can filter on any property in the result set.

“&$filter=properties/incidentNumber gt 10”

If your filter doesn’t match any incidents you’ll get an empty result (but still a response code 200 OK). When your filter matches one or more incidents the API gives the output below:

{
    "value": [
		{
            "id": "<snip>",
            "name": "<snip>",
            "etag": "<snip>",
            "type": "Microsoft.SecurityInsights/Incidents",
            "properties": {
                "title": "Custom Alert Rule",
                "description": "Custom incident rule",
                "severity": "Medium",
                "status": "New",
                "owner": {
                    "objectId": null,
                    "email": null,
                    "assignedTo": null,
                    "userPrincipalName": null
                },
                "labels": [],
                "firstActivityTimeUtc": "2021-05-13T09:41:08.844Z",
                "lastActivityTimeUtc": "2021-05-13T09:41:08.844Z",
                "lastModifiedTimeUtc": "2021-05-13T10:30:23.0752524Z",
                "createdTimeUtc": "2021-05-13T10:30:23.0615255Z",
                "incidentNumber": 113,
                "additionalData": {
                    "alertsCount": 1,
                    "bookmarksCount": 0,
                    "commentsCount": 0,
                    "alertProductNames": [
                        "Azure Sentinel"
                    ],
                    "tactics": [
                        "Persistence"
                    ]
                },
                "relatedAnalyticRuleIds": [ "<snip>" ],
                "incidentUrl": "<snip>",
                "providerName": "Azure Sentinel",
                "providerIncidentId": "113"
            }
        }
	]
}

If this is all you need, then you’re done! If you also need the entities that are related to the incident you’re in luck! Read the next paragraph.

Get entity data

Most of the time you also want some more detailed information about the incident, for example: what is the Account or IP (called Entities) from the event(s) that triggered the incident. To get these Entities, you’ll need to make a few extra calls to the Sentinel Management API.

As a rule of thumb, a Sentinel incident is always based on a Security Alert in the underlying Log Analytics workspace. For gathering the entity data related to this Security Alert, we first need to find the “SecurityAlertId” on which the Sentinel incident is based on.

Do this by sending a GET request with the ‘incidentId’ (GUID) to the relations part of the API.

$relationsURI = "https://management.azure.com" + $workspaceId + "/providers/Microsoft.SecurityInsights/incidents/"+$incidentId+"/relations?api-version=2020-01-01"
$relationsData = Invoke-RestMethod -Uri $relationsURI -Method GET -Headers $headers
$securityAlertId = $relationsData.value.properties.relatedResourceName

As you can see, the securityAlertId is stored in a property called “relatedResourceName” from the object returned by the API. The full result is shown below:

{
    "value": [
        {
            "id": "<snip>",
            "name": "<snip>",
            "type": "Microsoft.SecurityInsights/Incidents/relations",
            "properties": {
                "relatedResourceId": "<snip>",
                "relatedResourceName": "<snip>",
                "relatedResourceType": "Microsoft.SecurityInsights/entities",
                "relatedResourceKind": "SecurityAlert"
            }
        }
    ]
}

The next step is to find the SystemAlertId of the Security Alert, we can do this by – again – sending a GET request to the Sentinel API, but this time with the SecurityAlertId of the previous call.

$securityAlertURI = "https://management.azure.com" + $workspaceId + "/providers/Microsoft.SecurityInsights/entities/"+$securityAlertId+"?api-version=2020-01-01"
$securityAlertData = Invoke-RestMethod -Uri $securityAlertURI -Method GET -Headers $headers
$systemAlertId = $securityAlertData.properties.systemAlertId

The full result of this call is shown below, but we need to fetch the systemAlertId property before we can take the next and final step.

{
    "id": "<snip>",
    "name": "<snip>",
    "type": "Microsoft.SecurityInsights/entities",
    "kind": "SecurityAlert",
    "properties": {
        "systemAlertId": "<snip>",
        "tactics": [],
        "alertDisplayName": "Custom Alert Rule",
        "confidenceLevel": "Unknown",
        "severity": "Medium",
        "vendorName": "Microsoft",
        "productName": "Azure Sentinel",
        "productComponentName": "Scheduled Alerts",
        "alertType": "<snip>",
        "processingEndTime": "2021-05-13T10:20:08.8578918Z",
        "status": "New",
        "endTimeUtc": "2021-05-13T09:41:08.844Z",
        "startTimeUtc": "2021-05-13T09:41:08.844Z",
        "timeGenerated": "2021-05-13T10:20:08.844Z",
        "providerAlertId": "<snip>",
        "resourceIdentifiers": [
            {
                "type": "LogAnalytics",
                "workspaceId": "<snip>"
            }
        ],
        "additionalData": {
            "ProcessedBySentinel": "True",
            "Search Query Results Overall Count": "1",
            "Query Start Time UTC": "2021-05-13T09:28:21Z",
            "Query End Time UTC": "2021-05-13T11:28:21Z",
            "Analytic Rule Name": "Custom Alert Rule",
            "Trigger Threshold": "0",
            "Analytic Rule Ids": "["<snip>"]",
            "Trigger Operator": "GreaterThan",
            "Event Grouping": "SingleAlert",
            "Query Period": "02:00:00",
            "Data Sources": "["<snip>"]",
            "Query": "<snip>",
            "Total Account Entities": "1",
            "Total IP Entities": "1"
        },
        "friendlyName": "Custom Alert Rule"
    }
}

The last step – to get the entities – is to make a POST-Call to the Expand endpoint of the API. We need to POST a hardcoded GUID to the API to get the entities. For more options see my thread in the Tech Community: https://techcommunity.microsoft.com/t5/azure-sentinel/get-entities-for-a-sentinel-incidient-by-api/m-p/1422643

$expandBody = '{ "expansionId": "98b974fd-cc64-48b8-9bd0-3a209f5b944b" }'
$expandURI = "https://management.azure.com" + $workspaceId + "/providers/Microsoft.SecurityInsights/entities/"+$systemAlertId+"/expand?api-version=2020-01-01"
$expandData = Invoke-RestMethod -Uri $expandURI -Method POST -Headers $headers -Body $expandBody

All entities related to the incident can be found in the $expandData variable. Again the full output is shown below:

{
    "value": {
        "entities": [
            {
                "id": "<snip>"
                "name": "<snip>"
                "type": "Microsoft.SecurityInsights/entities",
                "kind": "Account",
                "properties": {
                    "accountName": "<snip>",
                    "upnSuffix": "<snip>",
                    "isDomainJoined": true,
                    "friendlyName": "<snip>"
                }
            },
            {
                "id": "<snip>",
                "name": "<snip>",
                "type": "Microsoft.SecurityInsights/entities",
                "kind": "Ip",
                "properties": {
                    "address": "<snip>",
                    "friendlyName": "<snip>"
                }
            }
        ],
        "edges": []
    },
    "metaData": {
        "aggregations": [
            {
                "entityKind": "Account",
                "count": 1
            },
            {
                "entityKind": "Ip",
                "count": 1
            }
        ]
    }
}

Now that we have all the information in Powershell variables, it is not that hard to write all the information you need another system or, in our case, a logfile to ingest in your SIEM.
We have been using this solution for over half a year now without issues.

It was a fun ride to explore the world of the (not well documented) Sentinel Management API, and to get the feedback from the Microsoft Community. Microsoft is working hard to expand this API, so you can also write incidents (already possible) with entities (not yet possible). I hope they will document the magic GUIDs you need to work with the Expand endpoint and other endpoints of this API that are not documented at this moment.