public/Convert-SentinelARYamlToArm.ps1

<#
.SYNOPSIS
Converts an Azure Sentinel Analytics Rule YAML file to ARM template
 
.DESCRIPTION
Converts an Azure Sentinel Analytics Rule YAML file to ARM template.
The YAML file can be provided as a file or as a string.
The ARM template file can be saved to the same directory as the YAML file.
 
.PARAMETER Filename
The path to the YAML file
 
.PARAMETER Data
The YAML data as a string
 
.PARAMETER OutFile
The path to the output ARM template file
 
.PARAMETER UseOriginalFilename
If set, the output file will be saved with the same name as the YAML file, but with a .json extension
 
.EXAMPLE
Convert-SentinelARYamlToArm -Filename "C:\Temp\MyRule.yaml" -OutFile "C:\Temp\MyRule.json"
 
.NOTES
  Author: Fabian Bader (https://cloudbrothers.info/)
#>


function Convert-SentinelARYamlToArm {
    [CmdletBinding(DefaultParameterSetName = 'Pipeline')]
    param (
        [Parameter(Mandatory = $true,
            ParameterSetName = 'Path')]
        [Parameter(Mandatory = $true,
            ParameterSetName = 'UseOriginalFilename')]
        [string]$Filename,

        [Alias('Json')]
        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ParameterSetName = 'Pipeline',
            Position = 0)]
        [array]$Data,

        [Parameter(Mandatory = $false,
            ParameterSetName = 'Path')]
        [Parameter(Mandatory = $false,
            ParameterSetName = 'Pipeline')]
        [string]$OutFile,

        [Parameter(Mandatory = $true,
            ParameterSetName = 'UseOriginalFilename')]
        [switch]$UseOriginalFilename,

        [ValidatePattern('^\d{4}-\d{2}-\d{2}(-preview)?$')]
        [Parameter(Mandatory = $false)]
        [string]$APIVersion = "2022-11-01"
    )

    begin {
        if ($PsCmdlet.ParameterSetName -in ("Path", "UseOriginalFilename") ) {
            if (-not (Test-Path $Filename) ) {
                throw "File not found"
            }
            if ($UseOriginalFilename) {
                $FileObject = Get-ChildItem $Filename
                $NewFileName = $FileObject.Name -replace $FileObject.Extension, ".json"
                $OutFile = Join-Path $FileObject.Directory $NewFileName
            }
        }
    }

    process {
        # Use pipeline data and create a variable containing all parsed strings
        if ($PsCmdlet.ParameterSetName -eq "Pipeline") {
            $FullYaml += $Data
        }
    }

    end {

        # Use parsed pipeline data if no file was specified (default)
        if ($PsCmdlet.ParameterSetName -eq "Pipeline") {
            $analyticRule = $FullYaml | ConvertFrom-Yaml
        } else {
            Write-Verbose "Read file `"$Filename`""
            $analyticRule = Get-Content $Filename | ConvertFrom-Yaml
        }

        Write-Verbose "Convert analytic rule $($analyticRule.name) ($($analyticRule.id)) to ARM template"

        $Template = @'
{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "workspace": {
            "type": "String"
        }
    },
    "resources": [
        {
            "id": "[concat(resourceId('Microsoft.OperationalInsights/workspaces/providers', parameters('workspace'), 'Microsoft.SecurityInsights'),'/alertRules/<TEMPLATEID>')]",
            "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/<TEMPLATEID>')]",
            "type": "Microsoft.OperationalInsights/workspaces/providers/alertRules",
            "kind": "<RULEKIND>",
            "apiVersion": "<APIVERSION>",
            "properties": <PROPERTIES>
        }
    ]
}
'@


        # Replace API version with specified version
        $Template = $Template.Replace('<APIVERSION>',$APIVersion)

        # Only include the following keys in ARM template
        $DefaultSortOrderInArmTemplate = @(
            "displayName",
            "description",
            "severity",
            "enabled",
            "query",
            "queryFrequency",
            "queryPeriod",
            "triggerOperator",
            "triggerThreshold",
            "suppressionDuration",
            "suppressionEnabled",
            "tactics",
            "techniques",
            "alertRuleTemplateName",
            "incidentConfiguration",
            "eventGroupingSettings",
            "alertDetailsOverride",
            "customDetails",
            "entityMappings",
            "sentinelEntitiesMappings"
        )

        $SkipYamlValues = @(
            "metadata",
            "kind",
            "requiredDataConnectors"
        )

        # Mapping of Arm template names to YAML name when different
        $ValueNameMappingYaml2Arm = [ordered]@{
            "name"               = "displayName"
            "id"                 = "alertRuleTemplateName"
            "version"            = "templateVersion"
            "relevantTechniques" = "techniques"
        }

        $CompareOperatorYaml2Arm = @{
            "eq" = "Equals"
            "gt" = "GreaterThan"
            "ge" = "GreaterThanOrEqual"
            "lt" = "LessThan"
            "le" = "LessThanOrEqual"
        }

        $ARMTemplate = [ordered]@{}
        foreach ($Item in $analyticRule.Keys) {
            # Skip certain values, because they are not needed in the ARM template
            if ( $Item -notin $SkipYamlValues ) {
                # Change the name of the value if needed
                $KeyName = $ValueNameMappingYaml2Arm[$Item]
                # If the name is not in the mapping, use the original name
                if ([string]::IsNullOrWhiteSpace($KeyName)) {
                    $KeyName = $Item
                }

                # Change values of compare operators
                if ( $analyticRule[$Item] -in $CompareOperatorYaml2Arm.Keys ) {
                    $Value = $CompareOperatorYaml2Arm[$analyticRule[$Item]]
                } else {
                    $Value = $analyticRule[$Item]
                }
                # Add value to hashtable
                if ($KeyName -notin $ARMTemplate.keys) {
                    $ARMTemplate.Add($KeyName, $Value)
                }
            }
        }

        # Add required parameters if missing with default values
        $RequiredParameters = @{
            "suppressionDuration" = "PT1H"
            "suppressionEnabled"  = $false
            "enabled"             = $true
            "customDetails"       = $null
            "entityMappings"      = $null
        }
        foreach ( $KeyName in $RequiredParameters.Keys ) {
            if (  $KeyName -notin $ARMTemplate.Keys ) {
                $ARMTemplate.Add($KeyName, $RequiredParameters[$KeyName])
            }
        }

        # Sort by custom order
        $ARMTemplateOrdered = [ordered]@{}
        $ErrorActionPreference = "SilentlyContinue"
        $AnalyticsRuleKeys = $ARMTemplate.Keys | Sort-Object { $i = $DefaultSortOrderInArmTemplate.IndexOf($_) ; if ( $i -eq -1 ) { 100 } else { $i } }
        $ErrorActionPreference = "Continue"
        foreach ($PropertyName in $AnalyticsRuleKeys) {
                $ARMTemplateOrdered.Add($PropertyName, $ARMTemplate.$PropertyName)
        }

        # Convert hashtable to JSON
        $JSON = $ARMTemplateOrdered | ConvertTo-Json -Depth 99
        # Use ISO8601 format for timespan values
        $JSON = $JSON -replace '"([0-9]+)m"', '"PT$1M"' -replace '"([0-9]+)h"', '"PT$1H"' -replace '"([0-9]+)d"', '"P$1D"'

        $ScheduleKind = $analyticRule.kind.substring(0, 1).toupper() + $analyticRule.kind.substring(1).tolower()

        $Result = $Template.Replace("<PROPERTIES>", $JSON)
        $Result = $Result.Replace("<TEMPLATEID>", $analyticRule.id)
        $Result = $Result.Replace("<RULEKIND>", $ScheduleKind)
        if ( $PSVersionTable.PSVersion -ge [version]'7.0.0' ) {
            # Beautify in PowerShell 7 and above
            $Result = $Result | ConvertFrom-Json | ConvertTo-Json -Depth 99
        }

        if ($OutFile -or $UseOriginalFilename) {
            $Result | Out-File $OutFile -Force
            Write-Verbose "Output written to file: `"$OutFile`""
        } else {
            return $Result
        }
    }
}

