Public/Reportings/New-EntraOpsReportingData.ps1

<#
.SYNOPSIS
    Generate (refresh) data for all EntraOps Reporting static web apps in a single call.

.DESCRIPTION
    Convenience wrapper around the Classification Explorer, Tier Breach Analyzer, EAM Dashboard,
    Privilege History, Configuration Analyzer, Access Package Flow, and Access Path Map generators. It runs all
    applicable generators in one call, so the whole
    EntraOps Reporting portal (Reports/index.html + all sub-apps) can be refreshed with a
    single cmdlet. Use Remove-EntraOpsReportingData to clear the generated data again.

.PARAMETER EntraOpsRoot
    Path to the EntraOps repository root. Used as the classification-logic source
    (Classification/Templates) for the Classification Explorer and as the PrivilegedEAM
    source root for the Tier Breach Analyzer. Defaults to the repository this module lives in.

.PARAMETER ConfigFilePath
    Configuration file used for Privilege History and Tenant Governance feature settings. Defaults
    to EntraOpsConfig.json below EntraOpsRoot.

.PARAMETER ClassificationExplorerRepoRoot
    Path to the repository that contains the classified roles and permission catalogs (the
    AzurePrivilegedIAM repository). Forwarded as -RepoRoot to New-EntraOpsClassificationExplorerData.
    When omitted, that cmdlet auto-detects the location.

.PARAMETER ClassificationExplorerAppRoot
    Path to the Classification Explorer app folder. Defaults to Reports/ClassificationExplorer
    under EntraOpsRoot.

.PARAMETER TierBreachImportPath
    Folder with the Privileged EAM export. Forwarded as -ImportPath to
    New-EntraOpsTierBreachAnalyzerData. Defaults to <EntraOpsRoot>/PrivilegedEAM.

.PARAMETER TierBreachAnalyzerAppRoot
    Path to the Tier Breach Analyzer app folder. Defaults to Reports/TierBreachAnalyzer under
    EntraOpsRoot.

.PARAMETER EamDashboardImportPath
    Folder with the Privileged EAM export. Forwarded as -ImportPath to
    New-EntraOpsPrivilegedEamDashboardData. Defaults to <EntraOpsRoot>/PrivilegedEAM.

.PARAMETER EamDashboardAppRoot
    Path to the EAM Dashboard app folder. Defaults to Reports/EamDashboard under
    EntraOpsRoot.

.PARAMETER EamDashboardResolveLinkedIdentityObjectIds
    Forwarded to New-EntraOpsPrivilegedEamDashboardData. Resolves linked identity
    IDs outside the Privileged EAM export through Microsoft Graph when enabled.
    When omitted, the EAM Dashboard generator defaults this option to `$true`.

.PARAMETER AccessPathMapTenantId
    Tenant id forwarded to New-EntraOpsAccessPathMapData (used to build role node ids
    matching the BloodHound OpenGraph export). Defaults to the ObjectTenantId found in the export.

.PARAMETER AccessPathMapImportPath
    Folder with the Privileged EAM export. Forwarded as -ImportPath to
    New-EntraOpsAccessPathMapData. Defaults to <EntraOpsRoot>/PrivilegedEAM.

.PARAMETER AccessPathMapAppRoot
    Path to the Access Path Map app folder. Defaults to Reports/AccessPathMap under
    EntraOpsRoot.

.PARAMETER AccessPathMapResolveObjectIdsOutsidePrivilegedEAM
    Forwarded as -ResolveObjectIdsOutsidePrivilegedEAM to New-EntraOpsAccessPathMapData (resolves edge
    endpoints outside the Privileged EAM export via Microsoft Graph instead of showing them as an
    unresolved placeholder). When omitted, New-EntraOpsAccessPathMapData falls back to the
    `AccessPathMap.ResolveObjectIdsOutsidePrivilegedEAM` setting in EntraOpsConfig.json, or $true.

.PARAMETER SkipClassificationExplorer
    Do not generate Classification Explorer data.

.PARAMETER SkipExplorerHistory
    Do not derive the Classification Explorer change history (git log over the classification
    source files). When omitted, New-EntraOpsClassificationExplorerData falls back to the
    inverse of `ClassificationExplorer.GenerateChangeHistory` in EntraOpsConfig.json. A missing
    setting or configuration file defaults generation to $false because deriving history from the
    AzurePrivilegedIAM git log is slow and rarely meaningful. Pass -SkipExplorerHistory:$false to
    generate it for an individual invocation.

