Public/Configuration/Update-EntraOpsAzureDevOpsSchedules.ps1

<#
.SYNOPSIS
    Updates Azure DevOps pipeline schedules and the service connection from EntraOpsConfig.json.

.DESCRIPTION
    Materializes the Azure DevOps YAML values that cannot be populated from runtime variables. Only
    regions delimited by the EntraOps managed schedule, pipeline trigger and service connection markers
    are changed.
    The service connection name is read from AzureDevOpsServiceConnection (default
    EntraOps-ServiceConnection) and written as the default of the compile-time serviceConnection
    parameter, because Azure Pipelines resolves service connections before runtime variables exist.
    The push and reporting pipelines run after entraops-pull only when it ran on BranchName, so a pull
    run of another branch can't trigger them with the service connection.

.PARAMETER ConfigFile
    Location of the EntraOps configuration file. Defaults to ./EntraOpsConfig.json.

.PARAMETER PipelineFolder
    Folder containing the EntraOps Azure DevOps pipeline YAML files. Defaults to
    ./.azure-pipelines.

.PARAMETER BranchName
    Branch included by generated Azure DevOps schedules and by the entraops-pull completion trigger of the
    push and reporting pipelines. Defaults to main; Update-EntraOps passes the branch of the current
    pipeline definitions.

.EXAMPLE
    Update-EntraOpsAzureDevOpsSchedules -ConfigFile ./EntraOpsConfig.json -BranchName main
#>


