Create a rule from a Microsoft Sentinel solution’s rule template

UPDATE

After playing around with the code a bit and testing some more, I found out an interesting aspect of the data being returned from the PowerShell call that will make the code much simpler. Instead of accessing “displayName” by using

$results.data[0].properties.template.resources.properties.displayName[0]

You can access it directly using

$results.data[0].properties.template.resources.properties[0].displayName

This will also work with arrays so there is no longer the need to jump through hoops to get rid of the last entry!

Introduction

In a previous post, Getting ALL the Microsoft Sentinel rule templates – Yet Another Security Blog (garybushey.com), I explained how you can use PowerShell to get all the rule templates that a Microsoft Solution created.

In this post, I will tell you how to then use these rule templates to create rules. Spoiler: It isn’t as simple as you would think!

You can get the code from: garybushey/CreateRuleFromSolutionTemplate: How to create a rule from a Microsoft Sentinel solution’s rule template (github.com)

What is the deal with all these arrays

Admit it, you read this in Jerry Seinfeld’s voice. If not, you just went back and did 😉

The reason I have this title, is that the call to get the solution’s rule template returns just about everything as an array. Even a straight text field like “displayName” is an array.

Look at this code below: garybushey/CreateRuleFromSolutionTemplate: How to create a rule from a Microsoft Sentinel solution’s rule template (github.com)

PS C:\Users\GaryB> $results.data[0].properties.template.resources.properties.displayName
SAP - Multiple Logons by User
PS C:\Users\GaryB> $results.data[0].properties.template.resources.properties.displayName.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

As you can see, it is an array. Compare that to what is returned when looking at the Microsoft Sentinel API:

PS C:\Users\GaryB> $results[1].properties.displayName
Heartbeat NRT
PS C:\Users\GaryB> $results[1].properties.displayName.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object

Here it is returned as a string. For some reason, the call to get the solution’s rule template adds a “null” entry as shown below:

PS C:\Users\GaryB> $results.data[0].properties.template.resources.properties.displayName[1] -eq $null
True

This means, that whenever we want to use a simple string, we need to use the zero entry of the array as in

$results.data[0].properties.template.resources.properties.displayName[0]

Otherwise, the call to the Microsoft Sentinel REST API to create a new rule will fail with a type mismatch error.

I must say it took me a while to figure that one out!

OK, so what about entries that should be arrays?

While not as much of a pain, the call to the solution’s rule template still adds a null entry to the arrays!

Let’s look at an example. In the code below, I am looking at the tactics that will get exposed.

PS C:\Users\GaryB> $results.data[0].properties.template.resources.properties.tactics
CredentialAccess
InitialAccess
Collection

Looks like 3 entries, right? If I take the count of that array, it will return 4, with the last entry being null:

PS C:\Users\GaryB> $results.data[0].properties.template.resources.properties.tactics.Count
4
PS C:\Users\GaryB> $results.data[0].properties.template.resources.properties.tactics[4] -eq $null
True

Luckily, PowerShell provides a nice way to get rid of the last entry. In the code below, I get rid of any entry that is null:

 $tactics = $template.tactics | Where-Object { $_ -ne $null }

That works great except when there is only the 2 entries. The correct text and the null. If you get rid of the null, then a string would be returned. Once again PowerShell has a way to get around that as well.

[String[]]$tactics = $template.tactics[0]

The complete code to return the correct array is:

if ($template.tactics.Count -eq 2) {
     [String[]]$tactics = $template.tactics[0]
}
else {
     #Return only those entries that are not null
     $tactics = $template.tactics | Where-Object { $_ -ne $null }
}

I had to do this for both the tactics and technique values.

Is that all?

Sadly, not quite. For some unknown reason, entities are stored differently. There could be a null value or a “.nan” value stored, so we need to get rid of both of those values in the aray

$entityMappings = $template.entityMappings  | Where-Object { $_ -ne $null } | Where-Object { $_ -ne ".nan" }