.PARAMETER SkipTierBreachAnalyzer
    Do not generate Tier Breach Analyzer data.

.PARAMETER SkipEamDashboard
    Do not generate EAM Dashboard data.

.PARAMETER SkipAccessPathMap
    Do not generate Access Path Map data.

.PARAMETER PrivilegeHistoryImportPath
    Folder with the Privileged EAM export whose git history is walked. Forwarded as
    -ImportPath to New-EntraOpsPrivilegedEamPrivilegeHistoryData. Defaults to
    <EntraOpsRoot>/PrivilegedEAM.

.PARAMETER PrivilegeHistoryAppRoot
    Path to the Privilege History app folder. Defaults to Reports/PrivilegeHistory under
    EntraOpsRoot.

.PARAMETER PrivilegeHistoryTimeRangeInDays
    Only consider commits from the last N days for the Privilege History dataset. Forwarded as
    -TimeRangeInDays to New-EntraOpsPrivilegedEamPrivilegeHistoryData. Defaults to the
    `PrivilegeHistory.TimeRangeInDays` setting in EntraOpsConfig.json, or the full git history
    ($null) when that config file/setting is not present.

.PARAMETER PrivilegeHistorySnapshotInterval
    Minimum spacing between two consecutive Privilege History snapshots, as an ISO 8601
    duration (`P1D` daily, `P1W` weekly, `P2W` bi-weekly, `P1M` monthly, `P3M` quarterly,
    `P1Y` yearly, ...). Forwarded as -SnapshotInterval to
    New-EntraOpsPrivilegedEamPrivilegeHistoryData. Defaults to the
    `PrivilegeHistory.SnapshotInterval` setting in EntraOpsConfig.json, or `P2W` (bi-weekly)
    when that config file/setting is not present.

.PARAMETER SkipPrivilegeHistory
    Do not generate the Privilege History data, regardless of the
    `PrivilegeHistory.EnablePrivilegeHistory` setting in EntraOpsConfig.json.

.PARAMETER ConfigurationAnalyzerImportPath
    Folder with the Tenant Governance snapshot export whose git history is walked.
    Forwarded as -ImportPath to New-EntraOpsTenantGovernanceConfigurationAnalyzerData.
    Defaults to <EntraOpsRoot>/TenantGovernance/Snapshots.

.PARAMETER ConfigurationAnalyzerAppRoot
    Path to the Configuration Analyzer app folder. Defaults to
    Reports/ConfigurationAnalyzer under EntraOpsRoot.

.PARAMETER SkipConfigurationAnalyzer
    Do not generate the Configuration Analyzer data. When not skipped, the data is only
    generated if the Tenant Governance snapshot folder exists (or
    `TenantGovernanceSnapshot.EnableTenantGovernanceSnapshot` is set to true in
    EntraOpsConfig.json), so repositories without the feature stay untouched.

.PARAMETER SkipAccessPackageFlow
    Do not generate Graph-backed group and resource enrichment for Access Package Flow.

.PARAMETER PassThru
    Emit the result object of each generator (one per app) to the pipeline.

.PARAMETER FailureAction
    Controls whether failures from optional generators stop the combined run or are reported as
    warnings. Continue preserves the interactive convenience behavior; Stop is intended for CI.

.PARAMETER AllowStaleTenantGovernanceSnapshot
    Allow Configuration Analyzer generation from a successful snapshot older than its maximum age.

.PARAMETER AllowPartialTenantGovernanceSnapshot
    Allow Configuration Analyzer generation from a partial snapshot with preserved stale resource types.

.EXAMPLE
    New-EntraOpsReportingData

    Regenerates the data for all reporting apps in this repository checkout.

.EXAMPLE
    New-EntraOpsReportingData -ClassificationExplorerRepoRoot "C:\Repos\AzurePrivilegedIAM" -WhatIf

    Shows what would be generated for all apps without changing any files.

.EXAMPLE
    New-EntraOpsReportingData -SkipClassificationExplorer

    Regenerates only the Tier Breach Analyzer data.
#>