function Update-EntraOpsAzureDevOpsSchedules {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })]
        [string]$ConfigFile = './EntraOpsConfig.json',

        [Parameter(Mandatory = $false)]
        [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
        [string]$PipelineFolder = './.azure-pipelines',

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]$BranchName = 'main'
    )

    $Config = Get-Content -LiteralPath $ConfigFile -Raw | ConvertFrom-Json -Depth 20
    # Validated before any pipeline is changed
    $ServiceConnection = [string]$Config.AzureDevOpsServiceConnection
    if ([string]::IsNullOrWhiteSpace($ServiceConnection)) { $ServiceConnection = 'EntraOps-ServiceConnection' }
    if ($ServiceConnection -match '[\r\n]') {
        throw 'AzureDevOpsServiceConnection must be a single-line service connection name.'
    }
    if ($BranchName -match '[\r\n]') {
        throw 'BranchName must be a single-line branch name.'
    }
    $ScheduleRegionPattern = '(?ms)^# BEGIN EntraOps managed schedules\r?\n.*?^# END EntraOps managed schedules\s*$'

    function Assert-EntraOpsCronExpression {
        param (
            [Parameter(Mandatory = $true)]
            [string]$Name,

            [Parameter(Mandatory = $true)]
            [string]$Value
        )

        if ($Value -match '[\r\n]' -or @($Value -split '\s+' | Where-Object { $_ }).Count -ne 5) {
            throw "$Name must be a single-line, five-field cron expression."
        }
    }

    function ConvertTo-EntraOpsYamlScalar {
        param ([Parameter(Mandatory = $true)][string]$Value)

        return "'$($Value.Replace("'", "''"))'"
    }

    function Set-EntraOpsPipelineSchedules {
        param (
            [Parameter(Mandatory = $true)]
            [string]$PipelineName,

            [Parameter(Mandatory = $true)]
            [AllowEmptyCollection()]
            [object[]]$Schedules
        )

        $PipelinePath = Join-Path -Path $PipelineFolder -ChildPath $PipelineName
        if (-not (Test-Path -LiteralPath $PipelinePath -PathType Leaf)) {
            throw "Azure DevOps pipeline not found: $PipelinePath"
        }

        $PipelineContent = Get-Content -LiteralPath $PipelinePath -Raw
        if ($PipelineContent -notmatch $ScheduleRegionPattern) {
            throw "Azure DevOps pipeline '$PipelinePath' does not contain the managed schedule markers."
        }

        $ScheduleLines = [System.Collections.Generic.List[string]]::new()
        $ScheduleLines.Add('# BEGIN EntraOps managed schedules')
        if ($Schedules.Count -gt 0) {
            $ScheduleLines.Add('schedules:')
            foreach ($Schedule in $Schedules) {
                Assert-EntraOpsCronExpression -Name $Schedule.Name -Value $Schedule.Cron
                $ScheduleLines.Add("- cron: $(ConvertTo-EntraOpsYamlScalar -Value $Schedule.Cron)")
                $ScheduleLines.Add(" displayName: $(ConvertTo-EntraOpsYamlScalar -Value $Schedule.DisplayName)")
                $ScheduleLines.Add(' branches:')
                $ScheduleLines.Add(' include:')
                $ScheduleLines.Add(" - $(ConvertTo-EntraOpsYamlScalar -Value $BranchName)")
                $ScheduleLines.Add(' always: true')
            }
        }
        $ScheduleLines.Add('# END EntraOps managed schedules')

        $UpdatedContent = [regex]::Replace($PipelineContent, $ScheduleRegionPattern, ($ScheduleLines -join [Environment]::NewLine), 1)
        if ($UpdatedContent -cne $PipelineContent) {
            Set-Content -LiteralPath $PipelinePath -Value $UpdatedContent -Encoding UTF8 -NoNewline
            Write-Host "Updated schedules in $PipelineName."
        } else {
            Write-Host "Schedules in $PipelineName are already current."
        }
    }

    $PullSchedules = @()
    if ($Config.WorkflowTrigger.PullScheduledTrigger -eq $true) {
        $PullSchedules = @([pscustomobject]@{
                Name        = 'WorkflowTrigger.PullScheduledCron'
                Cron        = [string]$Config.WorkflowTrigger.PullScheduledCron
                DisplayName = 'EntraOps privileged access pull'
            })
    }

    $ReportingSchedules = @()
    if ($Config.WorkflowTrigger.PushReportingScheduledTrigger -eq $true) {
        $ReportingSchedules = @([pscustomobject]@{
                Name        = 'WorkflowTrigger.PushReportingScheduledCron'
                Cron        = [string]$Config.WorkflowTrigger.PushReportingScheduledCron
                DisplayName = 'EntraOps reporting generation'
            })
    }

    $UpdateSchedules = @()
    if ($Config.AutomatedEntraOpsUpdate.UpdateScheduledTrigger -eq $true) {
        $UpdateSchedules = @([pscustomobject]@{
                Name        = 'AutomatedEntraOpsUpdate.UpdateScheduledCron'
                Cron        = [string]$Config.AutomatedEntraOpsUpdate.UpdateScheduledCron
                DisplayName = 'EntraOps update'
            })
    }

    $TenantGovernanceSchedules = @()
    if ($Config.TenantGovernanceSnapshot.EnableTenantGovernanceSnapshot -eq $true -and
        $Config.TenantGovernanceSnapshot.SnapshotScheduledTrigger -eq $true) {
        $TenantGovernanceSchedules = @(
            [pscustomobject]@{ Name = 'TenantGovernanceSnapshot.SnapshotScheduledCron'; Cron = [string]$Config.TenantGovernanceSnapshot.SnapshotScheduledCron; DisplayName = 'EntraOps Tenant Governance start' }
            [pscustomobject]@{ Name = 'TenantGovernanceSnapshot.SnapshotScheduledCronComplete'; Cron = [string]$Config.TenantGovernanceSnapshot.SnapshotScheduledCronComplete; DisplayName = 'EntraOps Tenant Governance collect' }
            [pscustomobject]@{ Name = 'TenantGovernanceSnapshot.SnapshotScheduledCronCompleteRetry1'; Cron = [string]$Config.TenantGovernanceSnapshot.SnapshotScheduledCronCompleteRetry1; DisplayName = 'EntraOps Tenant Governance collect retry 1' }
            [pscustomobject]@{ Name = 'TenantGovernanceSnapshot.SnapshotScheduledCronCompleteRetry2'; Cron = [string]$Config.TenantGovernanceSnapshot.SnapshotScheduledCronCompleteRetry2; DisplayName = 'EntraOps Tenant Governance collect retry 2' }
        )
    }

    Set-EntraOpsPipelineSchedules -PipelineName 'entraops-pull.yml' -Schedules $PullSchedules
    Set-EntraOpsPipelineSchedules -PipelineName 'entraops-push-reporting.yml' -Schedules $ReportingSchedules
    Set-EntraOpsPipelineSchedules -PipelineName 'entraops-update.yml' -Schedules $UpdateSchedules
    Set-EntraOpsPipelineSchedules -PipelineName 'entraops-pull-tenant-governance.yml' -Schedules $TenantGovernanceSchedules

    $ServiceConnectionRegionPattern = '(?m)^(?<indent>[ \t]*)# BEGIN EntraOps managed service connection\r?\n(?:.*\r?\n)*?\k<indent># END EntraOps managed service connection[ \t]*(?=\r?$)'
    foreach ($PipelineName in @('entraops-pull.yml', 'entraops-push.yml', 'entraops-push-reporting.yml', 'entraops-pull-tenant-governance.yml')) {
        $PipelinePath = Join-Path -Path $PipelineFolder -ChildPath $PipelineName
        if (-not (Test-Path -LiteralPath $PipelinePath -PathType Leaf)) {
            throw "Azure DevOps pipeline not found: $PipelinePath"
        }
        $PipelineContent = Get-Content -LiteralPath $PipelinePath -Raw
        $RegionMatch = [regex]::Match($PipelineContent, $ServiceConnectionRegionPattern)
        if (-not $RegionMatch.Success) {
            Write-Warning "Azure DevOps pipeline '$PipelinePath' does not contain the managed service connection markers; update the pipeline definitions with Update-EntraOps to set the service connection from the configuration."
            continue
        }
        $Indent = $RegionMatch.Groups['indent'].Value
        $RegionLines = @(
            "$Indent# BEGIN EntraOps managed service connection"
            "$Indent- name: serviceConnection"
            "$Indent displayName: Azure Resource Manager service connection"
            "$Indent type: string"
            "$Indent default: $(ConvertTo-EntraOpsYamlScalar -Value $ServiceConnection)"
            "$Indent# END EntraOps managed service connection"
        )
        $NewLine = if ($RegionMatch.Value -match "`r`n") { "`r`n" } else { "`n" }
        $UpdatedContent = $PipelineContent.Remove($RegionMatch.Index, $RegionMatch.Length).Insert($RegionMatch.Index, ($RegionLines -join $NewLine))
        if ($UpdatedContent -cne $PipelineContent) {
            Set-Content -LiteralPath $PipelinePath -Value $UpdatedContent -Encoding UTF8 -NoNewline
            Write-Host "Updated the service connection in $PipelineName."
        } else {
            Write-Host "Service connection in $PipelineName is already current."
        }
    }

    # The entraops-pull completion trigger of push and reporting is limited to the pipeline branch
    $TriggerRegionPattern = '(?m)^(?<indent>[ \t]*)# BEGIN EntraOps managed pipeline trigger\r?\n(?:.*\r?\n)*?\k<indent># END EntraOps managed pipeline trigger[ \t]*(?=\r?$)'
    foreach ($PipelineName in @('entraops-push.yml', 'entraops-push-reporting.yml')) {
        $PipelinePath = Join-Path -Path $PipelineFolder -ChildPath $PipelineName
        if (-not (Test-Path -LiteralPath $PipelinePath -PathType Leaf)) {
            throw "Azure DevOps pipeline not found: $PipelinePath"
        }
        $PipelineContent = Get-Content -LiteralPath $PipelinePath -Raw
        $RegionMatch = [regex]::Match($PipelineContent, $TriggerRegionPattern)
        if (-not $RegionMatch.Success) {
            Write-Warning "Azure DevOps pipeline '$PipelinePath' does not contain the managed pipeline trigger markers, so it runs after entraops-pull of any branch; update the pipeline definitions with Update-EntraOps to limit the trigger to '$BranchName'."
            continue
        }
        $Indent = $RegionMatch.Groups['indent'].Value
        $RegionLines = @(
            "$Indent# BEGIN EntraOps managed pipeline trigger"
            "${Indent}trigger:"
            "$Indent branches:"
            "$Indent include:"
            "$Indent - $(ConvertTo-EntraOpsYamlScalar -Value $BranchName)"
            "$Indent# END EntraOps managed pipeline trigger"
        )
        $NewLine = if ($RegionMatch.Value -match "`r`n") { "`r`n" } else { "`n" }
        $UpdatedContent = $PipelineContent.Remove($RegionMatch.Index, $RegionMatch.Length).Insert($RegionMatch.Index, ($RegionLines -join $NewLine))
        if ($UpdatedContent -cne $PipelineContent) {
            Set-Content -LiteralPath $PipelinePath -Value $UpdatedContent -Encoding UTF8 -NoNewline
            Write-Host "Updated the pipeline trigger branch in $PipelineName."
        } else {
            Write-Host "Pipeline trigger branch in $PipelineName is already current."
        }
    }
}

