Deploying Azure Sentinel Rules with Terraform

Recently I had to deploy Microsoft Sentinel Analytics rules using Terraform.

Fortunately the code required to deploy a simple Alert Rule is relatively straightforward, and documented. If, however, you wish to deploy more complicated Alert rules, or better yet, to mimic the process of the portal and deploy multiple rules based on existing Gallery templates then the required code is more complex, and not well documented.

(Thanks to my colleague Jamie, who provided invaluable assistance with writing some of the code shown here.)

Not wishing to re-invent the wheel, I sought out blogs of other people who had done the same. Unfortunately, I couldn’t find any. I did, however, find an issue raised with the Terraform AzureRM provider clearly trying, and failing, to do just what I wanted. One of the contributors chipped in with the solution to the issue, so there you are, head on over to that GitHub issue, grab the code and off you go.

If it were as easy as that then this post would be rather pointless.

Let’s step back a moment and look at what the proposed code is doing. Below I’ve setup code to look at a single Analytics rule for easier demonstration.

Firstly we define a variable with an Analytics rule template name. Then use a azurerm_sentinel_alert_rule_template data source to get the template information, using the template name and the log analytics workspace id. Finally we’ll have a look at the output of the data source:

variable SentinelTemplateRule {
    type    = string
    default = "Explicit MFA Deny"
}

data "azurerm_sentinel_alert_rule_template" "analytics_rule_template" {
    log_analytics_workspace_id = data.azurerm_log_analytics_workspace.law.id
    display_name               = var.SentinelTemplateRule
}

output "templateinfo" {
  value    = data.azurerm_sentinel_alert_rule_template.analytics_rule_template
}

My output looks like this (I’ve abbreviated it):

