Link to the ARM template for the full playbook can be found on Github.

Microsoft cloud SIEM, Azure Sentinel, is an amazing product which can provide central logging and reporting for your organization. At The Collective we are heavily using this to improve the security posture of our clients. It’s tightly integrated with all the different Microsoft Cloud products (like Azure AD, Azure ARM, Office 365) and offers a wide range of automation capabilities through playbooks (=> Logic Apps).

Azure Sentinel creates on alert when a certain analytic rule (KQL query) reaches a certain threshold. For example, there is a default rule ‘Failed login attempts to Azure Portal’ which monitors any failed attempts to the Azure Portal. This can be used to identify identity attacks on administrator accounts.

But this rule is a perfect example of a rule that can generate a lot of false positives. What about administrators that are just a second too late to approve an MFA request or fat finger their password? In cases like this, it can be helpful if you filter out the results that originate from corporate IP-addresses.

By default, the corporate IP addresses are not available in Sentinel. Inside Microsoft 365, you have two places to configure your corporate IP-addresses: Conditional Access, Named Locations and Cloud App Security, IP address ranges. The only way to use these in queries is to create a separate playbook which saves the corporate IPs into a custom log table in Azure Sentinel. This is exactly what we are going to do in this blog post. I chose to use the named locations for this, as Conditional Access is a part of the Microsoft Graph.

Getting the named locations from the Graph API is pretty easy.

The following code will filter out trusted named locations and select the IP range and displayName.

https://graph.microsoft.com/beta/conditionalAccess/namedLocations?$select=displayName,microsoft.graph.ipNamedLocation/ipRanges/&$filter=microsoft.graph.ipNamedLocation/isTrusted

As named locations are configured in CIDR-range format, we need to translate these to individual IP-addresses. One location might contain the range ‘5.4.3.1/31’, but we need to be able to save these as individual IP-addresses (5.4.3.1, 5.4.3.0) to Log Analytics in order to use these effectively in hunting queries.

There is no easy way in Logic Apps/Sentinel to translate ranges to IP-addresses, so that’s why I decided to utilize the ‘ip-cidr’ Javascript module. This module will convert any CIDR range you throw at it into an array of IP-addresses.

Because Logic Apps doesn’t support running inline Javascript code which utilizes modules, I used a simple Azure function with an HTTP trigger. This means the function can be called from a Logic App. The full code of the Function is as follows:

const IPCIDR = require("ip-cidr"); module.exports = async function (context, req) { context.log('JavaScript HTTP trigger function processed a request.'); var cidr = new IPCIDR (req.query.cidr); var IpLIst = cidr.toArray(); var JsonIPList = JSON.stringify(IpLIst); var JsonToReturn = JSON context.res = { body: JsonIPList }; context.res.headers = { 'Content-Type': 'application/json'}; context.done(); };

The entire Logic App looks like this?

As you can see there are quite a bit of for each loops. The workflow of the Logic Apps is as follows:

The Logic App runs every 7 days

It queries the Microsoft Graph API for named locations, which will return an array of named locations. One named location can contain multiple CIDR-ranges.

We initialize a variable, which is an array that will contain all our IP-addresses.

First we loop over all the separate named locations.

Then we loop over every CIDR-range in a named location.

We call our Azure Function for every CIDR-range, the function will return a list of IP-addresses. We add each of these IP’s to the array.

At last, we loop over our entire IP array and add these to a custom Log Analytics table.

It takes a while (+/- 10 minutes) before our the table is properly populated.

The following KQL query will return all the corporate IP’s:

sentinel_namedLocations_CL | where TimeGenerated >= ago(7d)

The output of the table will look like this:

Now we can use these IP-addresses in queries to filter out the corporate IP’s.

This sample query will save all corporate IP’s into the ‘ips’ variables and only return the SigninLogs which do not originate from a corporate IP.

let ips = ( sentinel_namedLocations_CL | where TimeGenerated >= ago(7d) | project ip_s); SigninLogs | where IPAddress !in (ips)

You can deploy this Logic app and Azure Function yourself through an ARM template which I have submitted to the Azure Sentinel GitHub page.

Play around with it and let me know what you do with it! I am very interested how this will be used in different Sentinel queries.

FYI: Deploying an Azure Function through an ARM template isn’t as easy as I thought, so this ARM template is also a good example for that :).