function New-EntraOpsReportingData {

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $false)]
        [System.String]$EntraOpsRoot,

        [Parameter(Mandatory = $false)]
        [System.String]$ConfigFilePath,

        [Parameter(Mandatory = $false)]
        [System.String]$ClassificationExplorerRepoRoot,

        [Parameter(Mandatory = $false)]
        [System.String]$ClassificationExplorerAppRoot,

        [Parameter(Mandatory = $false)]
        [System.String]$TierBreachImportPath,

        [Parameter(Mandatory = $false)]
        [System.String]$TierBreachAnalyzerAppRoot,

        [Parameter(Mandatory = $false)]
        [System.String]$EamDashboardImportPath,

        [Parameter(Mandatory = $false)]
        [System.String]$EamDashboardAppRoot,

        [Parameter(Mandatory = $false)]
        [System.Nullable[bool]]$EamDashboardResolveLinkedIdentityObjectIds,

        [Parameter(Mandatory = $false)]
        [System.String]$AccessPathMapTenantId,

        [Parameter(Mandatory = $false)]
        [System.String]$AccessPathMapImportPath,

        [Parameter(Mandatory = $false)]
        [System.String]$AccessPathMapAppRoot,

        [Parameter(Mandatory = $false)]
        [System.Nullable[bool]]$AccessPathMapResolveObjectIdsOutsidePrivilegedEAM,

        [Parameter(Mandatory = $false)]
        [System.String]$PrivilegeHistoryImportPath,

        [Parameter(Mandatory = $false)]
        [System.String]$PrivilegeHistoryAppRoot,

        [Parameter(Mandatory = $false)]
        [System.Nullable[int]]$PrivilegeHistoryTimeRangeInDays,

        [Parameter(Mandatory = $false)]
        [System.String]$PrivilegeHistorySnapshotInterval,

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

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

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

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

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

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

        [Parameter(Mandatory = $false)]
        [System.String]$ConfigurationAnalyzerImportPath,

        [Parameter(Mandatory = $false)]
        [System.String]$ConfigurationAnalyzerAppRoot,

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

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

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

        [Parameter(Mandatory = $false)]
        [ValidateSet('Continue', 'Stop')]
        [string]$FailureAction = 'Continue',

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

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

    # Resolve the repository root relative to the module location. Prefer the module's
    # own ModuleBase (always populated for exported module functions) over $PSScriptRoot,
    # which can be empty depending on how the function was invoked/loaded.
    $ModuleRoot = $MyInvocation.MyCommand.Module.ModuleBase
    if ([string]::IsNullOrWhiteSpace($ModuleRoot) -and -not [string]::IsNullOrWhiteSpace($PSScriptRoot)) {
        $ModuleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
    }
    if ([string]::IsNullOrWhiteSpace($ModuleRoot)) {
        throw "Unable to resolve the EntraOps module location. Import the module with 'Import-Module <path-to-EntraOps> -Force' and try again."
    }
    $RepositoryRoot = Split-Path -Parent $ModuleRoot
    if ([string]::IsNullOrWhiteSpace($EntraOpsRoot)) { $EntraOpsRoot = $RepositoryRoot }

    Write-Verbose "Using EntraOps repository root: $EntraOpsRoot"

    # Privilege History defaults come from EntraOpsConfig.json ("PrivilegeHistory": { "EnablePrivilegeHistory":
    # true|false, "TimeRangeInDays": <int>|null }) so it can be toggled without changing
    # calling scripts/workflows. Enabled by default when the config file/section is absent;
    # -SkipPrivilegeHistory always wins over the config setting.
    $EnablePrivilegeHistory = $true
    $ConfigPrivilegeHistoryTimeRangeInDays = $null
    $ConfigPrivilegeHistorySnapshotInterval = $null
    if ([string]::IsNullOrWhiteSpace($ConfigFilePath)) {
        $ConfigFilePath = Join-Path $EntraOpsRoot 'EntraOpsConfig.json'
    } else {
        $ConfigFilePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ConfigFilePath)
    }
    if (Test-Path -LiteralPath $ConfigFilePath -PathType Leaf) {
        try {
            $EntraOpsConfig = Get-Content -LiteralPath $ConfigFilePath -Raw | ConvertFrom-Json
            if ($null -ne $EntraOpsConfig.PrivilegeHistory) {
                if ($null -ne $EntraOpsConfig.PrivilegeHistory.EnablePrivilegeHistory) {
                    $EnablePrivilegeHistory = [bool]$EntraOpsConfig.PrivilegeHistory.EnablePrivilegeHistory
                }
                if ($null -ne $EntraOpsConfig.PrivilegeHistory.TimeRangeInDays) {
                    $ConfigPrivilegeHistoryTimeRangeInDays = [int]$EntraOpsConfig.PrivilegeHistory.TimeRangeInDays
                }
                if (-not [string]::IsNullOrWhiteSpace($EntraOpsConfig.PrivilegeHistory.SnapshotInterval)) {
                    $ConfigPrivilegeHistorySnapshotInterval = [string]$EntraOpsConfig.PrivilegeHistory.SnapshotInterval
                }
            }
        } catch {
            Write-Warning "Failed to read PrivilegeHistory settings from ${ConfigFilePath}: $($_.Exception.Message). Using defaults (enabled, full history, bi-weekly)."
        }
    }
    if (-not $PSBoundParameters.ContainsKey('PrivilegeHistoryTimeRangeInDays')) {
        $PrivilegeHistoryTimeRangeInDays = $ConfigPrivilegeHistoryTimeRangeInDays
    }
    if (-not $PSBoundParameters.ContainsKey('PrivilegeHistorySnapshotInterval')) {
        $PrivilegeHistorySnapshotInterval = $ConfigPrivilegeHistorySnapshotInterval
    }

    $results = [System.Collections.Generic.List[object]]::new()

    Write-Verbose "Starting reporting data generation. Skipped apps: TierBreachAnalyzer=$SkipTierBreachAnalyzer; EamDashboard=$SkipEamDashboard; PrivilegeHistory=$SkipPrivilegeHistory; ConfigurationAnalyzer=$SkipConfigurationAnalyzer; AccessPackageFlow=$SkipAccessPackageFlow; AccessPathMap=$SkipAccessPathMap; ClassificationExplorer=$SkipClassificationExplorer."

    if (-not $SkipTierBreachAnalyzer) {
        $TierBreachParams = @{
            RepoRoot = $EntraOpsRoot
            Verbose  = $VerbosePreference -eq 'Continue'
            WhatIf   = $WhatIfPreference
            Confirm  = $false
        }
        if (-not [string]::IsNullOrWhiteSpace($TierBreachImportPath)) { $TierBreachParams.ImportPath = $TierBreachImportPath }
        if (-not [string]::IsNullOrWhiteSpace($TierBreachAnalyzerAppRoot)) { $TierBreachParams.AppRoot = $TierBreachAnalyzerAppRoot }
        if ($PassThru) { $TierBreachParams.PassThru = $true }

        Write-Verbose "Generating Tier Breach Analyzer data..."
        $TierBreachResult = New-EntraOpsTierBreachAnalyzerData @TierBreachParams
        $results.Add([pscustomobject]@{ App = 'TierBreachAnalyzer'; Result = $TierBreachResult })
        Write-Verbose "Completed Tier Breach Analyzer data generation."
    }

    if (-not $SkipEamDashboard) {
        $EamDashboardParams = @{
            RepoRoot = $EntraOpsRoot
            Verbose  = $VerbosePreference -eq 'Continue'
            WhatIf   = $WhatIfPreference
            Confirm  = $false
        }
        if (-not [string]::IsNullOrWhiteSpace($EamDashboardImportPath)) { $EamDashboardParams.ImportPath = $EamDashboardImportPath }
        if (-not [string]::IsNullOrWhiteSpace($EamDashboardAppRoot)) { $EamDashboardParams.AppRoot = $EamDashboardAppRoot }
        if ($null -ne $EamDashboardResolveLinkedIdentityObjectIds) { $EamDashboardParams.ResolveLinkedIdentityObjectIds = $EamDashboardResolveLinkedIdentityObjectIds }
        if ($PassThru) { $EamDashboardParams.PassThru = $true }

        Write-Verbose "Generating EAM Dashboard data..."
        $EamDashboardResult = New-EntraOpsPrivilegedEamDashboardData @EamDashboardParams
        $results.Add([pscustomobject]@{ App = 'EamDashboard'; Result = $EamDashboardResult })
        Write-Verbose "Completed EAM Dashboard data generation."
    }

    if (-not $SkipPrivilegeHistory -and $EnablePrivilegeHistory) {
        $PrivilegeHistoryParams = @{
            RepoRoot = $EntraOpsRoot
            Verbose  = $VerbosePreference -eq 'Continue'
            WhatIf   = $WhatIfPreference
            Confirm  = $false
        }
        if (-not [string]::IsNullOrWhiteSpace($PrivilegeHistoryImportPath)) { $PrivilegeHistoryParams.ImportPath = $PrivilegeHistoryImportPath }
        if (-not [string]::IsNullOrWhiteSpace($PrivilegeHistoryAppRoot)) { $PrivilegeHistoryParams.AppRoot = $PrivilegeHistoryAppRoot }
        if ($null -ne $PrivilegeHistoryTimeRangeInDays) { $PrivilegeHistoryParams.TimeRangeInDays = $PrivilegeHistoryTimeRangeInDays }
        if (-not [string]::IsNullOrWhiteSpace($PrivilegeHistorySnapshotInterval)) { $PrivilegeHistoryParams.SnapshotInterval = $PrivilegeHistorySnapshotInterval }
        if ($PassThru) { $PrivilegeHistoryParams.PassThru = $true }

        Write-Verbose "Generating Privilege History data..."
        try {
            $PrivilegeHistoryResult = New-EntraOpsPrivilegedEamPrivilegeHistoryData @PrivilegeHistoryParams
            $results.Add([pscustomobject]@{ App = 'PrivilegeHistory'; Result = $PrivilegeHistoryResult })
        } catch {
            if ($FailureAction -eq 'Stop') { throw }
            Write-Warning "Skipped Privilege History data: $($_.Exception.Message)"
        }
    } elseif (-not $SkipPrivilegeHistory -and -not $EnablePrivilegeHistory) {
        Write-Verbose "Skipping Privilege History data (PrivilegeHistory.EnablePrivilegeHistory is false in $ConfigFilePath)."
    }

    # Only generate the snapshot-based apps (Configuration Analyzer, Access Package Flow) when
    # the Tenant Governance Snapshot feature is in use: the snapshot folder exists (working
    # tree) or the feature is enabled in EntraOpsConfig.json. Resolved outside the skip blocks
    # because both apps share these checks (for example -SkipConfigurationAnalyzer alone must
    # not disable the Access Package Flow enrichment).
    $TgSnapshotFolder = if (-not [string]::IsNullOrWhiteSpace($ConfigurationAnalyzerImportPath)) { $ConfigurationAnalyzerImportPath } else { Join-Path $EntraOpsRoot 'TenantGovernance/Snapshots' }
    $TgEnabledInConfig = $false
    try {
        if ($null -ne $EntraOpsConfig.TenantGovernanceSnapshot.EnableTenantGovernanceSnapshot) {
            $TgEnabledInConfig = [bool]$EntraOpsConfig.TenantGovernanceSnapshot.EnableTenantGovernanceSnapshot
        }
    } catch { $TgEnabledInConfig = $false }

    if (-not $SkipConfigurationAnalyzer) {
        if ((Test-Path -LiteralPath $TgSnapshotFolder -PathType Container) -or $TgEnabledInConfig) {
            $ConfigurationAnalyzerParams = @{
                RepoRoot = $EntraOpsRoot
                Verbose  = $VerbosePreference -eq 'Continue'
                WhatIf   = $WhatIfPreference
                Confirm  = $false
            }
            if (-not [string]::IsNullOrWhiteSpace($ConfigurationAnalyzerImportPath)) { $ConfigurationAnalyzerParams.ImportPath = $ConfigurationAnalyzerImportPath }
            if (-not [string]::IsNullOrWhiteSpace($ConfigurationAnalyzerAppRoot)) { $ConfigurationAnalyzerParams.AppRoot = $ConfigurationAnalyzerAppRoot }
            if ($AllowStaleTenantGovernanceSnapshot) { $ConfigurationAnalyzerParams.AllowStaleSnapshot = $true }
            if ($AllowPartialTenantGovernanceSnapshot) { $ConfigurationAnalyzerParams.AllowPartialSnapshot = $true }
            if ($PassThru) { $ConfigurationAnalyzerParams.PassThru = $true }

            Write-Verbose "Generating Configuration Analyzer data..."
            try {
                $ConfigurationAnalyzerResult = New-EntraOpsTenantGovernanceConfigurationAnalyzerData @ConfigurationAnalyzerParams
                $results.Add([pscustomobject]@{ App = 'ConfigurationAnalyzer'; Result = $ConfigurationAnalyzerResult })
            } catch {
                if ($FailureAction -eq 'Stop') { throw }
                Write-Warning "Skipped Configuration Analyzer data: $($_.Exception.Message)"
            }
        } else {
            Write-Verbose "Skipping Configuration Analyzer data (no Tenant Governance snapshot folder at $TgSnapshotFolder and TenantGovernanceSnapshot.EnableTenantGovernanceSnapshot is not enabled)."
        }
    }

    if (-not $SkipAccessPathMap) {
        $AccessPathParams = @{
            RepoRoot = $EntraOpsRoot
            Verbose  = $VerbosePreference -eq 'Continue'
            WhatIf   = $WhatIfPreference
            Confirm  = $false
        }
        if (-not [string]::IsNullOrWhiteSpace($AccessPathMapTenantId)) { $AccessPathParams.TenantId = $AccessPathMapTenantId }
        if (-not [string]::IsNullOrWhiteSpace($AccessPathMapImportPath)) { $AccessPathParams.ImportPath = $AccessPathMapImportPath }
        if (-not [string]::IsNullOrWhiteSpace($AccessPathMapAppRoot)) { $AccessPathParams.AppRoot = $AccessPathMapAppRoot }
        if ($null -ne $AccessPathMapResolveObjectIdsOutsidePrivilegedEAM) { $AccessPathParams.ResolveObjectIdsOutsidePrivilegedEAM = $AccessPathMapResolveObjectIdsOutsidePrivilegedEAM }
        if ($PassThru) { $AccessPathParams.PassThru = $true }

        Write-Verbose "Generating Access Path Map data..."
        $AccessPathResult = New-EntraOpsAccessPathMapData @AccessPathParams
        $results.Add([pscustomobject]@{ App = 'AccessPathMap'; Result = $AccessPathResult })
    }

    if (-not $SkipAccessPackageFlow -and ((Test-Path -LiteralPath $TgSnapshotFolder -PathType Container) -or $TgEnabledInConfig)) {
        $AccessPackageFlowParams = @{
            RepoRoot = $EntraOpsRoot
            Verbose  = $VerbosePreference -eq 'Continue'
            WhatIf   = $WhatIfPreference
            Confirm  = $false
        }
        if (-not [string]::IsNullOrWhiteSpace($ConfigurationAnalyzerImportPath)) { $AccessPackageFlowParams.SnapshotPath = $ConfigurationAnalyzerImportPath }
        if ($PassThru) { $AccessPackageFlowParams.PassThru = $true }

        Write-Verbose "Generating Access Package Flow enrichment data..."
        try {
            $AccessPackageFlowResult = New-EntraOpsAccessPackageFlowData @AccessPackageFlowParams
            $results.Add([pscustomobject]@{ App = 'AccessPackageFlow'; Result = $AccessPackageFlowResult })
        } catch {
            if ($FailureAction -eq 'Stop') { throw }
            Write-Warning "Skipped Access Package Flow enrichment: $($_.Exception.Message)"
        }
    }

    if (-not $SkipClassificationExplorer) {
        $ClassificationExplorerParams = @{
            EntraOpsRoot = $EntraOpsRoot
            Verbose      = $VerbosePreference -eq 'Continue'
            WhatIf       = $WhatIfPreference
            Confirm      = $false
        }
        if (-not [string]::IsNullOrWhiteSpace($ClassificationExplorerRepoRoot)) { $ClassificationExplorerParams.RepoRoot = $ClassificationExplorerRepoRoot }
        if (-not [string]::IsNullOrWhiteSpace($ClassificationExplorerAppRoot)) { $ClassificationExplorerParams.AppRoot = $ClassificationExplorerAppRoot }
        if ($PSBoundParameters.ContainsKey('SkipExplorerHistory')) { $ClassificationExplorerParams.SkipHistory = $SkipExplorerHistory }
        if ($PassThru) { $ClassificationExplorerParams.PassThru = $true }

        Write-Verbose "Generating Classification Explorer data..."
        $ClassificationExplorerResult = New-EntraOpsClassificationExplorerData @ClassificationExplorerParams
        $results.Add([pscustomobject]@{ App = 'ClassificationExplorer'; Result = $ClassificationExplorerResult })
    }

    if ($results.Count -eq 0) {
        Write-Host "No EntraOps Reporting data was generated (all apps skipped)."
    } else {
        Write-Host "Generated EntraOps Reporting data for: $(($results.App) -join ', ')."
    }

    if ($PassThru) { $results }
}