Changes to Outputs: 
  + templateinfo = {
      + display_name               = "Explicit MFA Deny"
      + id                         = "/subscriptions/<SUBID>/resourceGroups/<RGID>/providers/Microsoft.OperationalInsights/workspaces/<LAWID>/providers/Microsoft.SecurityInsights/alertRuleTemplates/"
      + name                       = "a22740ec-fc1e-4c91-8de6-c29c6450ad00"
      + scheduled_template         = [
          + {
              + description       = "User explicitly denies MFA push, indicating that login was not expected and the account's password may be compromised."
              + query             = <<-EOT
                    let aadFunc = (tableName:string)
<SNIP>

We can definitely use this as the input to create a scheduled alert rule. Great. We’ll setup a resource block to create a scheduled alert rule, using the input from the data source output, like this:

resource "azurerm_sentinel_alert_rule_scheduled" "rule" {

  name                       = data.azurerm_sentinel_alert_rule_template.analytics_rule_template.name
  log_analytics_workspace_id = data.azurerm_log_analytics_workspace.law.id
  alert_rule_template_guid   = data.azurerm_sentinel_alert_rule_template.analytics_rule_template.name
  display_name               = data.azurerm_sentinel_alert_rule_template.analytics_rule_template.display_name
  severity                   = data.azurerm_sentinel_alert_rule_template.analytics_rule_template.scheduled_template.0.severity
  query                      = data.azurerm_sentinel_alert_rule_template.analytics_rule_template.scheduled_template.0.query

  description                = data.azurerm_sentinel_alert_rule_template.analytics_rule_template.scheduled_template.0.description
  query_frequency            = data.azurerm_sentinel_alert_rule_template.analytics_rule_template.scheduled_template.0.query_frequency
  query_period               = data.azurerm_sentinel_alert_rule_template.analytics_rule_template.scheduled_template.0.query_period
  tactics                    = data.azurerm_sentinel_alert_rule_template.analytics_rule_template.scheduled_template.0.tactics
  trigger_operator           = data.azurerm_sentinel_alert_rule_template.analytics_rule_template.scheduled_template.0.trigger_operator
  trigger_threshold          = data.azurerm_sentinel_alert_rule_template.analytics_rule_template.scheduled_template.0.trigger_threshold
}

This looks great, until you deploy the rule and then compare it with the template in the console:

Some of the rule attributes are missing. The challenge is that these fields are not exposed by the azurerm_sentinel_alert_rule_template data source. For example, if we try to add a techniques property to the resource, we get the following error, indicating that the property is simply not available in the source data:

So how do we get all the attributes we need from the template data source? AZAPI provider to the rescue. This provider talks directly to the Azure APIs, enabling feature parity with Bicep or ARM templates.

We can use the Alert Rule Templates – Get Rest API. To power this we can’t use the friendly alert rule template name, but have to provide the template Id. We could look those up and put them in variable instead of the name or, since we already have a data source which is getting the template Id from the name, we’ll make use of that.

Let’s add another data block to provides the template ID (Name) to the API and have a look at the output This sits after the existing data block and before the resource block..

data "azapi_resource" "analytics_rule_template_api" {
    type                   = "Microsoft.SecurityInsights/alertRuletemplates@2022-11-01"
    parent_id              = data.azurerm_log_analytics_workspace.law.id
    name                   = data.azurerm_sentinel_alert_rule_template.analytics_rule_template.name
    response_export_values = [ "*" ]
}

The output is json encoded, so let’s update the output configuration.

output "templateinfo" {
  value = jsondecode(data.azapi_resource.analytics_rule_template_api.output)
}

Now we can see additional information like Entity mappings is available to us:

With this new source of data available we’ll have to update the resource block, but the changes aren’t too bad:

resource "azurerm_sentinel_alert_rule_scheduled" "rules" {
    name                        = jsondecode(data.azapi_resource.analytics_rule_template_api.output).properties.displayName
    log_analytics_workspace_id  = data.azurerm_log_analytics_workspace.law.id
    display_name                = jsondecode(data.azapi_resource.analytics_rule_template_api.output).properties.displayName
    query                       = jsondecode(data.azapi_resource.analytics_rule_template_api.output).properties.query
    severity                    = jsondecode(data.azapi_resource.analytics_rule_template_api.output).properties.severity

    alert_rule_template_guid    = data.azapi_resource.analytics_rule_template_api.name
    alert_rule_template_version = jsondecode(data.azapi_resource.analytics_rule_template_api.output).properties.version
    description                 = jsondecode(data.azapi_resource.analytics_rule_template_api.output).properties.description
    enabled                     = true
    tactics                     = jsondecode(data.azapi_resource.analytics_rule_template_api.output).properties.tactics
    query_frequency             = jsondecode(data.azapi_resource.analytics_rule_template_api.output).properties.queryFrequency
    query_period                = jsondecode(data.azapi_resource.analytics_rule_template_api.output).properties.queryPeriod
    techniques                  = jsondecode(data.azapi_resource.analytics_rule_template_api.output).properties.techniques
}

I have been able to add the techniques property successfully, and we can see that with the template compare tool.

The resource configuration is still missing the entities property, so let’s tackle that. As the property has multiple child objects, we’ll use a dynamic block. Add the following below the techniques property in the Terraform resource.

dynamic entity_mapping {
        for_each = jsondecode(data.azapi_resource.analytics_rule_template_api.output).properties.entityMappings
        content {
            entity_type = entity_mapping.value.entityType
            dynamic field_mapping {
                for_each = entity_mapping.value.fieldMappings
                content {
                    identifier  = field_mapping.value.identifier
                    column_name = field_mapping.value.columnName
                }
            }
        }
    }

Now if we compare our deployed rule with the template we see no discrepancies.

This is a lot of effort to deploy a single rule from a template, but the beauty of this is that the addition of some “for_each” loops within the resources means our original variable can become a list of strings, and we can deploy multiple alert rules just as easily as one, as demonstrated in the original GitHub issue I referred to earlier.

Of course there are other gotchas, not all these templates have all the Attributes, so we’ll have to deal with “null” values in our resource block and, potentially, add other attributes, but this blog and code should speed you on your way.

About the author