Bicep is a domain-specific language that uses declarative syntax to deploy Azure resources. It provides benefits over Azure Resource Management (ARM) templates including smaller file size, integrated parameter files, and better support to tools like Visual Studio code. To learn more about Bicep go to What is Bicep
In order to learn more about what you can do in Microsoft Sentinel in Bicep, you can go to Microsoft Sentinel Bicep resources. Note that this link takes you to "Aggregations" as the top-level entry (which is still incorrectly called "Azure Sentinel") doesn't have a direct link. If you were to select an entry, like "Alert Rules", you will see the format of the Bicep resource definition and at least one example of how to use it. Very handy.
Using this page, and a few others selected from the menu on the left side of the page, and some help from users on the internet including Kevin Hemelrijk and Mark Palmer, I have created the Bicep resource and parameter files. Note that due to some limitations in the Bicep resources, there will be a call to a PowerShell file as well.
Resource Group
While you can create a resource group using a Bicep file, due to the way the file is called, the resource group needs to be present first. You can do this using whatever process you like, including the Azure CLI command listed below:
az group create --location <location> --resource-group <resourceGroup>
For example
az group create --location eastus --resource-group testRG
Bicep File
The first thing that is done in the Bicep file is to setup the parameters that will be read in from the parameter file (more on that later) and some variables. Note that "subscription" and "resourceGroup" are built in variables that refer to the environment into which this Bicep file is being run.
@description('Specifies the name of the client who needs Sentinel.')
param workspaceName string
@description('Specifies the number of days to retain data.')
param retentionInDays int
@description('Which solutions to deploy automatically')
param contentSolutions string[]
var subscriptionId = subscription().id
var location = resourceGroup().location
//Sentinel Contributor role GUID
var roleDefinitionId = 'ab8e14d6-4a74-4a29-9ba8-549422addade'
Notice that the parameters have a type associated with them. This is another check to make sure the correct type of data is being passed in as a parameter. There can also be restrains applied like maximum length or allowed item selections. More on parameters later in this article.
The last parameter is an array that contains the name of the solutions from the content hub that will be deployed. Keep in mind that the name shown in the Azure Portal may not be the correct name to use here as some changes to the name may have taken place. The best way to get the proper name is to make a call to the Microsoft Sentinel "contentProductPackages" REST API. You can get more information about this at Microsoft Sentinel Content Packages. After making the call, you can look at the ".properties.displayName" to get the proper name.
Let's take a look at the Bicep call to create the Log Analytics workspace.
resource workspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
name: workspaceName
location: location
properties: {
retentionInDays: retentionInDays
}
}
- Line 1 starts with "resource" to say that Bicep is deploying a resource as opposed to something like a loop. Then the word "workspace" is called the symbolic name and is used to reference the resource later on. For example, when creating the Microsoft Sentinel instance, one of the properties is "workspaceResourceId" and it can be referenced by passing in "workspace.id"
- Line 2 is the name of the resource. In this case, the parameter called "workspaceName" is being used.
- Line 3 is the location for the workspace. The variable, location, is being passed in here.
- Line 4 starts the properties that are needed. In this case, only the retention time in days is being used. Different resources will have different properties.
Next are the calls to make the workspace, the Microsoft Sentinel instance, and a call to finish the Microsoft Sentinel onboarding:
// Create the Log Analytics Workspace
resource workspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
name: workspaceName
location: location
properties: {
retentionInDays: retentionInDays
}
}
// Create Microsoft Sentinel on the Log Analytics Workspace
resource sentinel 'Microsoft.OperationsManagement/solutions@2015-11-01-preview' = {
name: 'SecurityInsights(${workspaceName})'
location: location
properties: {
workspaceResourceId: workspace.id
}
plan: {
name: 'SecurityInsights(${workspaceName})'
product: 'OMSGallery/SecurityInsights'
promotionCode: ''
publisher: 'Microsoft'
}
}
// Onboard Sentinel after it has been created
resource onboardingStates 'Microsoft.SecurityInsights/onboardingStates@2022-12-01-preview' = {
scope: workspace
name: 'default'
}
After the Microsoft Sentinel instance has been setup, there are some settings that can be applied, including the Entity Behavior directory service, in this case Azure Entra Id (which is still called "AzureActiveDirectory" in the code), and the data sources.
// Enable the Entity Behavior directory service
resource EntityAnalytics 'Microsoft.SecurityInsights/settings@2023-02-01-preview' = {
name: 'EntityAnalytics'
kind: 'EntityAnalytics'
scope: workspace
properties: {
entityProviders: ['AzureActiveDirectory']
}
dependsOn: [
onboardingStates
]
}
// Enable the additional UEBA data sources
resource uebaAnalytics 'Microsoft.SecurityInsights/settings@2023-02-01-preview' = {
name: 'Ueba'
kind: 'Ueba'
scope: workspace
properties: {
dataSources: ['AuditLogs', 'AzureActivity', 'SigninLogs', 'SecurityEvent']
}
dependsOn: [
EntityAnalytics
]
}
Bicep does provide a way to deploy solutions, but it doesn't work correctly. This is due to the underlying REST API not working correctly. Also, as Bicep is used to create resources, there is no way to get a list of solutions to deploy using Bicep, hence the parameter mentioned previously. Luckily, it does provide a way to call PowerShell so that the solutions can be deployed.
Because the PowerShell interacts with Azure, there needs to be a user identity created. This identity will then need the proper permission, Microsoft Sentinel Contributor, on the resource group. There is a 5-minute pause in between these calls to allow the identity time to propagate. Calling a PowerShell script is described more below.
//Create the user identity to interact with Azure
@description('The user identity for the deployment script.')
resource scriptIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: 'script-identity'
location: location
}
//Pausing for 5 minutes to allow the new user identity to propagate
resource pauseScript 'Microsoft.Resources/deploymentScripts@2023-08-01' = {
name: 'pauseScript'
location: resourceGroup().location
kind: 'AzurePowerShell'
properties: {
azPowerShellVersion: '12.2.0'
scriptContent: 'Start-Sleep -Seconds 300'
timeout: 'PT30M'
cleanupPreference: 'OnSuccess'
retentionInterval: 'PT1H'
}
dependsOn: [
scriptIdentity
]
}
//Assign the Sentinel Contributor rights on the Resource Group to the User Identity that was just created
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(resourceGroup().name, roleDefinitionId)
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId)
principalId: scriptIdentity.properties.principalId
}
dependsOn: [
pauseScript
]
}
Finally, a call is made to the PowerShell script, Create-NewSolutionAndRulesFromList.ps1, to deploy the passed in solutions and create the rules from the rule templates. This code was taken from the Microsoft Sentinel All-In-One V2 offering so it won't be discussed much here. The only exception is that because the user identity account is making all the calls to Azure, the "Connect-AzAccount" is configured to pass in an identity
Connect-AzAccount -Identity -AccountId $Identity
Where $Identity is the client identity that was passed in as a parameter.
resource deploymentScript 'Microsoft.Resources/deploymentScripts@2023-08-01' = {
name: 'deploySolutionsScript'
location: resourceGroup().location
kind: 'AzurePowerShell'
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${scriptIdentity.id}': {}
}
}
properties: {
azPowerShellVersion: '12.2.0'
arguments: '-ResourceGroup ${resourceGroup().name} -Workspace ${workspaceName} -Region ${resourceGroup().location} -Solutions ${contentSolutions} -SubscriptionId ${subscriptionId} -TenantId ${subscription().tenantId} -Identity ${scriptIdentity.properties.clientId} '
scriptContent: loadTextContent('./Create-NewSolutionAndRulesFromList.ps1')
timeout: 'PT30M'
cleanupPreference: 'OnSuccess'
retentionInterval: 'P1D'
}
dependsOn: [
roleAssignment
]
}
Some things to note on this call:
- Lines 5-10: Because we are making calls into Azure, this resource call will require an Identity.
- Line 12: This was set to the latest version of PowerShell at the time of this writing. It could use a lower version but there will probably be a message in the logs that there is a newer version.
- Line 13: These are all the arguments being passed into the PowerShell script. They will need to be read in as parameters in the script
- Line 14: This is the location of the script. The code could be included inline, like what was done for the "pauseScript" call made above.
- Line 15: This is how long to allow the script to run. Keep this in mind if a lot of solutions were passed in.
- Line 16: This is the cleanup preference when the script executes. Other options include "Always" and "OnExpiration"
- Line 17: This is how long the script resource will be retained after it finishes running.
- Line 19: The "dependsOn" states that this resource will not start to run until after the other resource, "roleAssignment" in this case. finishes. There are other instances of this throughout the file. Note that if the resource makes use of data from a previous resource, it does not need a "dependsOn. For example, since the "sentinel" resource uses "workspace.id", it does not need a "dependsOn"
Parameter File
Bicep can use two different formats for the parameter file. It can either be a JSON or the Bicep Parameter file (that has a file extension of "bicepparam". The Bicep Parameter file has advantages over the regular JSON file.
The biggest advantage is that it references the Bicep file so editors like Visual Studio Code will know if there are any parameters missing from either the parameter file or the Bicep file.
For the Bicep file that is in this article, this is the parameter file
using './Sentinel.bicep'
param workspaceName = 'demo7'
param retentionInDays = 90
param contentSolutions = [
'Amazon Web Services'
'Microsoft Entra ID'
'Azure Logic Apps'
]
Most of it should be self-explanatory except for the first line. This is a reference to the Bicep file that this file is send its data into. This allows editors like Visual Studio Code to make sure all the parameters are accounted for in both the parameter file and the Bicep file. For more on the Bicep parameter file go to Bicep Parameter Files
Deploying a Bicep file
Using the Azure CLI, the Bicep file can be deployed by using the following call
az deployment group create --name <deploymentName> --template-file <BicepFile> --parameters <BicepParameterFile> --resource-group <resourceGroup>
For example
az deployment group create --name testDeploy --template-file .\sentinel.bicep --parameters .\sentinelParams.bicepparam --resource-group rgTest
Summary
This article shows how to create a Microsoft Sentinel instance, add some settings, and deploy solutions and Analytic Rule templates using Bicep templates. There is far more that can be done including setting Commitment Tiers, configuring some of the Microsoft Data Connectors like Entra ID or Azure Data logs, and setting limitations around the parameters.
You can save these templates as part of your CI/CD for Microsoft Sentinel and, if you have to deploy multiple Microsoft Sentinel instances, reuse the templates with just some minor tweaks to the parameter file.