# SIG # Begin signature block
# MIIRtAYJKoZIhvcNAQcCoIIRpTCCEaECAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUMuT+V/9FsOkJRC8VydfMXyhu
# PXCggg4AMIIGsDCCBJigAwIBAgIQCK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0B
# AQwFADBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD
# VQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVk
# IFJvb3QgRzQwHhcNMjEwNDI5MDAwMDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYD
# VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD
# ZXJ0IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEg
# Q0ExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5
# WRuxiEL1M4zrPYGXcMW7xIUmMJ+kjmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJP
# DqFX/IiZwZHMgQM+TXAkZLON4gh9NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXz
# ENOLsvsI8IrgnQnAZaf6mIBJNYc9URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bq
# HPNlaJGiTUyCEUhSaN4QvRRXXegYE2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTC
# fMjqGzLmysL0p6MDDnSlrzm2q2AS4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaD
# G7dqZy3SvUQakhCBj7A7CdfHmzJawv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urO
# kfW+0/tvk2E0XLyTRSiDNipmKF+wc86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7AD
# K5GyNnm+960IHnWmZcy740hQ83eRGv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4
# R+Z1MI3sMJN2FKZbS110YU0/EpF23r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlN
# Wdt4z4FKPkBHX8mBUHOFECMhWWCKZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0I
# U0F8WD1Hs/q27IwyCQLMbDwMVhECAwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYB
# Af8CAQAwHQYDVR0OBBYEFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaA
# FOzX44LScV1kTN8uZz/nupiuHA9PMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAK
# BggrBgEFBQcDAzB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9v
# Y3NwLmRpZ2ljZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4
# oDagNIYyaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJv
# b3RHNC5jcmwwHAYDVR0gBBUwEzAHBgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcN
# AQEMBQADggIBADojRD2NCHbuj7w6mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcT
# Ep6QRJ9L/Z6jfCbVN7w6XUhtldU/SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WT
# auPrINHVUHmImoqKwba9oUgYftzYgBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9
# ntSZz0rdKOtfJqGVWEjVGv7XJz/9kNF2ht0csGBc8w2o7uCJob054ThO2m67Np37
# 5SFTWsPK6Wrxoj7bQ7gzyE84FJKZ9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0
# HKKlS43Nb3Y3LIU/Gs4m6Ri+kAewQ3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL
# 6TEa/y4ZXDlx4b6cpwoG1iZnt5LmTl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+1
# 6oh7cGvmoLr9Oj9FpsToFpFSi0HASIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8
# M4+uKIw8y4+ICw2/O/TOHnuO77Xry7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrF
# hsP2JjMMB0ug0wcCampAMEhLNKhRILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy
# 1lKQ/a+FSCH5Vzu0nAPthkX0tGFuv2jiJmCG6sivqf6UHedjGzqGVnhOMIIHSDCC
# BTCgAwIBAgIQCoIwkEerNiPKwx+yPazrmjANBgkqhkiG9w0BAQsFADBpMQswCQYD
# VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD
# ZXJ0IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEg
# Q0ExMB4XDTIyMDUxODAwMDAwMFoXDTI1MDUxNzIzNTk1OVowTTELMAkGA1UEBhMC
# REUxEDAOBgNVBAcTB0hhbWJ1cmcxFTATBgNVBAoTDEZhYmlhbiBCYWRlcjEVMBMG
# A1UEAxMMRmFiaWFuIEJhZGVyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC
# AgEAwSPFSbbOIFCY82i///NpwIqHv7GJCDqju+CJg7TAojDV2CDSz72qN2PYjV5a
# nfh/jeJVGtA7BrCeKWkLzVH9P4pW52juEhwRe7fbv7s+PkpThLBdwQXh/JHEXpIv
# 9jLkOGH3YxrxoIS5bdnzKfuyUr8qJ/J+U6a9SgkOkFNM6pGHFGY2TsRA8wMjTdph
# YGTKf585hH4mD7/Gq1db72IQDpooKXYPZobQ+LAuLtF/RgTVH1Ytg/61md28pV35
# QyZujAccoYJjgDWzecx7O7cdYuwAlsPfh6L+YFVOx9LyuaVFQg6w63e1DNYEguIm
# Pl6tWtAMOHmgXxd4a4w/H0tvUkqjOH5K4dU4CWmcISnkdh2sdHNwx8gjfYe3TwpW
# xlFOU1HEae6HANF6tVtIyVhQRwS7J1DNJO1KIOGZDBhKhiPklr17WMnR5eYECOdc
# ackHDT9yZJ3QHkT0GMa3KnZSR56RhObz7NH8llJRSZ/2yzDOPAhiFOrKjZPYYL8R
# 5248ZkxOxbTJWpThW53dKPM6b9NotqiJW5ru4eOVq0yjSMdtPLttQAu6HEtNKI19
# 0Aiv5XPPQYMyI1PHVLY5sV7pm36hIpY5EW23HnJs3024AiF45FN1mxHlUkm7c+CY
# sNAbnyRJlIcUyF121akFNVuGQUwbIQntmQoa/kxd/vpY2pECAwEAAaOCAgYwggIC
# MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBT1CpTC
# fZbDHlbuSkDmmKmFygIOOTAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYB
# BQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0cDovL2NybDMuZGlnaWNlcnQu
# Y29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAy
# MUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy
# dFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3JsMD4G
# A1UdIAQ3MDUwMwYGZ4EMAQQBMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGln
# aWNlcnQuY29tL0NQUzCBlAYIKwYBBQUHAQEEgYcwgYQwJAYIKwYBBQUHMAGGGGh0
# dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBcBggrBgEFBQcwAoZQaHR0cDovL2NhY2Vy
# dHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0
# MDk2U0hBMzg0MjAyMUNBMS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsF
# AAOCAgEACcHIVShggRroVDxi+SDfJOqVM2Z92T25Yv8xyWGMUm14bGEOBgnfHiIU
# JmK9Bpm0k/hnYEpV5Ill8/Rf20l+yvlwTj1m4st2Rr4c84RSGmrW83mkYxMhg5YL
# tLiZdafNCcku9+26dgZ537K7YDhGuIeWg708VchAnDEb8CliqWMYLw6J4vagQ91E
# 5emPpq7FhDs2qNMElnrjWULjQkYRGlDfw22AcpstCrEBkc+18WZl6BD2Ow1D1whM
# V6P1472ZgTco6Pcp8BKhrqooUXq2CDwYXJb/iFNwRnu7Cs78u+dlLu+sXNxsbGuP
# T9Ig+5OvC1FiHMeOa4aS8HZSpTbu4w8cclL9EdXqlgVXFC2PlDir/2W9Vj9s6tiS
# p3hdlH7dIO5FEQh8JLrdPFwKXZ8drgvP26Mf11jCvykM+QQm9jhB/VhAnwiskgUo
# dIkfox0RjJtCQkNT1oXqJVErwBql/IVQUNQCR7Q7fA8U2jU8FBTkYryUQAQaIEqx
# av3c+GqM94Th3C5FvrOu4CU28/HZuTjZZCBP7s2EW//4bRUQSnXB4maszUR+/8R+
# bX++yfH/Ou1HQL5aGo9q2L36oaVFjaM282w1pzFAEUf0jgpUkBeJOFUeFvirYWyq
# ex+oKwy8Vzgs+BKd7FOShLa7wCai1fjfYvpO7GxbpdYJqanNMmAxggMeMIIDGgIB
# ATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8G
# A1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBT
# SEEzODQgMjAyMSBDQTECEAqCMJBHqzYjysMfsj2s65owCQYFKw4DAhoFAKB4MBgG
# CisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcC
# AQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYE
# FKeJm0TG0PT/li//A5PqzkoDc+VmMA0GCSqGSIb3DQEBAQUABIICAK07mqTrjfyA
# EzmZdarpNVtSbPZpf7ubsqwbZUoPNVQkxR1XPM5hK1ap9eCobLd2V4OP+1eSDJHK
# PPvrUmxDUjSaii+BEb37HWd/QHE6HtsrcaTaFtbyfzrhwijhmQ2rqcWfsvvCFV9i
# Mph6oTrBp0IpnVzwLpOgoFA28By9UCq8GRhX1ZGPDVPAyw8FDv3lHhf+fInUFk10
# Xn6y2yBVtx1q0KR96xWpk0deERe1ivLIz2CF4p9lnMfOvx+ki5QGlaO5g8RddDeR
# cftUGW58cY+3Unm14oyGavbxUYZb03RNn1iLX5/oscm7XOlpihvzwfDcnQwvCdr0
# XwYFEWlkGt1KlOlQTVkC5RLmXkC950H5ELaC4Rf4P2fU7C9040geiLWdphHGaTDX
# UE32/Oo/qpCYpogkvAEHtAxyvLcCntK5HCvtPQM2chBFELa7mUY+WRyqv3ZunQas
# S4YLxLG0NUZ1JH2Wd4cCd3oeuCv7HokYvEny3W2g3c7oa77tbr9rug1J4iwQASZh
# uhHY0NyiXys/QJneqeF1flCcdHmK50QKiZGMjR1eQf9Q6spIrMleBZBk2QiuUscZ
# b7vFROMrjj36LjqIucQCy1NhU0XHPb9HRCVak4FAYgljTDuWPg29988AcopaxKM1
# qVM1a6CHvo7JD64Zcm64OlvTuw1DfbY9
# SIG # End signature block