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 Analytics Rule 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 original filename of the ARM template file
The extension will be replaced with .json
 
.PARAMETER UseDisplayNameAsFilename
If set, the output file will be saved with the display name of the Analytics Rule as filename
The extension will be replaced with .json
 
.PARAMETER UseIdAsFilename
If set, the output file will be saved with the id of the Analytics Rule as filename
The extension will be replaced with .json
 
.PARAMETER APIVersion
Set API version of the ARM template. Default is "2024-01-01-preview"
 
.PARAMETER NamePrefix
Set prefix for the name of the ARM template. Default is none
 
.PARAMETER Severity
Overwrite the severity of the provided YAML file with a custom one. Default is emtpy
 
.PARAMETER StartRunningAt
Set the startTimeUtc property of the ARM template. Default is empty
To successfully deploy the ARM template the startTimeUtc property must be set to a future date.
Start time must be between 10 minutes and 30 days from now. This is not validated by the cmdlet.
 
.PARAMETER DisableIncidentCreation
If set, the incidentCreation property of the ARM template will be set to false. Default is to keep the value from the YAML file.
 
.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 = 'StdOut')]
    param (
        [Parameter(Mandatory,
            Position = 0,
            ParameterSetName = 'Path')]
        [Parameter(Mandatory,
            Position = 0,
            ParameterSetName = 'UseOriginalFilename')]
        [Parameter(Mandatory,
            Position = 0,
            ParameterSetName = 'UseDisplayNameAsFilename')]
        [Parameter(Mandatory,
            Position = 0,
            ParameterSetName = 'UseIdAsFilename')]
        [Parameter(Mandatory,
            Position = 0,
            ParameterSetName = 'StdOut')]
        [string]$Filename,

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

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

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

        [Parameter(ParameterSetName = 'UseDisplayNameAsFilename')]
        [switch]$UseDisplayNameAsFilename,

        [Parameter(ParameterSetName = 'UseIdAsFilename')]
        [switch]$UseIdAsFilename,

        [ValidatePattern('^\d{4}-\d{2}-\d{2}(-preview)?$')]
        [Parameter()]
        [string]$APIVersion = "2024-01-01-preview",

        [Parameter()]
        [string]$NamePrefix,

        [ValidateSet("Informational", "Low", "Medium", "High")]
        [Parameter()]
        [string]$Severity,

        [Parameter()]
        [string]$ParameterFile,

        [Parameter()]
        [datetime]$StartRunningAt,

        [Parameter()]
        [switch]$DisableIncidentCreation
    )

    begin {
        if ($PsCmdlet.ParameterSetName -ne "Pipeline" ) {
            try {
                if (-not (Test-Path $Filename)) {
                    Write-Error -Exception
                }
            } catch {
                throw "File not found"
            }
        }

        if ($ParameterFile) {
            try {
                if (-not (Test-Path $ParameterFile)) {
                    Write-Error -Exception
                }
            } catch {
                throw "Parameters file not found"
            }
        }
    }

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

    end {

        try {
            # 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
            }
        } catch {
            throw "Could not convert source file. YAML might be corrupted"
        }

        try {
            if ($ParameterFile) {
                Write-Verbose "Read parameters file `"$ParameterFile`""
                $Parameters = Get-Content $ParameterFile | ConvertFrom-Yaml
            } else {
                Write-Verbose "No parameters file provided"
            }
        } catch {
            throw "Could not convert parameters file. YAML might be corrupted"
        }

        #region Parameter file handling
        if ($Parameters) {
            #region Overwrite values from parameters file
            if ($Parameters.OverwriteProperties) {
                foreach ($Key in $Parameters.OverwriteProperties.Keys) {
                    if ($analyticRule.ContainsKey($Key)) {
                        Write-Verbose "Overwriting property $Key with $($Parameters.OverwriteProperties[$Key])"
                        $analyticRule[$Key] = $Parameters.OverwriteProperties[$Key]
                    } else {
                        Write-Verbose "Add new property $Key with $($Parameters.OverwriteProperties[$Key])"
                        $analyticRule.Add($Key, $Parameters.OverwriteProperties[$Key])
                    }
                }
            } else {
                Write-Verbose "No properties to overwrite in provided parameters file"
            }
            #endregion Overwrite values from parameters file

            #region Prepend KQL query with data from parameters file
            if ($Parameters.PrependQuery) {
                $analyticRule.query = $Parameters.PrependQuery + $analyticRule.query
            } else {
                Write-Verbose "No query to prepend in provided parameters file"
            }
            #endregion Prepend KQL query with data from parameters file

            #region Append KQL query with data from parameters file
            if ($Parameters.AppendQuery) {
                $analyticRule.query = $analyticRule.query + $Parameters.AppendQuery
            } else {
                Write-Verbose "No query to append in provided parameters file"
            }
            #endregion Append KQL query with data from parameters file

            #region Replace variables in KQL query with data from parameters file
            if ($Parameters.ReplaceQueryVariables) {
                foreach ($Key in $Parameters.ReplaceQueryVariables.Keys) {
                    if ($Parameters.ReplaceQueryVariables[$Key].Count -gt 1) {
                        # Join array values with comma and wrap in quotes
                        $ReplaceValue = $Parameters.ReplaceQueryVariables[$Key] -join '","'
                        $ReplaceValue = '"' + $ReplaceValue + '"'
                    } else {
                        # Use single value
                        $ReplaceValue = $Parameters.ReplaceQueryVariables[$Key]
                    }
                    Write-Verbose "Replacing variable %%$Key%% with $($ReplaceValue)"
                    $analyticRule.query = $analyticRule.query -replace "%%$($Key)%%", $ReplaceValue
                }
            } else {
                Write-Verbose "No variables to replace in provided parameters file"
            }
            #endregion Replace variables in KQL query with data from parameters file

            Write-Verbose "$($analyticRule | ConvertTo-Json -Depth 99)"
        }
        #endregion Parameter file handling

        if ( [string]::IsNullOrWhiteSpace($analyticRule.name) -or [string]::IsNullOrWhiteSpace($analyticRule.id) ) {
            throw "Analytics Rule name or id is empty. YAML might be corrupted"
        }

        # Generate new guid if id is not a valid guid
        if ($analyticRule.id -notmatch "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}") {
            Write-Warning "Error reading current Id. Generating new Id."
            $analyticRule.id = (New-Guid).Guid
        }

        # Add prefix to name if specified
        if ($NamePrefix) {
            $analyticRule.name = $NamePrefix + $analyticRule.name
        }

        # Overwrite severity with custom severity
        if (-not [string]::IsNullOrWhiteSpace($Severity) ) {
            $analyticRule.severity = $Severity
        }

        Write-Verbose "Convert Analytics Rule $($analyticRule.name) ($($analyticRule.id)) to ARM template"

        #region Set output filename to defined value if not specified by user
        if ($PsCmdlet.ParameterSetName -in ("UseOriginalFilename", "UseDisplayNameAsFilename", "UseIdAsFilename") ) {
            $FileObject = Get-ChildItem $Filename
            if ($UseOriginalFilename) {
                # Use original filename as new filename
                $NewFileName = $FileObject.Name -replace $FileObject.Extension, ".json"
            }
            if ($UseDisplayNameAsFilename) {
                # Use the display name of the Analytics Rule as filename
                $NewFileName = $analyticRule.name -Replace '[^0-9A-Z]', ' '
                # Convert To CamelCase
                $NewFileName = ((Get-Culture).TextInfo.ToTitleCase($NewFileName) -Replace ' ') + '.json'
            }
            if ($UseIdAsFilename) {
                # Use id as of the Analytics Rule filename
                $NewFileName = $analyticRule.id + '.json'
            }
            $OutFile = Join-Path $FileObject.Directory $NewFileName
        }
        #endregion

        $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)

        $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
            "templateVersion"     = "1.0.0"
        }
        foreach ( $KeyName in $RequiredParameters.Keys ) {
            if (  $KeyName -notin $ARMTemplate.Keys ) {
                $ARMTemplate.Add($KeyName, $RequiredParameters[$KeyName])
            }
        }
        # Minimum API version that supports MITRE sub-techniques
        if (([datetime]::parseexact($APIVersion, 'yyyy-MM-dd-preview', $null)) -ge [datetime]"2023-12-01") {
            $ARMTemplate.subTechniques = @($ARMTemplate.techniques | Where-Object { $_ -match "(T\d{4})\.\d{3}" })
        }

        # Remove any sub-techniques from the techniques array
        if ($ARMTemplate.techniques) {
            $ARMTemplate.techniques = $ARMTemplate.techniques -replace "(T\d{4})\.\d{3}", '$1'
        }

        # Remove any invalid or non-existent techniques from the techniques array
        if ($ARMTemplate.techniques) {
            $ARMTemplate.techniques = $ARMTemplate.techniques | Where-Object { Test-MITRETechnique $_ }
        }

        # Remove duplicate techniques
        if ($ARMTemplate.techniques) {
            $ARMTemplate.techniques = @($ARMTemplate.techniques | Sort-Object -Unique)
        }

        # Remove any invalid or non-existent tactics from the tactics array
        if ($ARMTemplate.tactics) {
            $ARMTemplate.tactics = $ARMTemplate.tactics | Where-Object { Test-MITRETactic $_ }
        }

        # Remove duplicate tactics
        if ($ARMTemplate.tactics) {
            $ARMTemplate.tactics = @($ARMTemplate.tactics | Sort-Object -Unique)
        }

        # Add startRunningAt property if specified
        if ($StartRunningAt -and $analyticRule.kind -eq "Scheduled") {
            # Remove existing startTimeUtc property
            if ("startTimeUtc" -in $ARMTemplate.Keys) {
                $ARMTemplate.Remove("startTimeUtc")
            }
            # Add new startTimeUtc property
            $ARMTemplate.Add("startTimeUtc", $StartRunningAt.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"))
        } elseif ($StartRunningAt) {
            Write-Warning "StartRunningAt parameter is only supported for scheduled rules. Ignoring parameter."
        }

        # Disable incident creation if specified
        if ($DisableIncidentCreation) {
            # Remove existing createIncident property
            if ("createIncident" -in $ARMTemplate.incidentConfiguration.Keys) {
                $ARMTemplate.incidentConfiguration.Remove("createIncident")
            }
            # Check if incidentConfiguration container is present and if not create it
            if (-not $ARMTemplate.incidentConfiguration) {
                $ARMTemplate.Add("incidentConfiguration", [ordered]@{})
            }
            $ARMTemplate.incidentConfiguration.Add("createIncident", $false)
        }

        # Convert hashtable to JSON
        $JSON = $ARMTemplate | 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"'

        if ($analyticRule.kind -eq "Scheduled") {
            $ScheduleKind = "Scheduled"
        } elseif ($analyticRule.kind -eq "Nrt") {
            $ScheduleKind = "NRT"
        } else {
            $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)

        # Sort all property keys in ARM template and convert to JSON string object
        $Result = Invoke-SortJSONObject -object ( $Result | ConvertFrom-Json )
        $Result = $Result | ConvertTo-Json -Depth 99

        if ($OutFile) {
            $Result | Out-File $OutFile -Force
            Write-Verbose "Output written to file: `"$OutFile`""
        } else {
            return $Result
        }
    }
}
# SIG # Begin signature block
# MIIoBgYJKoZIhvcNAQcCoIIn9zCCJ/MCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBaAca9/LiMBh5B
# 0d/PbQq0b1Pb08R2y0FHgzL6qVTkIKCCIQkwggWNMIIEdaADAgECAhAOmxiO+dAt
# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa
# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E
# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy
# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF
# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1
# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB
# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR
# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6
# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB
# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S
# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x
# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB
# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP
# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC
# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc
# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov
# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy
# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW
# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF
# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z
# twGpn1eqXijiuZQwggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqG
# SIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMx
# CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMy
# RGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcg
# Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXH
# JQPE8pE3qZdRodbSg9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMf
# UBMLJnOWbfhXqAJ9/UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w
# 1lbU5ygt69OxtXXnHwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRk
# tFLydkf3YYMZ3V+0VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYb
# qMFkdECnwHLFuk4fsbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUm
# cJgmf6AaRyBD40NjgHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP6
# 5x9abJTyUpURK1h0QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzK
# QtwYSH8UNM/STKvvmz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo
# 80VgvCONWPfcYd6T/jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjB
# Jgj5FBASA31fI7tk42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXche
# MBK9Rp6103a50g5rmQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB
# /wIBADAdBgNVHQ4EFgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU
# 7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoG
# CCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29j
# c3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdp
# Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDig
# NqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9v
# dEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZI
# hvcNAQELBQADggIBAH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd
# 4ksp+3CKDaopafxpwc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiC
# qBa9qVbPFXONASIlzpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl
# /Yy8ZCaHbJK9nXzQcAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeC
# RK6ZJxurJB4mwbfeKuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYT
# gAnEtp/Nh4cku0+jSbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/
# a6fxZsNBzU+2QJshIUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37
# xJV77QpfMzmHQXh6OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmL
# NriT1ObyF5lZynDwN7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0
# YgkPCr2B2RP+v6TR81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJ
# RyvmfxqkhQ/8mJb2VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIG
# sDCCBJigAwIBAgIQCK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQsw
# CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
# ZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQw
# HhcNMjEwNDI5MDAwMDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEX
# MBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0
# ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjAN
# BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zr
# PYGXcMW7xIUmMJ+kjmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHM
# gQM+TXAkZLON4gh9NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8Irg
# nQnAZaf6mIBJNYc9URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyC
# EUhSaN4QvRRXXegYE2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0
# p6MDDnSlrzm2q2AS4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQa
# khCBj7A7CdfHmzJawv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0
# XLyTRSiDNipmKF+wc86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960I
# HnWmZcy740hQ83eRGv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2
# FKZbS110YU0/EpF23r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBH
# X8mBUHOFECMhWWCKZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q2
# 7IwyCQLMbDwMVhECAwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYD
# VR0OBBYEFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1k
# TN8uZz/nupiuHA9PMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcD
# AzB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj
# ZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t
# L0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0
# cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmww
# HAYDVR0gBBUwEzAHBgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIB
# ADojRD2NCHbuj7w6mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6j
# fCbVN7w6XUhtldU/SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmI
# moqKwba9oUgYftzYgBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtf
# JqGVWEjVGv7XJz/9kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrx
# oj7bQ7gzyE84FJKZ9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3
# LIU/Gs4m6Ri+kAewQ3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx
# 4b6cpwoG1iZnt5LmTl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9
# Oj9FpsToFpFSi0HASIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+I
# Cw2/O/TOHnuO77Xry7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug
# 0wcCampAMEhLNKhRILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5
# Vzu0nAPthkX0tGFuv2jiJmCG6sivqf6UHedjGzqGVnhOMIIGwjCCBKqgAwIBAgIQ
# BUSv85SdCDmmv9s/X+VhFjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEX
# MBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0
# ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIzMDcxNDAw
# MDAwMFoXDTM0MTAxMzIzNTk1OVowSDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRp
# Z2lDZXJ0LCBJbmMuMSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMzCC
# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKNTRYcdg45brD5UsyPgz5/X
# 5dLnXaEOCdwvSKOXejsqnGfcYhVYwamTEafNqrJq3RApih5iY2nTWJw1cb86l+uU
# UI8cIOrHmjsvlmbjaedp/lvD1isgHMGXlLSlUIHyz8sHpjBoyoNC2vx/CSSUpIIa
# 2mq62DvKXd4ZGIX7ReoNYWyd/nFexAaaPPDFLnkPG2ZS48jWPl/aQ9OE9dDH9kgt
# XkV1lnX+3RChG4PBuOZSlbVH13gpOWvgeFmX40QrStWVzu8IF+qCZE3/I+PKhu60
# pCFkcOvV5aDaY7Mu6QXuqvYk9R28mxyyt1/f8O52fTGZZUdVnUokL6wrl76f5P17
# cz4y7lI0+9S769SgLDSb495uZBkHNwGRDxy1Uc2qTGaDiGhiu7xBG3gZbeTZD+BY
# QfvYsSzhUa+0rRUGFOpiCBPTaR58ZE2dD9/O0V6MqqtQFcmzyrzXxDtoRKOlO0L9
# c33u3Qr/eTQQfqZcClhMAD6FaXXHg2TWdc2PEnZWpST618RrIbroHzSYLzrqawGw
# 9/sqhux7UjipmAmhcbJsca8+uG+W1eEQE/5hRwqM/vC2x9XH3mwk8L9CgsqgcT2c
# kpMEtGlwJw1Pt7U20clfCKRwo+wK8REuZODLIivK8SgTIUlRfgZm0zu++uuRONhR
# B8qUt+JQofM604qDy0B7AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYD
# VR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgG
# BmeBDAEEAjALBglghkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxq
# II+eyG8wHQYDVR0OBBYEFKW27xPn783QZKHVVqllMaPe1eNJMFoGA1UdHwRTMFEw
# T6BNoEuGSWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRH
# NFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGD
# MIGAMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYB
# BQUHMAKGTGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0
# ZWRHNFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQEL
# BQADggIBAIEa1t6gqbWYF7xwjU+KPGic2CX/yyzkzepdIpLsjCICqbjPgKjZ5+PF
# 7SaCinEvGN1Ott5s1+FgnCvt7T1IjrhrunxdvcJhN2hJd6PrkKoS1yeF844ektrC
# QDifXcigLiV4JZ0qBXqEKZi2V3mP2yZWK7Dzp703DNiYdk9WuVLCtp04qYHnbUFc
# jGnRuSvExnvPnPp44pMadqJpddNQ5EQSviANnqlE0PjlSXcIWiHFtM+YlRpUurm8
# wWkZus8W8oM3NG6wQSbd3lqXTzON1I13fXVFoaVYJmoDRd7ZULVQjK9WvUzF4UbF
# KNOt50MAcN7MmJ4ZiQPq1JE3701S88lgIcRWR+3aEUuMMsOI5ljitts++V+wQtaP
# 4xeR0arAVeOGv6wnLEHQmjNKqDbUuXKWfpd5OEhfysLcPTLfddY2Z1qJ+Panx+VP
# NTwAvb6cKmx5AdzaROY63jg7B145WPR8czFVoIARyxQMfq68/qTreWWqaNYiyjvr
# moI1VygWy2nyMpqy0tg6uLFGhmu6F/3Ed2wVbK6rr3M66ElGt9V/zLY4wNjsHPW2
# obhDLN9OTH0eaHDAdwrUAuBcYLso/zjlUlrWrBciI0707NMX+1Br/wd3H3GXREHJ
# uEbTbDJ8WC9nR2XlG3O2mflrLAZG70Ee8PBf4NvZrZCARK+AEEGKMIIHSDCCBTCg
# AwIBAgIQCoIwkEerNiPKwx+yPazrmjANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQG
# EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0
# IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0Ex
# MB4XDTIyMDUxODAwMDAwMFoXDTI1MDUxNzIzNTk1OVowTTELMAkGA1UEBhMCREUx
# EDAOBgNVBAcTB0hhbWJ1cmcxFTATBgNVBAoTDEZhYmlhbiBCYWRlcjEVMBMGA1UE
# AxMMRmFiaWFuIEJhZGVyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
# wSPFSbbOIFCY82i///NpwIqHv7GJCDqju+CJg7TAojDV2CDSz72qN2PYjV5anfh/
# jeJVGtA7BrCeKWkLzVH9P4pW52juEhwRe7fbv7s+PkpThLBdwQXh/JHEXpIv9jLk
# OGH3YxrxoIS5bdnzKfuyUr8qJ/J+U6a9SgkOkFNM6pGHFGY2TsRA8wMjTdphYGTK
# f585hH4mD7/Gq1db72IQDpooKXYPZobQ+LAuLtF/RgTVH1Ytg/61md28pV35QyZu
# jAccoYJjgDWzecx7O7cdYuwAlsPfh6L+YFVOx9LyuaVFQg6w63e1DNYEguImPl6t
# WtAMOHmgXxd4a4w/H0tvUkqjOH5K4dU4CWmcISnkdh2sdHNwx8gjfYe3TwpWxlFO
# U1HEae6HANF6tVtIyVhQRwS7J1DNJO1KIOGZDBhKhiPklr17WMnR5eYECOdcackH
# DT9yZJ3QHkT0GMa3KnZSR56RhObz7NH8llJRSZ/2yzDOPAhiFOrKjZPYYL8R5248
# ZkxOxbTJWpThW53dKPM6b9NotqiJW5ru4eOVq0yjSMdtPLttQAu6HEtNKI190Aiv
# 5XPPQYMyI1PHVLY5sV7pm36hIpY5EW23HnJs3024AiF45FN1mxHlUkm7c+CYsNAb
# nyRJlIcUyF121akFNVuGQUwbIQntmQoa/kxd/vpY2pECAwEAAaOCAgYwggICMB8G
# A1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBT1CpTCfZbD
# HlbuSkDmmKmFygIOOTAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH
# AwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0cDovL2NybDMuZGlnaWNlcnQuY29t
# L0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNB
# MS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRy
# dXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3JsMD4GA1Ud
# IAQ3MDUwMwYGZ4EMAQQBMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNl
# cnQuY29tL0NQUzCBlAYIKwYBBQUHAQEEgYcwgYQwJAYIKwYBBQUHMAGGGGh0dHA6
# Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBcBggrBgEFBQcwAoZQaHR0cDovL2NhY2VydHMu
# ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2
# U0hBMzg0MjAyMUNBMS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOC
# AgEACcHIVShggRroVDxi+SDfJOqVM2Z92T25Yv8xyWGMUm14bGEOBgnfHiIUJmK9
# Bpm0k/hnYEpV5Ill8/Rf20l+yvlwTj1m4st2Rr4c84RSGmrW83mkYxMhg5YLtLiZ
# dafNCcku9+26dgZ537K7YDhGuIeWg708VchAnDEb8CliqWMYLw6J4vagQ91E5emP
# pq7FhDs2qNMElnrjWULjQkYRGlDfw22AcpstCrEBkc+18WZl6BD2Ow1D1whMV6P1
# 472ZgTco6Pcp8BKhrqooUXq2CDwYXJb/iFNwRnu7Cs78u+dlLu+sXNxsbGuPT9Ig
# +5OvC1FiHMeOa4aS8HZSpTbu4w8cclL9EdXqlgVXFC2PlDir/2W9Vj9s6tiSp3hd
# lH7dIO5FEQh8JLrdPFwKXZ8drgvP26Mf11jCvykM+QQm9jhB/VhAnwiskgUodIkf
# ox0RjJtCQkNT1oXqJVErwBql/IVQUNQCR7Q7fA8U2jU8FBTkYryUQAQaIEqxav3c
# +GqM94Th3C5FvrOu4CU28/HZuTjZZCBP7s2EW//4bRUQSnXB4maszUR+/8R+bX++
# yfH/Ou1HQL5aGo9q2L36oaVFjaM282w1pzFAEUf0jgpUkBeJOFUeFvirYWyqex+o
# Kwy8Vzgs+BKd7FOShLa7wCai1fjfYvpO7GxbpdYJqanNMmAxggZTMIIGTwIBATB9
# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE
# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz
# ODQgMjAyMSBDQTECEAqCMJBHqzYjysMfsj2s65owDQYJYIZIAWUDBAIBBQCggYQw
# GAYKKwYBBAGCNwIBDDEKMAigAoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGC
# NwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQx
# IgQggr0cQzADbqupQ6i//k9PtBiHcBO3QuWNwcyPQaUHTu0wDQYJKoZIhvcNAQEB
# BQAEggIAZzqZGDesx3ah7KO97LECgN8KN3xO4C0QuCntzQuzXoJCXvLXiUYnmu/4
# 6IGNvjrjuIn12r6FO1foWGZzouiwGwTYIika4JuzZhPqM8fJuyalLgcwWFpI6Ykt
# OGiSThprUuRCAvGMW+oOoupcmwLKei3esElgJAeaCwO0bwrEe5jEcXEiIQ3l1RlI
# 3bGX6ZnASWwAHP1mYGuLu1yeVGFpFiEM0ySxCGV4MzEIq5FFY4iBjrx7vzP0xbfc
# S76Y0g9/upJted1tK/ePlXVGnMXqZN44UXv5KEVQwY4VnFJNTY6y0+KRrq97TlNt
# u+5b9ajj7z+E8JSw7UBRu9iGPWZK3yLSTO1BD6VQosy614y4L5XATGJ/f3JD7o2i
# 5JU17GKdRpNqp5ws+H6USf2C08YoVbB9d5x16tXqWAL+rg0avVg3IQz6WccGLEac
# c0oCU0eARFeMpPqg0FtYw5DCDlR02f6G9GApvw6LaJT0+YImmoTpowB5osHfjvGF
# 8g1OY5L6+Q0GL9Mqaaxg4lkr0rKGtWcpsZbUXIfbk5LFgaEJURYrCwicMjls8Ssg
# s2+/DW7E60LTc9AqQ0EBV8he0oeoOiq+aOxrREkocM1Q682ulhN+Lcqi5DQihiW1
# coWLZ3j0Iq3G3QRbQFyOdGlDTiVKgTBuz+xkAbN8ZM02GtVyhR6hggMgMIIDHAYJ
# KoZIhvcNAQkGMYIDDTCCAwkCAQEwdzBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMO
# RGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNB
# NDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBAhAFRK/zlJ0IOaa/2z9f5WEWMA0G
# CWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG
# 9w0BCQUxDxcNMjQwODA2MDgyNjU4WjAvBgkqhkiG9w0BCQQxIgQgpZt41YvX+XxX
# QKXVMcckT6B4HdqbIqqDF2bV4H13zKYwDQYJKoZIhvcNAQEBBQAEggIAABoHWSSM
# w997HuALv/ai5vssc3Tb5iY1MV4vWIKYbiutyYTftThSxDWXdqZgKWuB5jd6GQul
# BFEGkjdrvW0aVYgAcMkwtw+/2jA3WvLKr1NGsditeJwl7HbiCjsVl2YSDM+Z8lX6
# bE/4lmL/curVZbJYnZ5l9+tzSjaz6yIhzsbf0bqjK2UvWWQ2aKmkLDCiBuCY8zq6
# yHPr+Z+HHOz5CxXsPmdFRpsi+/juYs8ALTmGxmGDzJlLtE72k4hapMfRWIwB9q4q
# GlBzbA9PEQfbLAkfDcM+2xzS3W1OnwJvPSNSjCz+vtM+VLA6MiIql17YM+8A8uOm
# PWCi2jqBBhETobFCngcNg/Y6m1TcFVCniiT0c+5EwPiKBVYrmeqcRYuiR0hm6LTB
# efyc1prTS2t7BlfAsS3cz59crhoQ8YeL7sjNp314dX/t/CMCL3RlS+CR0jTNd83D
# 9+pliaofPoz6GdsahJkS6GTU96DYMVZfFCA8Jk/jx705YphUj9y/9HGkN0n54IrB
# uOIDJjkk7WpadJL+2RhP7ATRmIgj2ypMVxzwZoOQ46cERbHsOqiblFnbm0qiPuFJ
# o85Te0yKzT7pfqiO/BIuMaHCanBmdDgOs3muKMhqsLN8XPvd9FMcXnJPKJSfOkXa
# OCrsz3iOuS6fj9CTDb81p9My2/bCWdpyPqU=
# SIG # End signature block