# SIG # Begin signature block
# MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB0TTjRf7jLCLkR
# RSCI1MsiALnXq7qicnskc8R0B8clnaCCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j
# AEkoya49fu0nMA0GCSqGSIb3DQEBDAUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK
# ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2Vy
# dGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5l
# dHdvcmsgQ0EwHhcNMjEwNTMxMDY0MzA2WhcNMjkwOTE3MDY0MzA2WjCBgDELMAkG
# A1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAl
# BgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMb
# Q2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMIICIjANBgkqhkiG9w0BAQEFAAOC
# Ag8AMIICCgKCAgEAvfl4+ObVgAxknYYblmRnPyI6HnUBfe/7XGeMycxca6mR5rlC
# 5SBLm9qbe7mZXdmbgEvXhEArJ9PoujC7Pgkap0mV7ytAJMKXx6fumyXvqAoAl4Va
# qp3cKcniNQfrcE1K1sGzVrihQTib0fsxf4/gX+GxPw+OFklg1waNGPmqJhCrKtPQ
# 0WeNG0a+RzDVLnLRxWPa52N5RH5LYySJhi40PylMUosqp8DikSiJucBb+R3Z5yet
# /5oCl8HGUJKbAiy9qbk0WQq/hEr/3/6zn+vZnuCYI+yma3cWKtvMrTscpIfcRnNe
# GWJoRVfkkIJCu0LW8GHgwaM9ZqNd9BjuiMmNF0UpmTJ1AjHuKSbIawLmtWJFfzcV
# WiNoidQ+3k4nsPBADLxNF8tNorMe0AZa3faTz1d1mfX6hhpneLO/lv403L3nUlbl
# s+V1e9dBkQXcXWnjlQ1DufyDljmVe2yAWk8TcsbXfSl6RLpSpCrVQUYJIP4ioLZb
# MI28iQzV13D4h1L92u+sUS4Hs07+0AnacO+Y+lbmbdu1V0vc5SwlFcieLnhO+Nqc
# noYsylfzGuXIkosagpZ6w7xQEmnYDlpGizrrJvojybawgb5CAKT41v4wLsfSRvbl
# jnX98sy50IdbzAYQYLuDNbdeZ95H7JlI8aShFf6tjGKOOVVPORa5sWOd/7cCAwEA
# AaOCAT4wggE6MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLahVDkCw6A/joq8
# +tT4HKbROg79MB8GA1UdIwQYMBaAFAh2zcsH/yT2xc3tu5C84oQ3RnX3MA4GA1Ud
# DwEB/wQEAwIBBjAvBgNVHR8EKDAmMCSgIqAghh5odHRwOi8vY3JsLmNlcnR1bS5w
# bC9jdG5jYS5jcmwwawYIKwYBBQUHAQEEXzBdMCgGCCsGAQUFBzABhhxodHRwOi8v
# c3ViY2Eub2NzcC1jZXJ0dW0uY29tMDEGCCsGAQUFBzAChiVodHRwOi8vcmVwb3Np
# dG9yeS5jZXJ0dW0ucGwvY3RuY2EuY2VyMDkGA1UdIAQyMDAwLgYEVR0gADAmMCQG
# CCsGAQUFBwIBFhhodHRwOi8vd3d3LmNlcnR1bS5wbC9DUFMwDQYJKoZIhvcNAQEM
# BQADggEBAFHCoVgWIhCL/IYx1MIy01z4S6Ivaj5N+KsIHu3V6PrnCA3st8YeDrJ1
# BXqxC/rXdGoABh+kzqrya33YEcARCNQOTWHFOqj6seHjmOriY/1B9ZN9DbxdkjuR
# mmW60F9MvkyNaAMQFtXx0ASKhTP5N+dbLiZpQjy6zbzUeulNndrnQ/tjUoCFBMQl
# lVXwfqefAcVbKPjgzoZwpic7Ofs4LphTZSJ1Ldf23SIikZbr3WjtP6MZl9M7JYjs
# NhI9qX7OAo0FmpKnJ25FspxihjcNpDOO16hO0EoXQ0zF8ads0h5YbBRRfopUofbv
# n3l6XYGaFpAP4bvxSgD5+d2+7arszgowggZrMIIEU6ADAgECAhBX5gfP8LabvnxC
# XoItEBa/MA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhB
# c3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBDb2RlIFNp
# Z25pbmcgMjAyMSBDQTAeFw0yNjEwMDYwODI4MjNaFw0yNzEwMDYwODI4MjJaMIGQ
# MQswCQYDVQQGEwJERTEdMBsGA1UECAwUUmhpbmVsYW5kLVBhbGF0aW5hdGUxEjAQ
# BgNVBAcMCUxhaG5zdGVpbjEeMBwGA1UECgwVT3BlbiBTb3VyY2UgRGV2ZWxvcGVy
# MS4wLAYDVQQDDCVPcGVuIFNvdXJjZSBEZXZlbG9wZXIgVGhvbWFzIE5hdW5oZWlt
# MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAphp2ljSmVsRB6gvkEq7A
# DUMENpKZiDFoAfF1ktD1nCwaVyI+bJ0ROjHwAn4iJgS+vlBDPOUIw2GLHkiqUhOg
# HSwzg6gj45GNmt6zxsKP0zPlhMB/fb0zmz7/F7e3yfKCXLfhbHuk0Es9gJ+8Wmet
# 9StVwwSE/K0MpW06GKA9nPWyE4N/ADwigDIj9rsTKJR4cve9w8G1b3zohpUc/qMr
# onab+HVw5r9b3sOBnnaXC6zvrX4fovRz1ztwfWjRjILLoFG6mW/TuK5F2ICD3Rhl
# 9R2rJWWWhzTDMsQlzvX+/SaM74q3OFpIVNdhvtmWU9pKUBRAyYMijcDqKEFGSzrc
# 8JFmGMpP5TuroWF+N21d3px2eARvW1mV0DCfFAH+Og20SH/gVW6/ltQQEkH/z/Zt
# 49ARyJrgESLOQtlMCIYSoKISdkk4ml4VUS1E/PcbzXEYj3a7m6DOxcd3WuY03UT9
# cnk3p0LMN6LeLYAZgVY+HRloZlH+kW2SkOHNuWgtXvZhAgMBAAGjggF4MIIBdDAM
# BgNVHRMBAf8EAjAAMD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly9jY3NjYTIwMjEu
# Y3JsLmNlcnR1bS5wbC9jY3NjYTIwMjEuY3JsMHMGCCsGAQUFBwEBBGcwZTAsBggr
# BgEFBQcwAYYgaHR0cDovL2Njc2NhMjAyMS5vY3NwLWNlcnR1bS5jb20wNQYIKwYB
# BQUHMAKGKWh0dHA6Ly9yZXBvc2l0b3J5LmNlcnR1bS5wbC9jY3NjYTIwMjEuY2Vy
# MB8GA1UdIwQYMBaAFN10XUwA23ufoHTKsW73PMAywHDNMB0GA1UdDgQWBBTBvGyH
# MXj1dMbVnxuof9oP4PY7cDBLBgNVHSAERDBCMAgGBmeBDAEEATA2BgsqhGgBhvZ3
# AgUBBDAnMCUGCCsGAQUFBwIBFhlodHRwczovL3d3dy5jZXJ0dW0ucGwvQ1BTMBMG
# A1UdJQQMMAoGCCsGAQUFBwMDMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsF
# AAOCAgEAkC165c7/r6y3VeEEpNXas9bQs6ZpKPdJffHkf9xoPi4kWvAIWGc4ocG1
# DJLb18RXiDYShDmT5sb/6I08FeipKi2DNJkuOq6yOTJ3S7+hNFIZCyd6i7WKO8ea
# P5I+FAbyJP4+5coZoMg45bUa9JoocaJCm+5n7WiCfN7/EQpClW5Trm4+ZMbp7EMN
# F1uLJgGCMnN0SsoUarp7PHlqupMxMC4Uhhe+lAP2ytcSIU81L97/qjrADbLWqICk
# 9il5iGM+aZndguq/cmfH9rJj4GRZHWnG6WwnzYD9+KgYlRI6em+/9dq+IRiXe2Bs
# thUuIaFW7U2gWw8tBTEYm1UBLHdJx+L1gjJCLs5KT/wy5wU32wZxNDqPUQejUC1x
# GvU3cKqmLgRq+yNf3gFVfzqIoggyM3U9wQvnk7nBtCkpjXS2hdx4yxzcmrlWsDHr
# z6+6/jeyQySzbYUlFmKnc4n9Mw2soHOajEua5poFARVRS5KgUBuiiQ21YFNYzWBo
# qGuECaaAI/C5rGxapMJ3dAOBnHw6NLqBet+ZXBJEccX5y89MLlOQu+jDmJP8HieR
# qDLlwfhVZnw4RY8Rus43NZBwhNlVuOUXqocHKmJhKNlekda3qsJj93eQBQOxKJcc
# w5O08q9jukpH2+yzFAj+7ll33DuNUgWptogc4Awtbx3glzcEvJQwggaCMIIEaqAD
# AgECAhAo8HfBHDa9/l90MkdwJy4DMA0GCSqGSIb3DQEBDAUAMFYxCzAJBgNVBAYT
# AlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMT
# G0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQTAeFw0yNjAzMTEwNzM0NTRaFw0z
# NjAyMjcwNzM0NTRaMFAxCzAJBgNVBAYTAlBMMSEwHwYDVQQKDBhBc3NlY28gRGF0
# YSBTeXN0ZW1zIFMuQS4xHjAcBgNVBAMMFUNlcnR1bSBUaW1lc3RhbXAgMjAyNjCC
# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALi9zfBw+xI12hi2F9uNQBNO
# STkTIi13fuh3DiWybI5392bBfJw6tO7zDs7UqP/rejVf4xFTU3XNOPsuOFkP5SPF
# snWwn3VJL+NhNhsuujvYsQtaQnvNo1wk46reTgws1L8OFEdKaxgyqT4zOQx81mLw
# NZSqXNU9Rrb9oAJWJF4Mydz+C1ebu0D0+vgM3tqAQ8TU203fqgKCPqIdHloaFqH9
# eyR+8tnpmy2hnnN59ZOtmsuHeRwf5iTOhlhf0d/qqquLOSIGPgGVL1Hj5XBUox2O
# E7tQRr+l/xxNbL6OuwBO4Aa2EbuxYg37cpXv5i2/lTL4NASB/duf9W4fjA3Ro+GP
# +bC54n2OYiM3Cq8z92ejx8F5HWO2e4/75eC95B9tMw/eq7109GDyW2+2ut5Bn0o8
# MXJTERkyystWbqcnUkomA97qfiDCwvpWuOMdfTOksXe9S+xDxOJaCbiyTivgPulx
# cT+tfQTRXBr5UwtPvc+INnafOEilzVHA11tv+pHcylryxBc08cUihf/ttqkZS0bm
# x3VxQay8uohaTp0//ECA9vYGW51SKdJMJhXvKyIdBLy8WYNchTt8yRITv9Adxg9J
# qGujh+TnP1bW3aQjFtlEl7SzgNeI8OVA/CJAWWUuRT3Au9ToOpHy/flrrknhN4he
# Ew7/6ILMv4HSsj0YZ4otAgMBAAGjggFQMIIBTDB1BggrBgEFBQcBAQRpMGcwOwYI
# KwYBBQUHMAKGL2h0dHA6Ly9zdWJjYS5yZXBvc2l0b3J5LmNlcnR1bS5wbC9jdHNj
# YTIwMjEuY2VyMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0dW0u
# Y29tMB8GA1UdIwQYMBaAFL5UAi+/QGxzQ86sCSVOnkNEGu7gMAwGA1UdEwEB/wQC
# MAAwOQYDVR0fBDIwMDAuoCygKoYoaHR0cDovL3N1YmNhLmNybC5jZXJ0dW0ucGwv
# Y3RzY2EyMDIxLmNybDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAOBgNVHQ8BAf8E
# BAMCB4AwIgYDVR0gBBswGTAIBgZngQwBBAIwDQYLKoRoAYb2dwIFAQswHQYDVR0O
# BBYEFCM5aiirmhKnpKfyH/lFmr+N/oIDMA0GCSqGSIb3DQEBDAUAA4ICAQBm/pOb
# rAveTqYFPCSthxNVwBHTW4TfuBQY2KGCdMlH6ALjVPYf2XARNGMTXScR0JQ2/c2L
# xLpoHHhRwxFf3d933DhFS165rYj6iPSQKm41rc9AMpTCjMdmai1aK9o870pOzTzL
# RhWFVIxm+5Qf+t4V5z14tPoUsAWdfRKD1Zzvzfe5AHlE9rxOZcGROiULxs3Edf7Z
# J8RZI0jzD9juj2oEX2XS7BVKuNCK1W0h1mjapv028yqo8UtZh5/1Ib48Yyp88BDY
# Shh5PSGUUuS5tYXVfcduIPBe3PdNTYRYdT5VNxkwLgjMzIcr/6SH2fejFJDjnCxj
# vXLuarErJg/IdI+hXdGIBDVOGv1zrkF4ktoQvFZIp5DIAYh0GwyUhc87qqn5UaYT
# j2+iyiBDSNh7aGCrkuAnqg7Qh7frhX+QhMGeJ9SpLpnG76qN85hLyjvYR6gRYiLT
# j8G4fxjqV400/buPw0rBo9yJZyq/8cGdCOLPOYf9qpefTcrZKeO9pg+UJHx8UABb
# aTqizRxm8sUSEycccCCFhIit3AI/3wGuSOADeiA676NcQ7+dsP04YvzvdN7pTZNy
# g672WpBDJdg0eItHl4yomvNeIOWh1qs4QqOW6TI8Ocsi4uSDTLorxLUDnBTwKQzz
# pxd1YUctEgRh32R0J9FkFkeEruJU94RljFr1uDCCBrkwggShoAMCAQICEQCZo4AK
# JlU7ZavcboSms+o5MA0GCSqGSIb3DQEBDAUAMIGAMQswCQYDVQQGEwJQTDEiMCAG
# A1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVt
# IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3Rl
# ZCBOZXR3b3JrIENBIDIwHhcNMjEwNTE5MDUzMjE4WhcNMzYwNTE4MDUzMjE4WjBW
# MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu
# MSQwIgYDVQQDExtDZXJ0dW0gQ29kZSBTaWduaW5nIDIwMjEgQ0EwggIiMA0GCSqG
# SIb3DQEBAQUAA4ICDwAwggIKAoICAQCdI88EMCM7wUYs5zNzPmNdenW6vlxNur3r
# Lfi+5OZ+U3iZIB+AspO+CC/bj+taJUbMbFP1gQBJUzDUCPx7BNLgid1TyztVLn52
# NKgxxu8gpyTr6EjWyGzKU/gnIu+bHAse1LCitX3CaOE13rbuHbtrxF2tPU8f253Q
# gX6eO8yTbGps1Mg+yda3DcTsOYOhSYNCJiL+5wnjZ9weoGRtvFgMHtJg6i671OPX
# IciiHO4Lwo2p9xh/tnj+JmCQEn5QU0NxzrOiRna4kjFaA9ZcwSaG7WAxeC/xoZSx
# F1oK1UPZtKVt+yrsGKqWONoK6f5EmBOAVEK2y4ATDSkb34UD7JA32f+Rm0wsr5aj
# zftDhA5mBipVZDjHpwzv8bTKzCDUSUuUmPo1govD0RwFcTtMXcfJtm1i+P2UNXad
# PyYVKRxKQATHN3imsfBiNRdN5kiVVeqP55piqgxOkyt+HkwIA4gbmSc3hD8ke66t
# 9MjlcNg73rZZlrLHsAIV/nJ0mmgSjBI/TthoGJDydekOQ2tQD2Dup/+sKQptalDl
# ui59SerVSJg8gAeV7N/ia4mrGoiez+SqV3olVfxyLFt3o/OQOnBmjhKUANoKLYlK
# mUpKEFI0PfoT8Q1W/y6s9LTI6ekbi0igEbFUIBE8KDUGfIwnisEkBw5KcBZ3XwnH
# mfznwlKo8QIDAQABo4IBVTCCAVEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU
# 3XRdTADbe5+gdMqxbvc8wDLAcM0wHwYDVR0jBBgwFoAUtqFUOQLDoD+Oirz61Pgc
# ptE6Dv0wDgYDVR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMDAGA1Ud
# HwQpMCcwJaAjoCGGH2h0dHA6Ly9jcmwuY2VydHVtLnBsL2N0bmNhMi5jcmwwbAYI
# KwYBBQUHAQEEYDBeMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0
# dW0uY29tMDIGCCsGAQUFBzAChiZodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwv
# Y3RuY2EyLmNlcjA5BgNVHSAEMjAwMC4GBFUdIAAwJjAkBggrBgEFBQcCARYYaHR0
# cDovL3d3dy5jZXJ0dW0ucGwvQ1BTMA0GCSqGSIb3DQEBDAUAA4ICAQB1iFgP5Y9Q
# KJpTnxDsQ/z0O23JmoZifZdEOEmQvo/79PQg9nLF/GJe6ZiUBEyDBHMtFRK0mXj3
# Qv3gL0sYXe+PPMfwmreJHvgFGWQ7XwnfMh2YIpBrkvJnjwh8gIlNlUl4KENTK5DL
# qsYPEtRQCw7R6p4s2EtWyDDr/M58iY2UBEqfUU/ujR9NuPyKk0bEcEi62JGxauFY
# zZ/yld13fHaZskIoq2XazjaD0pQkcQiIueL0HKiohS6XgZuUtCKA7S6CHttZEsOb
# QJ1j2s0urIDdqF7xaXFVaTHKtAuMfwi0jXtF3JJphrJfc+FFILgCbX/uYBPBlbBI
# P4Ht4xxk2GmfzMn7oxPITpigQFJFWuzTMUUgdRHTxaTSKRJ/6Uh7ki/pFjf9sUAS
# WgxT69QF9Ki4JF5nBIujxZ2sOU9e1HSCJwOfK07t5nnzbs1LbHuAIGJsRJiQ6HX/
# DW1XFOlXY1rc9HufFhWU+7Uk+hFkJsfzqBz3pRO+5aI6u5abI4Qws4YaeJH7H7M8
# X/YNoaArZbV4Ql+jarKsE0+8XvC4DJB+IVcvC9Ydqahi09mjQse4fxfef0L7E3hh
# o2O3bLDM6v60rIRUCi2fJT2/IRU5ohgyTch4GuYWefSBsp5NPJh4QRTP9DC3gc5Q
# EKtbrTY0Ka87Web7/zScvLmvQBm8JDFpDjCCBrkwggShoAMCAQICEQDn/2nHOzXO
# S5Em2HR8aKWHMA0GCSqGSIb3DQEBDAUAMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UE
# ChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENl
# cnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBO
# ZXR3b3JrIENBIDIwHhcNMjEwNTE5MDUzMjA3WhcNMzYwNTE4MDUzMjA3WjBWMQsw
# CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQw
# IgYDVQQDExtDZXJ0dW0gVGltZXN0YW1waW5nIDIwMjEgQ0EwggIiMA0GCSqGSIb3
# DQEBAQUAA4ICDwAwggIKAoICAQDpEh8ENe25XXrFppVBvoplf0530W0lddNmjtv4
# YSh/f7eDQKFaIqc7tHj7ox+u8vIsJZlroakUeMS3i3T8aJRC+eQs4FF0GqvkM6+W
# ZO8kmzZfxmZaBYmMLs8FktgFYCzywmXeQ1fEExflee2OpbHVk665eXRHjH7MYZIz
# Nnjl2m8Hy8ulB9mR8wL/W0v0pjKNT6G0sfrx1kk+3OGosFUb7yWNnVkWKU4qSxLv
# 16kJ6oVJ4BSbZ4xMak6JLeB8szrK9vwGDpvGDnKCUMYL3NuviwH1x4gZG0JAXU3x
# 2pOAz91JWKJSAmRy/l0s0l5bEYKolg+DMqVhlOANd8Yh5mkQWaMEvBRE/kAGzIqg
# WhwzN2OsKIVtO8mf5sPWSrvyplSABAYa13rMYnzwfg08nljZHghquCJYCa/xHK9a
# cev9UD7Y+usr15d7mrszzxhF1JOr1Mpup2chNSBlyOObhlSO16rwrffVrg/SzaKf
# SndS5swRhr8bnDqNJY9TNyEYvBYpgF95K7p0g4LguR4A++Z1nFIHWVY5v0fNVZmg
# zxD9uVo/gta3onGOQj3JCxgYx0KrCXu4yc9QiVwTFLWbNdHFSjBCt5/8Q9pLuRhV
# ocdCunhcHudMS1CGQ/Rn0+7P+fzMgWdRKfEOh/hjLrnQ8BdJiYrZNxvIOhM2aa3z
# EDHNwwIDAQABo4IBVTCCAVEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUvlQC
# L79AbHNDzqwJJU6eQ0Qa7uAwHwYDVR0jBBgwFoAUtqFUOQLDoD+Oirz61PgcptE6
# Dv0wDgYDVR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMDAGA1UdHwQp
# MCcwJaAjoCGGH2h0dHA6Ly9jcmwuY2VydHVtLnBsL2N0bmNhMi5jcmwwbAYIKwYB
# BQUHAQEEYDBeMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0dW0u
# Y29tMDIGCCsGAQUFBzAChiZodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwvY3Ru
# Y2EyLmNlcjA5BgNVHSAEMjAwMC4GBFUdIAAwJjAkBggrBgEFBQcCARYYaHR0cDov
# L3d3dy5jZXJ0dW0ucGwvQ1BTMA0GCSqGSIb3DQEBDAUAA4ICAQC4k1l3yUwV/ZQH
# CKCneqAs8EGTnwEUJLdDpokN/dMhKjK0rR5qX8nIIHzxpQR3TAw2IRw1Uxsr2Pli
# G3bCFqSdQTUbfaTq6V3vBzEebDru9QFjqlKnxCF2h1jhLNFFplbPJiW+JSnJTh1f
# KEqEdKdxgl9rVTvlxfEJ7exOn25MGbd/wGPwuSmMxRJVO0wnqgS7kmoJjNF9zqee
# hFSDDP8ZVkWg4EZ2tIS0M3uZmByRr+1Lkwjjt8AtW83mVnZTyTsOb+FNfwJY7DS4
# FmWhkRbgcHRetreoTirPOr/ozyDKhT8MTSTf6Lttg6s6T/u08mDWw6HK04ZRDfQ9
# sb77QV8mKgO44WGP31vXnVKoWVJpFBjPvjL8/Zck/5wXX2iqjOaLStFOR/IQki+E
# hn4zlcgVm22ZVCBPF+l8nAwUUShCtKuSU7GmZLKCmmxQMkSiWILTm8EtVD6AxnJh
# oq8EnhjEEyUoflkeRF2WhFiVQOmWTwZRr44IxWGkNJC6tTorW5rl2Zl+2e9JLPYf
# 3pStAPMDoPKIjVXd6NW2+fZrNUBeDo2eOa5Fn7Brs/HLQff5Xgris5MeUbdVgDrF
# 8uxO6cLPvZPo63j62SsNg55pTWk9fUIF9iPoRbb4QurjoY/woI1RAOKtYtTic6aA
# Jq3u83RIPpGXBSJKwx4KJAOZnCDCtTGCBqIwggaeAgEBMGowVjELMAkGA1UEBhMC
# UEwxITAfBgNVBAoTGEFzc2VjbyBEYXRhIFN5c3RlbXMgUy5BLjEkMCIGA1UEAxMb
# Q2VydHVtIENvZGUgU2lnbmluZyAyMDIxIENBAhBX5gfP8LabvnxCXoItEBa/MA0G
# CWCGSAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZI
# hvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcC
# ARUwLwYJKoZIhvcNAQkEMSIEIDJA/DgFCF+OJoGEysdN1QkEnc/lrwAvEKn5Kupf
# lVpfMA0GCSqGSIb3DQEBAQUABIIBgDrx+iodqAWBKWmkf7hoG3+5N+1HQAD/Bjiy
# 1kMOM/QVcHW4wtB4/UCc197jTXKNLvGAHpXiTATsy1SoHtElkha/rxTL9gdoe55M
# a4P5SSAX8/qgjbu7uo22IhZocEWQ0z8yMAGGtzIqyw/NPRQJ1lcUg8mRb+XoAPq2
# pgbQv/JEZnRZrafQ1KFURYmdTC4OsIFaC8xUQiUAUsa/g+MNkhcceE9qGemQ6Krb
# f8EzSzQ94uC5ESJYKC/5FX+DXImJFhaL+9dFvm5plx0MLyspewOLYJG8nbB56TsI
# 3EJa6hBYOMIm6drZzlJgd/YkJ9WihT1c0cCZZKUM2Z0/lzqkrgy0KX8/ae+M427p
# cC0hBf4oqcGS35Vx2MSL4PX7TXcS4RXplnf5NFsS5P0KMPpt0HADNIc/IFJRl8Up
# h+P5M8btN93HGWOR6F+Ar0JVwqkrU8kZsjRAG0ixkoMCjGFDY8M2gU/PkEwU1MVa
# L6P0mGw7qNjLUs+A5iGaIfjEue66CqGCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID
# 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0
# ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ
# KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD
# MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMjcwNTM5MjlaMDcG
# CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY
# 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDBh1UzoiviXHt0D+pEmXZBnVUNQwsUWFno5
# a9vhv0x3doLakrdf8m9ebZajeHeaAMswgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ
# MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh
# MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0
# dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI
# hvcNAQEBBQAEggIAL43uVNydSpQ8lTkeCKmLJ8cVhHoMYmpWmM012Wh5qt+B61/F
# gZ8GYGPY10YnstBP8tIoJ+FtjAujIFMnNFmRWjbHtH9s8by5wmZf2TSVlQHO39sX
# Aqeb72Zys/BRBBA5p9tTj34U5a4r6HN79L/pqRVHuKyX2D8jtb8RYYI6jO32grGw
# f7UYXyXH02fGqEtQ0ohW+C9fLy5Il1cUff7CavWnJI5BVZc3l3oCdsozHOM9gOqD
# fgqekP7fSkqOPiwmH/TcYzoM5azEyPZcc9EEZKh8lTNDw1egMfc1lvX7000cdhgc
# VIrlHz3NOl2R4362C6pMQwraY1SgyUCNq4B+9MqkWixwLmfl2NfwT9PP59wB5wG7
# nmoN0eX2NsbMZgFHqfJnW7s15E7hi8+58pJjMIURkS1t7AujfXc0wkdyTakBf6hd
# Zcsn1RbaIIaik/DujsEX3DWcBPOnkUJ4kwWWqlUoGeCvcvO29fB32y2bFoTbhvSo
# C/9Jkkpzmy3tEWDsFOr26UjsgDGhe4J3nCUmm/tx3cJefa8uxdaPteBUWWM2Imx6
# ZsB5VM6Vw3qGNMpHqJ+A0LRPOIEa0BQgFo+6mu8hvDa0H0g37bS+qp+0i3/rakDi
# l897CZ05k+ZGGgFzgNaZ+UNXK6FJInpKbRexQ6MG9D3n7mmkjhGtNIt9GzI=
# SIG # End signature block