Public/Reportings/Invoke-EntraOpsReportingGeneration.ps1

function Invoke-EntraOpsReportingGeneration {
    <#
    .SYNOPSIS
        Generates the configured EntraOps reports for any automation host.

    .DESCRIPTION
        Converts AutomatedReportingGeneration settings into one fail-fast reporting run, manages a
        shared EntraOps connection for Graph-backed reports, and optionally removes an externally
        prepared classification repository checkout. CI-specific checkout, authentication bootstrap,
        browser setup, artifact upload, and release publication remain outside the module.

    .PARAMETER ConfigFilePath
        Path to the EntraOps configuration file with the AutomatedReportingGeneration settings. Default is
        ./EntraOpsConfig.json. The file must exist.

    .PARAMETER ClassificationRepositoryPath
        Path to an AzurePrivilegedIAM checkout prepared outside the module. When the folder exists, it is used as
        repository root of the Classification Explorer. Default is ./.reporting/AzurePrivilegedIAM.

    .PARAMETER AuthenticationType
        Authentication type passed to Connect-EntraOps when a selected report needs Microsoft Graph. Default is
        AlreadyAuthenticated. The connection made by this cmdlet is disconnected after the run.

    .PARAMETER AllowStaleTenantGovernanceSnapshot
        Generates Configuration Analyzer data from a Tenant Governance snapshot older than its maximum age
        without applying ConfigurationAnalyzer.StaleTenantGovernanceSnapshotAction. Default is false. The
        maximum age is ConfigurationAnalyzer.MaxTenantGovernanceSnapshotAgeHours, or derived from
        TenantGovernanceSnapshot.SnapshotScheduledCron when not set (daily 30 hours, weekly 174 hours,
        monthly 750 hours).

    .PARAMETER AllowPartialTenantGovernanceSnapshot
        Allows Configuration Analyzer data from a partially successful Tenant Governance snapshot. Defaults to
        ConfigurationAnalyzer.AllowPartialTenantGovernanceSnapshot of the configuration file (true when not set).

    .PARAMETER RemoveClassificationRepositoryAfterRun
        Removes the folder of -ClassificationRepositoryPath after the run, also when the generation fails.

    .PARAMETER Force
        Generates the reports even when AutomatedReportingGeneration.ApplyAutomatedReportingGeneration is
        disabled. Without it, the cmdlet returns the status Disabled.

    .EXAMPLE
        Generate the configured reports with the existing Azure context.
        Invoke-EntraOpsReportingGeneration -ConfigFilePath ./EntraOpsConfig.json

    .EXAMPLE
        Generate the reports although automated reporting is disabled, and remove the prepared AzurePrivilegedIAM checkout afterwards.
        Invoke-EntraOpsReportingGeneration -Force -RemoveClassificationRepositoryAfterRun
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]$ConfigFilePath = './EntraOpsConfig.json',

        [Parameter(Mandatory = $false)]
        [string]$ClassificationRepositoryPath = './.reporting/AzurePrivilegedIAM',

        [Parameter(Mandatory = $false)]
        [ValidateSet('UserInteractive', 'SystemAssignedMSI', 'UserAssignedMSI', 'FederatedCredentials', 'AlreadyAuthenticated', 'DeviceAuthentication')]
        [string]$AuthenticationType = 'AlreadyAuthenticated',

        [Parameter(Mandatory = $false)]
        [Nullable[bool]]$AllowStaleTenantGovernanceSnapshot,

        [Parameter(Mandatory = $false)]
        [Nullable[bool]]$AllowPartialTenantGovernanceSnapshot,

        [Parameter(Mandatory = $false)]
        [switch]$RemoveClassificationRepositoryAfterRun,

        [Parameter(Mandatory = $false)]
        [switch]$Force
    )

    $ErrorActionPreference = 'Stop'
    $ResolvedConfigFile = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ConfigFilePath)
    if (-not (Test-Path -LiteralPath $ResolvedConfigFile -PathType Leaf)) {
        throw "EntraOps configuration file '$ResolvedConfigFile' does not exist."
    }
    $Config = Get-Content -LiteralPath $ResolvedConfigFile -Raw | ConvertFrom-Json -Depth 100 -ErrorAction Stop
    $ReportingConfig = $Config.AutomatedReportingGeneration
    if (-not $Force -and $ReportingConfig.ApplyAutomatedReportingGeneration -ne $true) {
        Write-Verbose 'Automated reporting generation is disabled in the configuration.'
        return [pscustomobject]@{ Status = 'Disabled'; Reports = @() }
    }

    $GenerateClassificationExplorer = $ReportingConfig.GenerateClassificationExplorer -ne $false
    $GenerateTierBreachAnalyzer = $ReportingConfig.GenerateTierBreachAnalyzer -ne $false
    $GenerateEamDashboard = $ReportingConfig.GenerateEamDashboard -ne $false
    $GeneratePrivilegedAssets = $ReportingConfig.GeneratePrivilegedAssets -ne $false
    $GenerateAccessPathMap = $ReportingConfig.GenerateAccessPathMap -ne $false
    $GeneratePrivilegeHistory = $ReportingConfig.GeneratePrivilegeHistory -ne $false -and $Config.PrivilegeHistory.EnablePrivilegeHistory -ne $false
    $GenerateConfigurationAnalyzer = $ReportingConfig.GenerateConfigurationAnalyzer -ne $false
    $GenerateAccessPackageFlow = $ReportingConfig.GenerateAccessPackageFlow -ne $false
    $ResolveEamDashboardObjects = $Config.EamDashboard.ResolveLinkedIdentityObjectIds -ne $false
    $ResolvePrivilegedAssetsObjects = $Config.PrivilegedAssets.ResolveRelatedObjectIds -ne $false
    $ResolveAccessPathObjects = $Config.AccessPathMap.ResolveObjectIdsOutsidePrivilegedEAM -ne $false

    if ($null -eq $AllowPartialTenantGovernanceSnapshot) {
        $AllowPartialTenantGovernanceSnapshot = $Config.ConfigurationAnalyzer.AllowPartialTenantGovernanceSnapshot -ne $false
    }
    if ($null -eq $AllowStaleTenantGovernanceSnapshot) {
        $AllowStaleTenantGovernanceSnapshot = $false
    }

    # Stale snapshot settings are only validated when the Configuration Analyzer is generated, so an
    # invalid value never blocks the other reports of a run that does not use it.
    $MaxSnapshotAgeHours = $null
    if ($GenerateConfigurationAnalyzer) {
        $StaleSnapshotAction = "$($Config.ConfigurationAnalyzer.StaleTenantGovernanceSnapshotAction)".Trim()
        if ([string]::IsNullOrWhiteSpace($StaleSnapshotAction)) { $StaleSnapshotAction = 'Warn' }
        if ($StaleSnapshotAction -notin @('Warn', 'Skip', 'Fail')) {
            throw "ConfigurationAnalyzer.StaleTenantGovernanceSnapshotAction must be 'Warn', 'Skip' or 'Fail', but is '$StaleSnapshotAction'."
        }
        $MaxSnapshotAgeHours = Resolve-EntraOpsTenantGovernanceSnapshotMaxAge -Config $Config

        $SnapshotManifestPath = Join-Path $EntraOpsBaseFolder 'TenantGovernance/Snapshots/.SnapshotManifest.json'
        if (-not (Test-Path -LiteralPath $SnapshotManifestPath -PathType Leaf)) {
            Write-Warning "Skipping Configuration Analyzer generation because no Tenant Governance snapshot manifest exists at $SnapshotManifestPath. Enable and run the Tenant Governance snapshot automation first."
            $GenerateConfigurationAnalyzer = $false
        } elseif (-not $AllowStaleTenantGovernanceSnapshot) {
            # Checked before connecting, so a stale snapshot never fails the run after the other reports were generated.
            $SnapshotAge = Get-EntraOpsTenantGovernanceSnapshotAge -ManifestPath $SnapshotManifestPath
            if ($null -ne $SnapshotAge -and $SnapshotAge.AgeHours -gt $MaxSnapshotAgeHours) {
                $StaleMessage = "Tenant Governance snapshot captured $($SnapshotAge.CapturedDateTime.UtcDateTime.ToString('u')) is $([math]::Round($SnapshotAge.AgeHours, 1)) hour(s) old, exceeding the $MaxSnapshotAgeHours-hour limit."
                switch ($StaleSnapshotAction) {
                    'Warn' {
                        Write-Warning "$StaleMessage Configuration Analyzer data is generated from it and marked as stale (ConfigurationAnalyzer.StaleTenantGovernanceSnapshotAction = Warn)."
                    }
                    'Skip' {
                        Write-Warning "$StaleMessage Skipping Configuration Analyzer generation (ConfigurationAnalyzer.StaleTenantGovernanceSnapshotAction = Skip)."
                        $GenerateConfigurationAnalyzer = $false
                    }
                    'Fail' {
                        throw "$StaleMessage Run a new Tenant Governance snapshot, raise ConfigurationAnalyzer.MaxTenantGovernanceSnapshotAgeHours, or pass -AllowStaleTenantGovernanceSnapshot."
                    }
                }
            }
            # The decision is taken once here. Configuration Analyzer is generated after the other reports, so
            # the generator must not fail the run when the snapshot crosses the limit in the meantime; it still
            # marks a stale snapshot in the report.
            $AllowStaleTenantGovernanceSnapshot = $true
        }
    }

    $NeedsConnection = $GenerateEamDashboard -or
    ($GeneratePrivilegedAssets -and $ResolvePrivilegedAssetsObjects) -or
    ($GenerateAccessPathMap -and $ResolveAccessPathObjects) -or
    $GenerateConfigurationAnalyzer -or $GenerateAccessPackageFlow
    $ConnectedHere = $false
    $ResolvedClassificationRepositoryPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ClassificationRepositoryPath)

    try {
        if ($NeedsConnection) {
            $ConnectParameters = @{
                AuthenticationType = $AuthenticationType
                TenantName         = [string]$Config.TenantName
                ConfigFilePath     = $ResolvedConfigFile
                NoWelcome          = $true
            }
            if (-not [string]::IsNullOrWhiteSpace([string]$Config.TenantId)) {
                $ConnectParameters.TenantId = [string]$Config.TenantId
            }
            Connect-EntraOps @ConnectParameters
            $ConnectedHere = $true
        }

        $ReportingParameters = @{
            ConfigFilePath                                    = $ResolvedConfigFile
            SkipClassificationExplorer                        = -not $GenerateClassificationExplorer
            SkipTierBreachAnalyzer                            = -not $GenerateTierBreachAnalyzer
            SkipEamDashboard                                  = -not $GenerateEamDashboard
            SkipPrivilegedAssets                              = -not $GeneratePrivilegedAssets
            SkipAccessPathMap                                 = -not $GenerateAccessPathMap
            SkipPrivilegeHistory                              = -not $GeneratePrivilegeHistory
            SkipConfigurationAnalyzer                         = -not $GenerateConfigurationAnalyzer
            SkipAccessPackageFlow                             = -not $GenerateAccessPackageFlow
            EamDashboardResolveLinkedIdentityObjectIds        = $ResolveEamDashboardObjects
            PrivilegedAssetsResolveRelatedObjectIds           = $ResolvePrivilegedAssetsObjects
            AccessPathMapResolveObjectIdsOutsidePrivilegedEAM = $ResolveAccessPathObjects
            AllowStaleTenantGovernanceSnapshot                = [bool]$AllowStaleTenantGovernanceSnapshot
            AllowPartialTenantGovernanceSnapshot              = [bool]$AllowPartialTenantGovernanceSnapshot
            FailureAction                                     = 'Stop'
            PassThru                                          = $true
        }
        if ($null -ne $MaxSnapshotAgeHours) {
            $ReportingParameters.ConfigurationAnalyzerMaxSnapshotAgeHours = $MaxSnapshotAgeHours
        }
        if (Test-Path -LiteralPath $ResolvedClassificationRepositoryPath -PathType Container) {
            $ReportingParameters.ClassificationExplorerRepoRoot = $ResolvedClassificationRepositoryPath
        }
        if (-not [string]::IsNullOrWhiteSpace([string]$Config.TenantId)) {
            $ReportingParameters.AccessPathMapTenantId = [string]$Config.TenantId
        }
        if ($null -ne $Config.PrivilegeHistory.TimeRangeInDays) {
            $ReportingParameters.PrivilegeHistoryTimeRangeInDays = [int]$Config.PrivilegeHistory.TimeRangeInDays
        }
        if (-not [string]::IsNullOrWhiteSpace([string]$Config.PrivilegeHistory.SnapshotInterval)) {
            $ReportingParameters.PrivilegeHistorySnapshotInterval = [string]$Config.PrivilegeHistory.SnapshotInterval
        }
        if ($null -ne $Config.ClassificationExplorer.GenerateChangeHistory) {
            $ReportingParameters.SkipExplorerHistory = -not [bool]$Config.ClassificationExplorer.GenerateChangeHistory
        }

        $Reports = @(New-EntraOpsReportingData @ReportingParameters)
        return [pscustomobject]@{
            Status  = 'Generated'
            Reports = $Reports
        }
    } finally {
        if ($ConnectedHere) {
            Disconnect-EntraOps | Out-Null
        }
        if ($RemoveClassificationRepositoryAfterRun -and (Test-Path -LiteralPath $ResolvedClassificationRepositoryPath)) {
            # Only a folder inside the working folder is removed
            if (Test-EntraOpsPathWithinRoot -Path $ResolvedClassificationRepositoryPath -Root $EntraOpsBaseFolder) {
                Remove-Item -LiteralPath $ResolvedClassificationRepositoryPath -Recurse -Force -ErrorAction SilentlyContinue
            } else {
                Write-Warning "Classification repository '$ResolvedClassificationRepositoryPath' is not inside the EntraOps working folder '$EntraOpsBaseFolder' and was not removed."
            }
        }
    }
}