It is entirely possible that the entity will only have null and “.nan” values stored, so we then need to check to make sure the variable we just created, “$entityMappings” is not null. If it isn’t, we need to see if it was returned as a string and if so, convert into a JSON array. We cannot use the same formatting that we did above (not really sure why) to create an array, so we convert into and out of a JSON to make it into an array. By using “-NoEnumerate” when converting out of JSON, we make sure that it stays as an array, even if there is only one entry in it.

#If the arrary of EntityMappings only contained one entry, it will not be returned as an arry
# so we need to convert it into JSON while forcing it to be an array and then convert it back
# without enumerating the output so that it remains an array
if ($null -ne $entityMappings) {
    if ($entityMappings.GetType().BaseType.Name -ne "Array") {
        $entityMappings = $entityMappings | ConvertTo-Json -Depth 5 -AsArray | ConvertFrom-Json -NoEnumerate
    }
}

That must be it, right? No. One last check. For some entities, they are stored as just empty strings. If you pass in the entity into the Microsoft Sentinel REST API to create a rule as an empty string, it will fail. So, our last check it to see if the variable just contains null or white space and if so, set it to null:

#Some entity mappings are stored as empty strings (not sure why) so we need 
#to check for that and set to null if it is empty so no error gets thrown
if ([String]::IsNullOrWhiteSpace($entityMappings)) {
     $entityMappings = $null
}

That is it! Now we can use these values to create a new rule!

Creating the rule

First, create the “$template” variable to make life easier, this is done at the top of the “foreach”:

foreach ($result in $results.data) {
        $template = $result.properties.template.resources.properties

This will save a lot of typing later!

I do a “switch” based on the rule template’s type since the different types have different settings as shown below:

 "NRT" {
                #For some reason, all the string values are returned as arrays (with null as the second entry)
                #and we only care about the first entry hence the [0] after everything
                $body = @{
                    "kind"       = "NRT"
                    "properties" = @{
                        "enabled"               = "true"
                        "alertRuleTemplateName" = $name
                        "displayName"           = $template.displayName[0]
                        "description"           = $template.description[0]
                        "severity"              = $template.severity[0]
                        "tactics"               = $tactics
                        "techniques"            = $techniques
                        "query"                 = $template.query[0]
                        "suppressionDuration"   = $template.suppressionDuration[0]
                        "suppressionEnabled"    = $template.suppressionEnabled[0]
                        "eventGroupingSettings" = $template.eventGroupingSettings[0]
                        "templateVersion"       = $template.version[0]
                        "entityMappings"        = $entityMappings
                    }
                }
            }
            "Scheduled" {
                #For some reason, all the string values are returned as arrays (with null as the second entry)
                #and we only care about the first entry hence the [0] after everything
                $body = @{
                    "kind"       = "Scheduled"
                    "properties" = @{
                        "enabled"               = "true"
                        "alertRuleTemplateName" = $name
                        "displayName"           = $template.displayName[0]
                        "description"           = $template.description[0]
                        "severity"              = $template.severity[0]
                        "tactics"               = $tactics
                        "techniques"            = $techniques
                        "query"                 = $template.query[0]
                        "queryFrequency"        = $template.queryFrequency[0]
                        "queryPeriod"           = $template.queryPeriod[0]
                        "triggerOperator"       = $template.triggerOperator[0]
                        "triggerThreshold"      = $template.triggerThreshold[0]
                        "suppressionDuration"   = $template.suppressionDuration[0]
                        "suppressionEnabled"    = $false
                        "eventGroupingSettings" = $template.eventGroupingSettings[0]
                        "templateVersion"       = $template.version[0]
                        "entityMappings"        = $entityMappings
                    }
                }
            }

As you can see, I use “[0]” to reference any string that gets returned as an array and I setup other variables before this call. Then it is just a simple matter to make the call to create the actual rule.

Keep in mind, that depending on the rule template, there could be errors thrown if the needed tables don’t exist or some rule templates actually have KQL errors in them. Any error is written to the screen so you can see why it happened.

Summary

This blog post goes over some of the issues I ran into trying to create a new rule from a Microsoft Sentinel solution rule template. I have no idea why there are extra nulls added everywhere and maybe someday they will go away; in which case I will have to modify this code 😉

Hope you find this useful!

Leave a Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.