# SIG # Begin signature block
# MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB+Fi94AsQ/LM9V
# vTX2aoqflQtWdR+Ain+t0mJyNbWLMqCCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j
# 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
# ARUwLwYJKoZIhvcNAQkEMSIEIHJ+VWsbLAvIGq0Uf+3ZNst7fSVeG2LqhGdye+N4
# vS7zMA0GCSqGSIb3DQEBAQUABIIBgJceknXNYB+qEKBh7NnxflHcR3tcazwY8Q2P
# yrylWJDpVmMfoYiKTQH++6r6KkQOMTt4M4CNcdSo9LvvP4bwjdXxvOoytApIr5Bl
# VG/kwcSHY+iChKqyt5J0W9kxeUYHPcP/WnoCRoyHcVTuHUAmGyb3Q9ZcZFS2w35B
# lfCN/RY6pXzlVZEddUfYIXHSyI/3OpiwCWbr+LUdvvhlaGuqcxw5hxZ1vah+mlT9
# 7qcQpk4Ki2OMGKG2fvdF8CNQGu2W3+oeIglA5xlg5tSDHXClaIscTuR2koskd5KS
# a+FGQa9xlJtoXWSs3bWnXhCCDPAgJl7ZLhH0mMvx+QBueI7yTkWnavoZPvRBoQAQ
# GTNzYo/jPyWYmyEx3EBkJTMY1DWpJItxL76CxHbYaOZDYdICF2vjcScEaLz2ELhW
# XxX60JuIOqU69QQ1EkGdoZCgNe4Sz4ya4KtdhJiOClp8xriP8f3orbwcBLNg7YND
# uBKZoeIw9VmstSg+UV8TChyeEu3AsaGCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID
# 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0
# ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ
# KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD
# MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMTMwOTQxNTVaMDcG
# CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY
# 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDBriQV/i4CfJY1ZiQiinbZ2ajqB5ZINKwuS
# YIkiDpnBPQvzLHOrUe/fUf01VeLYIzcwgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ
# MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh
# MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0
# dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI
# hvcNAQEBBQAEggIAt9xPcUm69nY0vZbhH98iS87SKmyYWJk90169o7pYlwaoEknY
# +f42GW6RywWmS85NzWM8wuIo9qe8SuF+LD7SD7RWnPEm7cqPxto1Rq1QZKFAaaCc
# Z9nNr+IaHarM08Aa5PiOzOnmzx/TXQdUkQe619flls13ucu6/yBh0eU6O50vJe53
# I4VKjlo12dm55l2SahlYjjJKcmDeoL2u3aJ+ZvN2Zc2yILARHTcUJpZ4sSxH46mi
# tn7bJU/k+mg2OCaStTKeleclhgwPNP7wUsvAFH99cVqjrZhQib3qXlhhoPhT7Rsb
# /nkYhdZrg3xtmIx5cdmDO0+JG0pxChVgyE4MxhPIra2tPrVR4p+ZPdEOx/C4DZP6
# K9Zimb719XUAIQ6ST+RnIrcHUXvomUW7Ic2Ghm8St2AP7NM4shWD0RkvmjmcmnuE
# dqOiVT1GYYujj92bi+AIWNtXuJ9/5CET1aqQX2J2Z3ykgRc1vLNbCKbQ28Nv8EdM
# Ybw/ZDhBJWx8k0P2WEWzufUJJYWGCSJDmjOpGO//hvsYJGXXqy3DOTOrqxD5D635
# dHIFxLPXdG9Isfl/lgaWs5pYP5hLIeNWh14baWoy3mPFRVpKyL8s5Ovcz/ptBz4J
# 0EDQEgTUSYMExddssLz+bPTqmkW8H5/2jaVVJ/MhPxovPeDfTKwAOsHbTEk=
# SIG # End signature block