# SIG # Begin signature block
# MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAz+uRGs0TRtGAF
# uQ+tA/exYWYB68I1JE2a02NJsw6wn6CCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j
# 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
# ARUwLwYJKoZIhvcNAQkEMSIEIC+ybqdfaoM6LB44MKrgstDYcIRwj08NpAwxkk3m
# 8zL0MA0GCSqGSIb3DQEBAQUABIIBgJRewUlEWqi7U020Ple6gx/BzXKiKP2kDeGq
# DXDAO0AHf3qDY3tfodCMq036Js6HFTi99uXAZ9B04Z3rsQ+h9cCSmGe8CEmAO2tL
# 2afhXv7DIBYC0hmWzfcTSUidZPtjXXYTAf4lhuNh6mm3xLbfobjGhgu1o8QgrXmJ
# zqFWJhb2utLRXjRsUTngeHoOia8VsGgVJOyEgy7B/z7ZXMquFKUUSN13r/Lz78Ys
# kIODVdHYWX7vGT4cyOrmqZSWbvSNg2SiffztJ3AWlDgLdetlQjUuy1i5APGAQgq4
# E+rpH2+Ul1Cm985DKpzlzexz6IMmay/fFqNcRi6+wWOCJZuMZjbEZMQanN2xazRp
# BGguBV3wTCO+mQsnIjJoP064SyPixoCUTiKfEChpwyGCTRjBX3sQlFD0Dt8JcSrP
# OVz1iLP/tQ+XWf5v3KeTXwdHtdEutHOUqulaJbG/wldEYB/ANeIrDMPtQDDXq+8H
# 1qyMs++hyAR2+Px+PnurrSvLwNASoqGCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID
# 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0
# ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ
# KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD
# MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMjIxMTQwMTRaMDcG
# CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY
# 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDAa887igmu6o8zTG9eGkLqzMDOjq+bZR/tD
# j6DJ0qYmRbXbsvZKsAbSiAj8cYGm4jswgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ
# MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh
# MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0
# dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI
# hvcNAQEBBQAEggIAKsu3fiWxYDg/Zn0U2MXov0gw4zT/2PVbKpG4XYCGlH4iaNHq
# ieM597VWbLVZ/7M7JrbL8sJQZkZTu+g+Mr1ewAkbUbJy5xnuInnN3fvN8KE4Urkc
# dsRub9IiAtqW0Kjgjc5Y8ckn1O2o8Es3eb1dqbRf8pf4oKqqCRVThdrv92taxMFe
# a8f/xyStJ90OBeYLY07DCifBvCF4NINWFsAyDC6or4q8dxzTeOPERrOqJcfnvxH7
# 6Ne0xGjI6T/Xmv0sSmIniNyCjaCR9QgfoFp5WCth66RUCamY3QDK39xgqjXcVZAQ
# j2U5M9EAgJ4BSiQWSh6nVlc1m7EtSYCjjREfADWO9D3na7RBd7i3D28pNRc49Xna
# yZEZOGy54R5DOXa2oIQvsLPG0DzHHdf1/T1x+0mL8vZhX2YbrJWy11VrxtP29Vq8
# stQek/RTwOOHHb98PSLdSD+caBhZT/HSyqYFmVEHeqFAzG9eBrV+HxnawLfCBxev
# Z/4YUF96dOXH8gD10xk8F9SYiYMezDOm7PuLb+Yi1vkqr5iuKeP7XEB/LIBQflgF
# iC+CpQa7moBEFXF5GvdnteOgZ6fqFVt4hkb5+Pc3KPhIUfcpXA+iBt/V0z8ADxE0
# 5iUw8JXViosvzEFn3s1JdX8LCWL1NP+wVL4rzhLUOOLhjO2akn6WSHlQdk0=
# SIG # End signature block