Modules/Permissions/PermissionsHelper.psm1

Import-Module (Join-Path -Path $PSScriptRoot -ChildPath '../Utility/Utility.psm1') -Function Get-ScubaGearCatalog

Function Get-ScubaGearPermissions {
    <#
    .SYNOPSIS
        Retrieves the Graph/REST permission scopes required by ScubaGear.
 
    .DESCRIPTION
        Returns the leastPermissions (or higherPermissions) scopes for a product or a specific
        cmdlet. Base URLs, Graph request URIs, OAuth scopes, service principal roles and API
        headers are produced by the dedicated Get-ScubaGear* functions (Get-ScubaGearServiceEndpoint,
        Get-ScubaGearGraphEndpoint, Get-ScubaGearOAuthScope, Get-ScubaGearServicePrincipalRole,
        Get-ScubaGearApiHeader, Get-ScubaGearResourceAppId).
 
    .PARAMETER CmdletName
        The name of the cmdlet for which the permissions are to be retrieved.
 
    .PARAMETER PermissionLevel
        The level of permission to be retrieved. The possible values are 'least' and 'higher'. Default is 'least'.
 
    .PARAMETER ServicePrincipal
        The switch to indicate that the permissions are to be retrieved for a service principal.
 
    .PARAMETER Product
        The product for which the permissions are to be retrieved. Options are 'aad', 'exo', 'securitysuite', 'teams', 'sharepoint', 'powerplatform'. Can be an array of products and used in pipeline. 'securitysuite' replaces 'defender'.
 
    .PARAMETER Environment
        The Environment for which the permissions are to be retrieved. Options are 'commercial', 'gcc', 'gcchigh', 'dod'. Default is 'commercial'.
 
    .EXAMPLE
        Get-ScubaGearPermissions -CmdletName Get-MgBetaDirectorySetting
 
    .EXAMPLE
        Get-ScubaGearPermissions -CmdletName Get-MgBetaDirectorySetting -PermissionLevel higher
 
    .EXAMPLE
        Get-ScubaGearPermissions -Product aad
        Get-ScubaGearPermissions -Product exo
        Get-ScubaGearPermissions -Product securitysuite
 
    .EXAMPLE
        Get-ScubaGearPermissions -Product aad -servicePrincipal
 
    .EXAMPLE
        'aad','scubatank' | Get-ScubaGearPermissions
 
    .NOTES
        NAME: Get-ScubaGearPermissions
        VERSION: 3.0
 
        USE TO FIND PERMS:
            (Find-MgGraphCommand -Command Get-MgBetaPolicyRoleManagementPolicyAssignment).Permissions | Select Name, IsLeastPrivilege
            Find-MgGraphPermission -All
 
        CHANGELOG:
        2024-10-03 - Initial version
        2024-12-20 - Added pipeline and multi-product support
        2026-06-19 - Added SecuritySuite and removed ScubaTank to product list
        2026-10-07 - Split -OutAs output modes into dedicated Get-ScubaGear* functions; catalog now keyed by functionName/entryType
    #>


    [CmdletBinding(DefaultParameterSetName = 'CmdletName')]
    param (
        [Parameter(Mandatory = $false, ParameterSetName = 'CmdletName')]
        [Alias('Command')]
        [string]$CmdletName,

        [Parameter(Mandatory = $false, ParameterSetName = 'ServicePrincipal')]
        [switch]$ServicePrincipal,

        [Parameter(Mandatory = $true, ParameterSetName = 'ServicePrincipal',ValueFromPipeline=$true)]
        [ValidateSet('aad', 'exo', 'securitysuite', 'teams', 'sharepoint', 'scubatank', 'powerplatform', 'teamsunified', '*')]
        [string[]]$Product,

        [Parameter(Mandatory = $false)]
        [ValidateSet('least', 'higher')]
        [Alias('PermissionType')]
        [string]$PermissionLevel = 'least',

        [Parameter(Mandatory = $false)]
        [ValidateSet('commercial', 'gcc', 'gcchigh', 'dod')]
        [string]$Environment = 'commercial'
    )
    Begin{
        $ErrorActionPreference = 'Stop'

        # If ProductName is * then set Product to all possible values
        if ($Product -contains '*') {
            $Product = @('aad', 'exo', 'securitysuite', 'teams', 'sharepoint', 'powerplatform')
        }

        # Permission records are every entry except the restHelper path-only records (those carry no grantable permission of their own).
        $permissionSet = Get-ScubaGearCatalog | Where-Object { $_.entryType -ne 'restHelper' }
        Write-Verbose "Command: `$permissionSet = Get-ScubaGearCatalog | Where-Object { `$_.entryType -ne 'restHelper' }"

        # This hashtable contains the Entra AppId values for MS Graph (aad), Office 365 Exchange Online and SharePoint.
        # The New-ScubaGearServicePrincipal cmdlet references these AppIds and their respective permissions from ScubaGearApiCatalog.json to
        # create the service principal permissions in the tenant.
        $ResourceAPIHash = @{
            'aad'        = '00000003-0000-0000-c000-000000000000'
            'exo'        = @(
                '00000002-0000-0ff1-ce00-000000000000',
                '00000007-0000-0ff1-ce00-000000000000'
            )
            'securitysuite' = @(
                '00000002-0000-0ff1-ce00-000000000000',
                '00000007-0000-0ff1-ce00-000000000000'
            )
            'sharepoint' = '00000003-0000-0ff1-ce00-000000000000'
            'teams' = '48ac35b8-9aa8-4d74-927d-1f4a14a0b239'
        }

        # Start with an empty array to build the filter
        $conditions = @()
        $conditionsmsg = @()
        $output = @()
    }
    Process{

        switch($PSBoundParameters.Keys){
            'CmdletName' {
                $conditions += {$_.functionName -eq $CmdletName}
                $conditionsmsg += '`$_.functionName -eq "' + $CmdletName + '"'
            }

            'Product' {
                # Build OR condition for products - item should match ANY of the specified products
                $productCondition = {
                    $item = $_
                    $matchFound = $false
                    foreach($prod in $Product) {
                        if ($item.scubaGearProduct -contains $prod) {
                            $matchFound = $true
                            break
                        }
                    }
                    return $matchFound
                }
                $conditions += $productCondition
                $conditionsmsg += '`$_.scubaGearProduct -contains any of "' + ($Product -join '", "') + '"'

                If($true){
                    Foreach($ProductItem in $Product){
                        If($ServicePrincipal -or $ProductItem -eq 'aad'){
                            # When running non-interactive, fetch the permissions associated with with the product's REST API record in the JSON (e.g. "Teams admin REST API").
                            # These are the permissions that a service principal needs and are listed in /docs/prerequisites/noninteractive.md for the specific product.
                            # aad is the only exception. For aad for both interactive and non-interactive we need to fetch the Graph permissions.
                            $conditions += {$_.resourceAPIAppId -match ($ResourceAPIHash[$ProductItem] -join '|')}
                            $conditionsmsg += '`$_.resourceAPIAppId -match "' + ($ResourceAPIHash[$ProductItem] -Join '|') + '"'
                        }
                        else{
                            # When running interactive, the permissions returned are only the minimum necessary to run
                            # the MS Graph API /organization to get information about the tenant which is called by all of the products.
                            # This logic fetches the permissions from the /beta/organization record in the JSON.
                            $conditions += {$_.resourceAPIAppId -notmatch ($ResourceAPIHash[$ProductItem] -join '|')}
                            $conditionsmsg += '`$_.resourceAPIAppId -notmatch "' + ($ResourceAPIHash[$ProductItem] -join '|') + '"'
                        }
                    }
                }
            }
        }

        foreach ($EnvironmentItem in $Environment) {
            $conditions += {$_.supportedEnv -contains $EnvironmentItem }
            $conditionsmsg += '`$_.supportedEnv -contains "' + $EnvironmentItem + '"'
        }

        #write a verbose statement where the values are expanded in the $conditions
        # the $_ causes the join to fail, so we need to replace it with another character
        $filterCondition = $conditionsmsg -join '' -replace '`', ' -and ' -replace '^\s+(-and)\s+', ''

        # Correct verbose message with escaped braces
        Write-Verbose -Message ("Command: `$collection = `$permissionSet | Where-Object {{ {0} }}" -f $filterCondition)

        # Combine the conditions into a single script block
        $filterScript = {
            $result = $true
            foreach ($condition in $conditions) {
                $result = $result -and (&$condition)
            }
            return $result
        }

        $collection = $permissionSet | Where-Object $filterScript

        # This function now returns only permission scopes. The former -OutAs modes (endpoint, api,
        # apiHeader, oauthScope, role, appId) each have a dedicated Get-ScubaGear* function.
        # The Graph connect record (graphConnect) carries only the auth-time User.Read scope, so it is excluded.
        If($PermissionLevel -eq 'least'){
            Write-Verbose -Message "Command: `$collection | Where-Object {`$_.entryType -ne 'graphConnect'} | Select-Object -ExpandProperty leastPermissions -Unique"
            $output += $collection | Where-Object {$_.entryType -ne 'graphConnect'} | Select-Object -ExpandProperty leastPermissions -Unique
        }
        else{
            Write-Verbose -Message "Command: `$collection | Where-Object {`$_.entryType -ne 'graphConnect'} | Select-Object -ExpandProperty higherPermissions -Unique"
            $output += $collection | Where-Object {$_.entryType -ne 'graphConnect'} | Select-Object -ExpandProperty higherPermissions -Unique
        }
    }
    End{
        return $output | Sort-Object
    }
}

Function Get-ScubaGearEntraMinimumPermissions{
    <#
    .SYNOPSIS
        This Function is used to retrieve the redundant permissions of the SCuBAGear module
 
    .DESCRIPTION
        This Function is used to retrieve the redundant permissions of the SCuBAGear module for aad only
 
    .PARAMETER Environment
        The Environment for which the permissions are to be retrieved. Options are 'commercial', 'gcc', 'gcchigh', 'dod'. Default is 'commercial'
 
    .EXAMPLE
        Get-ScubaGearEntraMinimumPermissions
    #>


    param(
        [Parameter(Mandatory = $false)]
        [ValidateSet('commercial', 'gcc', 'gcchigh', 'dod')]
        [string]$Environment = 'commercial'
    )

    # Create a list to hold the filtered permissions
    $filteredPermissions = @()

    # get all modules with least and higher permissions
    $allPermissions = Get-ScubaGearProductRecord -Product aad -Environment $Environment

    # Compare the permissions to find the redundant ones
    $comparedPermissions = Compare-Object $allPermissions.leastPermissions $allPermissions.higherPermissions -IncludeEqual

    # filter to get the higher overwriting permissions
    $OverwriteHigherPermissions = $comparedPermissions | Where-Object {$_.SideIndicator -eq "=="} | Select-Object -ExpandProperty InputObject -Unique

    # loop thru each module and grab the least permissions unless the higher permissions is one from the $overriteHigherPermissions
    # Don't include the least permissions that are overwriten by the higher permissions
    foreach($permission in $allPermissions){
        if( (Compare-Object $permission.higherPermissions -DifferenceObject $OverwriteHigherPermissions -IncludeEqual).SideIndicator -notcontains "=="){
            $filteredPermissions += $permission
        }
    }

    $NewPermissions = @()
    # Build a new list of permissions that includes the least permissions and the higher permissions that overwrite them

    $NewPermissions += $filteredPermissions | Select-Object -ExpandProperty leastPermissions -Unique

    # include overwrite higher permissions
    $NewPermissions += $OverwriteHigherPermissions
    $NewPermissions = $NewPermissions | Sort-Object -Unique

    # Display the filtered permissions
    return $NewPermissions
}

Function Get-ServicePrincipalPermissions {
    <#
    .SYNOPSIS
        This Function is used to retrieve the permissions that are needed for running ScubaGear with a service principal
 
    .DESCRIPTION
        This Function is used to retrieve the permissions of the SCuBAGear module for service principals. It also accounts for the minimum permissions required for Entra roles which may overwrite least permissions.
 
    .PARAMETER Environment
 
        The Environment for which the permissions are to be retrieved. Options are 'commercial', 'gcc', 'gcchigh', 'dod'. Default is 'commercial'
        Different environments may have different minimum permissions, I.E. GCCHigh
 
    .EXAMPLE
        Get-ServicePrincipalPermissions
    #>


    param(
        [Parameter(Mandatory = $false)]
        [ValidateSet('commercial', 'gcc', 'gcchigh', 'dod')]
        [string]$Environment = 'commercial'
    )

    $ProductNames = "aad", "exo", "sharepoint", "securitysuite"

    # Create a list to hold the filtered permissions
    $filteredPermissions = @()

    # get all modules with least and higher permissions
    $allPermissions = Get-ScubaGearProductRecord -Product $ProductNames -Environment $Environment

    # Only get overwrite higher permissions if AAD is in the product list
    if ($ProductNames -contains 'aad') {
        $OverwriteHigherPermissions = Get-ScubaGearEntraMinimumPermissions -Environment $Environment
    } else {
        $OverwriteHigherPermissions = @()
    }

    # if the ServicePrincipal switch is used, then add the appropriate resourceAPIAppId to $OverwriteHigherPermissions from line 356 by looking at the $allPermissions
    $newOverwriteHigherPermissions = @()
    if ($OverwriteHigherPermissions.Count -gt 0) {
        ForEach($permission in $OverwriteHigherPermissions) {
            $resourceAPIAppId = ($allPermissions | Where-Object { $_.leastPermissions -contains $permission } | Select-Object -ExpandProperty resourceAPIAppId -Unique)

            # Get the scubaGearProduct for this permission - combine all products that use this permission
            $products = ($allPermissions | Where-Object { $_.leastPermissions -contains $permission } | Select-Object -ExpandProperty scubaGearProduct) | Sort-Object -Unique

            $newObject = [PSCustomObject]@{
                resourceAPIAppId   = $resourceAPIAppId
                leastPermissions   = $permission
                scubaGearProduct   = $products  # This will be an array of all products
            }
            $newOverwriteHigherPermissions += $newObject
        }
    }

    # loop thru each module and grab the least permissions unless the higher permissions is one from the $overriteHigherPermissions
    # Don't include the least permissions that are overwriten by the higher permissions
    foreach($permission in $allPermissions){
        if ($OverwriteHigherPermissions.Count -eq 0 -or
            (Compare-Object $permission.higherPermissions -DifferenceObject $OverwriteHigherPermissions -IncludeEqual).SideIndicator -notcontains "=="){
            $filteredPermissions += $permission
        }
    }

    $NewPermissions = @()
    # Build a new list of permissions that includes the least permissions and the higher permissions that overwrite them

    $NewPermissions += $filteredPermissions

    # include overwrite higher permissions only if they exist
    if ($newOverwriteHigherPermissions.Count -gt 0) {
        $NewPermissions += $newOverwriteHigherPermissions
    }

    # Group by permission name and resourceAPIAppId to combine duplicate entries with different products
    $groupedPermissions = $NewPermissions | Group-Object -Property @{Expression={$_.leastPermissions}}, @{Expression={$_.resourceAPIAppId}}

    $deduplicatedPermissions = @()
    foreach ($group in $groupedPermissions) {
        # Combine all products from duplicate entries
        $allProducts = @()
        foreach ($item in $group.Group) {
            if ($item.scubaGearProduct) {
                # Handle both arrays and single values
                if ($item.scubaGearProduct -is [array]) {
                    $allProducts += $item.scubaGearProduct
                } else {
                    $allProducts += $item.scubaGearProduct
                }
            }
        }

        # Get unique products
        $uniqueProducts = $allProducts | Sort-Object -Unique

        # Take the first item and update its scubaGearProduct with all unique products
        $consolidatedItem = $group.Group[0].PSObject.Copy()
        $consolidatedItem.scubaGearProduct = $uniqueProducts

        $deduplicatedPermissions += $consolidatedItem
    }

    # Display the filtered permissions - return deduplicated results
    return $deduplicatedPermissions | Select-Object -Property LeastPermissions, ResourceAPIAppID, scubaGearProduct -Unique
}

Export-ModuleMember -Function Get-ScubaGearPermissions, Get-ScubaGearEntraMinimumPermissions, Get-ServicePrincipalPermissions
function Get-ScubaGearProductRecord {
    <#
    .SYNOPSIS
        Returns the full catalog records for the given product(s) and environment.
    .DESCRIPTION
        Replaces the former 'Get-ScubaGearPermissions -OutAs all' usage. restHelper path-only
        records are excluded automatically because they carry no supportedEnv.
    .FUNCTIONALITY
        Internal
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string[]]$Product,

        [Parameter(Mandatory = $false)]
        [ValidateSet('commercial', 'gcc', 'gcchigh', 'dod')]
        [string]$Environment = 'commercial'
    )
    Get-ScubaGearCatalog | Where-Object {
        $item = $_
        ($item.entryType -ne 'restHelper') -and
        ($Product | Where-Object { $item.scubaGearProduct -contains $_ }) -and
        ($item.supportedEnv -contains $Environment)
    }
}

function Get-ScubaGearServicePrincipalRole {
    <#
    .SYNOPSIS
        Returns the Entra directory role(s) a service principal needs for the given product(s).
    .DESCRIPTION
        Replaces the former 'Get-ScubaGearPermissions -OutAs role'.
    .FUNCTIONALITY
        Internal
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string[]]$Product,

        [Parameter(Mandatory = $false)]
        [ValidateSet('commercial', 'gcc', 'gcchigh', 'dod')]
        [string]$Environment = 'commercial'
    )
    process {
        Get-ScubaGearCatalog | Where-Object {
            $item = $_
            ($item.entryType -ne 'restHelper') -and
            ($Product | Where-Object { $item.scubaGearProduct -contains $_ }) -and
            ($item.supportedEnv -contains $Environment) -and
            (@($item.spRolePermissions).Count -gt 0)
        } | Select-Object -ExpandProperty spRolePermissions -Unique
    }
}

function Get-ScubaGearResourceAppId {
    <#
    .SYNOPSIS
        Returns the resource API application ID(s) for the given product(s)/environment.
    .DESCRIPTION
        Replaces the former 'Get-ScubaGearPermissions -OutAs appId'.
    .FUNCTIONALITY
        Internal
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string[]]$Product,

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

        [Parameter(Mandatory = $false)]
        [ValidateSet('commercial', 'gcc', 'gcchigh', 'dod')]
        [string]$Environment = 'commercial'
    )
    process {
        # -ServicePrincipal is accepted so callers can pass it, but the app IDs do not depend on the auth mode.
        $null = $ServicePrincipal
        Get-ScubaGearCatalog | Where-Object {
            $item = $_
            ($item.entryType -ne 'restHelper') -and
            ($Product | Where-Object { $item.scubaGearProduct -contains $_ }) -and
            ($item.supportedEnv -contains $Environment)
        } | Select-Object -ExpandProperty resourceAPIAppId -Unique | ForEach-Object { ($_ -split '#')[0] }
    }
}

function Get-ScubaGearEndpointRest {
    <#
    .SYNOPSIS
        Lists the REST API hosts that ScubaGear connects to, for building firewall or proxy allow lists.
 
    .DESCRIPTION
        Returns the service hosts ScubaGear calls to assess the selected products in the selected
        Microsoft 365 environment: Microsoft Graph plus each product's admin API. The list is read
        from ScubaGearApiCatalog.json, so it reflects the version of ScubaGear that is installed.
 
        Sign-in (Entra ID) hosts and hosts used only to install or update ScubaGear are not included.
        All connections use HTTPS on port 443.
 
        SharePoint admin hosts include the tenant name. Pass -Domain to get the real host name;
        otherwise the host is shown with a <tenant> placeholder.
 
    .PARAMETER ProductNames
        The products to include. Accepts the same values as Invoke-SCuBA. 'defender' has been replaced by
        'securitysuite', and '*' (the default) selects every product ScubaGear tests.
 
    .PARAMETER M365Environment
        The Microsoft 365 environment: 'commercial' (default), 'gcc', 'gcchigh' or 'dod'.
 
    .PARAMETER Domain
        The tenant name used in SharePoint admin host names, for example 'contoso' for contoso.onmicrosoft.com.
 
    .PARAMETER Format
        'Object' (default) returns one object per host and product. 'Hosts' returns the unique host names,
        one per line. 'Csv', 'Json' and 'Markdown' return text that can be pasted into a ticket or document.
 
    .PARAMETER OutFile
        Writes the output to this file as UTF-8 without a byte order mark. With -Format Object the file
        contains the 'Hosts' format.
 
    .PARAMETER Clipboard
        Copies the output to the clipboard. With -Format Object the clipboard receives the 'Hosts' format.
 
    .EXAMPLE
        Get-ScubaGearEndpointRest
 
        Lists the hosts for all products in a commercial tenant.
 
    .EXAMPLE
        Get-ScubaGearEndpointRest -ProductNames exo, teams -M365Environment gcchigh -Format Hosts
 
    .EXAMPLE
        Get-ScubaGearEndpointRest -M365Environment gcc -Domain contoso -Format Markdown -OutFile .\scubagear-hosts.md
 
    .FUNCTIONALITY
        Public
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject], [string])]
    param(
        [Parameter(Mandatory = $false)]
        [ValidateSet('teams', 'exo', 'securitysuite', 'aad', 'powerplatform', 'sharepoint', 'powerbi', '*', IgnoreCase = $false)]
        [string[]]$ProductNames = '*',

        [Parameter(Mandatory = $false)]
        [ValidateSet('commercial', 'gcc', 'gcchigh', 'dod')]
        [string]$M365Environment = 'commercial',

        [Parameter(Mandatory = $false)]
        [ValidatePattern('^[A-Za-z0-9-]+$')]
        [string]$Domain,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Object', 'Hosts', 'Csv', 'Json', 'Markdown')]
        [string]$Format = 'Object',

        [Parameter(Mandatory = $false)]
        [string]$OutFile,

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

    $AllProducts = 'aad', 'securitysuite', 'exo', 'powerplatform', 'sharepoint', 'teams', 'powerbi'
    $Products = if ($ProductNames -contains '*') { $AllProducts } else { $ProductNames }
    $Products = @($Products | Sort-Object -Unique)
    
    # Teams also reads its tenant settings from the unified settings host, which the catalog lists as its own product.
    $CatalogProducts = @($Products | ForEach-Object { $_; if ($_ -eq 'teams') { 'teamsunified' } })

    $Catalog = @(Get-ScubaGearCatalog)

    # Every product's data comes from Microsoft Graph, so its host is needed whenever the catalog
    # has Graph resources for a selected product in this environment.
    $UsesGraph = [bool]($Catalog | Where-Object {
            $Item = $_
            $Item.entryType -eq 'graphResource' -and
            $Item.supportedEnv -contains $M365Environment -and
            ($Products | Where-Object { $Item.scubaGearProduct -contains $_ })
        })

    $Inventory = foreach ($Entry in $Catalog) {
        if ($Entry.entryType -notin 'graphConnect', 'restBase' -or $Entry.supportedEnv -notcontains $M365Environment) { continue }

        $EntryProducts = @($CatalogProducts | Where-Object { $Entry.scubaGearProduct -contains $_ })
        if ($Entry.entryType -eq 'graphConnect') {
            if (-not $UsesGraph) { continue }
            $Label = 'Microsoft Graph'
            $EntryProducts = $Products
        }
        elseif ($EntryProducts.Count -eq 0) { continue }
        else {
            # Purpose is the API named in the catalog note, e.g. "SharePoint Admin API".
            $Sentence = (([string]$Entry.notes) -split '(?<=\.)\s', 2)[0].Trim()
            $Label = if ($Sentence -match 'calls to the (.+?)\.?$') { $Matches[1] } else { $Sentence }
            # The unified settings host is listed under its own catalog product but belongs to Teams.
            $EntryProducts = @($EntryProducts -replace '^teamsunified$', 'teams')
        }

        $Placeholder = $Entry.endpointPath -match '\{domain\}'
        $BaseUrl = $Entry.endpointPath -replace '\{domain\}', $(if ($Domain) { $Domain } else { '<tenant>' })
        $HostName = ($BaseUrl -replace '^https?://', '') -replace '/.*$', ''

        [PSCustomObject]@{
            Host        = $HostName
            Port        = 443
            BaseUrl     = $BaseUrl
            Products    = ($EntryProducts | Sort-Object -Unique) -join ', '
            Environment = $M365Environment
            Purpose     = $Label
            Note        = $(if ($Placeholder -and -not $Domain) { 'Replace <tenant> with your tenant name, or pass -Domain.' } else { '' })
        }
    }
    $Inventory = @($Inventory | Sort-Object -Property Host, Products -Unique)

    $TextFormat = if ($Format -eq 'Object') { 'Hosts' } else { $Format }
    $Text = switch ($TextFormat) {
        'Hosts' { (@($Inventory.Host | Sort-Object -Unique)) -join [Environment]::NewLine }
        'Csv' { ($Inventory | ConvertTo-Csv -NoTypeInformation) -join [Environment]::NewLine }
        'Json' { ConvertTo-Json -InputObject @($Inventory) }
        'Markdown' {
            $Escape = { param($Value) ([string]$Value) -replace '\|', '\|' }
            $Lines = @('| Host | Port | Products | Purpose | Note |', '| --- | --- | --- | --- | --- |')
            $Lines += foreach ($Row in $Inventory) {
                "| $(& $Escape $Row.Host) | $($Row.Port) | $(& $Escape $Row.Products) | $(& $Escape $Row.Purpose) | $(& $Escape $Row.Note) |"
            }
            $Lines -join [Environment]::NewLine
        }
    }

    if ($OutFile) {
        $Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutFile)
        [System.IO.File]::WriteAllText($Path, $Text + [Environment]::NewLine, (New-Object System.Text.UTF8Encoding($false)))
    }
    if ($Clipboard) {
        if (Get-Command -Name Set-Clipboard -ErrorAction SilentlyContinue) { Set-Clipboard -Value $Text }
        else { Write-Warning 'Set-Clipboard is not available on this system; the clipboard was not changed.' }
    }

    if ($Format -eq 'Object') { return $Inventory }
    return $Text
}

Export-ModuleMember -Function Get-ScubaGearPermissions, Get-ScubaGearEntraMinimumPermissions, Get-ServicePrincipalPermissions, Get-ScubaGearServicePrincipalRole, Get-ScubaGearResourceAppId, Get-ScubaGearProductRecord, Get-ScubaGearEndpointRest

# SIG # Begin signature block
# MIIu9wYJKoZIhvcNAQcCoIIu6DCCLuQCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCkrXJA8omI10RS
# Zabfss55CF9LgWuu2af8tZi6ZiVsYqCCE6MwggWQMIIDeKADAgECAhAFmxtXno4h
# MuI5B72nd3VcMA0GCSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNV
# BAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0z
# ODAxMTUxMjAwMDBaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0
# IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
# AL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/z
# G6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZ
# anMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7s
# Wxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL
# 2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfb
# BHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3
# JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3c
# AORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqx
# YxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0
# viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aL
# T8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1Ud
# EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzf
# Lmc/57qYrhwPTzANBgkqhkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNk
# aA9Wz3eucPn9mkqZucl4XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjS
# PMFDQK4dUPVS/JA7u5iZaWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK
# 7VB6fWIhCoDIc2bRoAVgX+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eB
# cg3AFDLvMFkuruBx8lbkapdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp
# 5aPNoiBB19GcZNnqJqGLFNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msg
# dDDS4Dk0EIUhFQEI6FUy3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vri
# RbgjU2wGb2dVf0a1TD9uKFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ7
# 9ARj6e/CVABRoIoqyc54zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5
# nLGbsQAe79APT0JsyQq87kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3
# i0objwG2J5VT6LaJbVu8aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0H
# EEcRrYc9B9F1vM/zZn4wggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0G
# CSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0
# IFRydXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTla
# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE
# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz
# ODQgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C
# 0CiteLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce
# 2vnS1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0da
# E6ZMswEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6T
# SXBCMo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoA
# FdE3/hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7Oh
# D26jq22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM
# 1bL5OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z
# 8ujo7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05
# huzUtw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNY
# mtwmKwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP
# /2NPTLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0T
# AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYD
# VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG
# A1UdJQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV
# HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU
# cnVzdGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATAN
# BgkqhkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95Ry
# sQDKr2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HL
# IvdaqpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5Btf
# Q/g+lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnh
# OE7abrs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIh
# dXNSy0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV
# 9zeKiwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/j
# wVYbKyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYH
# Ki8QxAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmC
# XBVmzGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l
# /aCnHwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZW
# eE4wggdXMIIFP6ADAgECAhAMM6tnPejLgA9WVhXroQvSMA0GCSqGSIb3DQEBCwUA
# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE
# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz
# ODQgMjAyMSBDQTEwHhcNMjYwMTE0MDAwMDAwWhcNMjcwMTEzMjM1OTU5WjBfMQsw
# CQYDVQQGEwJVUzEdMBsGA1UECBMURGlzdHJpY3Qgb2YgQ29sdW1iaWExEzARBgNV
# BAcTCldhc2hpbmd0b24xDTALBgNVBAoTBENJU0ExDTALBgNVBAMTBENJU0EwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCuXYolNHqlh6smLTE592waXheZ
# 8VHzxeds4pMaepGuwmjf8d1jG9wUNuJX9/qb0a1dgGz5D/EAz5NRTIin4SZYQEE8
# qvdl2yQJ5uWxXIjsFbrOyc1fWscUXw0Kt7OPLOafcEkdDoe8K0tO4h2GL3RWRzjp
# uLfQhhnAmD6NT1l+ughnfmarV/ODgIn/RFR4YORlu4YP2xQX6KRxeTDslg7F+z6X
# +t87/U8m8gQ9XTm5kBmteP4GcE/ytnyI+ScIxNRybzGomWIBm848XDE5yYhlYQ2R
# SnCoo6M4CRqp9WFGVyoLkoPP0OlxzryKWaE1/nuPbYG/kf/rUB1OhqxvSSGwmNhs
# vkkjsC0Z9H5Jy6heFdoxOu/+ZQksKoP/fMvHxuCCtkIJbV8tk0oT6MQ8EJbgsWDZ
# TKhui1wxW6JIZyBOMPWoZUOouOzo2h5Cz7LBPKME5FkcUzcs47lpRlDkJco4PLcj
# wJSo4XPnx3G/2DIjNEFNyfKCWfH8uW6nJjmDBiveFZ2j0YvgdQ+7MOjQnw7R/MAD
# DTagrKl3rLV60+X2TY6/onKhCUuU3pMAjVbOwZ3PkzDLZnsEGRfm6hgp6014aXml
# t8h4nu+uC41U8vUSGHl0vqKuzvmShLmnI+Iv0l95pmnqomuCZzRDrjEoaLPx7OxL
# Dy/Id9E7yQDip4jBdQIDAQABo4ICAzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8R
# hvv+YXsIiGX0TkIwHQYDVR0OBBYEFN30sVU+fpfQQfPQWMhkO1qd+MxoMD4GA1Ud
# IAQ3MDUwMwYGZ4EMAQQBMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNl
# cnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMw
# gbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0Rp
# Z2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5j
# cmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0
# ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3JsMIGUBggrBgEF
# BQcBAQSBhzCBhDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29t
# MFwGCCsGAQUFBzAChlBodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNl
# cnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJ
# BgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQA77T42YiOx5wWPItgo+kvB+Gzb
# ZFCRHRFfTAZZNQt9o/0CqNmyA2xFklX4t5Z9VNdiIOx14AnmJdQkcRdk3vsU5gby
# jKEup7LTWtvcWrl6hQwGNt3l892BgUbPKsPBE+AriktVqn5yMSXVVzeboqsqAG8e
# Syei0B54/QdgR4whfHvQ/qpCACsJTlJgAykXVgDPJNKnQ7wc17loLQutqF1JUcbO
# XKWt1AA9Zas5q30LDZzZeK4B56yojK68CTQXN7toSRFIuZDMKKDfIZpCX8cmbaaO
# DFOOu44/QWMv+Xc6+ISYGkrTTzqWhOqiXgLVBeXGn/WrOJJ8R29mZMneCpBesCLs
# YII1gCFOo7Vt6mvOKxAPQ3KhJYBFEHkp+GI65koaQkO2xv50iLS0+/j2YC66uviU
# MFe0JEOdXuE7Rn/OmWNSzQ+6kPNYDJQASQ974C3wUejJoMtGZEzoTbly/HufQTrd
# rhcL2aC69CxSN+idTXPLC9UT3xo4sFdOw+hXkmbXtoB1GDsd5p1TWFgRbnTXDkbM
# YMBWYVB6/Tk1bzwj4iTp4g0YrtB628FXPX/ko+JWZlv0Ea865S23w1uGlnDNVxIi
# +8oi74G5DM66Q6ENt6+3WoRGRrdoyE6uCh1haY+oPYSgumb0ozzrp8tw89TRKVrK
# KSXGBxlExEvjQ6dYwjGCGqowghqmAgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNV
# BAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0
# IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENBMQIQDDOrZz3oy4AP
# VlYV66EL0jANBglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgACh
# AoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAM
# BgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCAj6lutEMiTMrZsJRAFX7Q161Jx
# s+gp0S8WYKfXfXyApTANBgkqhkiG9w0BAQEFAASCAgAut2AUwOntH1hcFMXycHVH
# fNQS0j7nJsP9mtG1ruD8vmWD7sfaKnWPctxXwNPdgkjTebZsFJaCdtKQ7cZsAkjD
# uVvOOIwQYCSgvGj9zz/rvrYcHwPHgEfdkD7oI6ZttltwklXR1R/rcGwz8vtpZ1UO
# 7HtQhpJE7oQBvKROygaRw/UjCl1eA50sMG/xznyzoT6NdcrIrk9LNMM+AQQKg8I2
# Cx09P3Di/pP0OJwdhjJXB7VNBuYU/RT7aXIN0rhnyCxv3Johj/B0nPmcRNgi2jSz
# RfVsLAoJ4sm84GfM4TnsqTacq0p+a7WXbK8RymQkG6TSiM5LU7ecty30Hwq17E1W
# uZpISR8knK08ualUshrN5rGtwU03sh0ejwEUvQdHoZb2SwurvAhO1AX6qlUbzkX2
# +w/CxzHaTOIYodx/ZLk0lRZWmBr9ZjNhVGPykO/LRlQRgRZkKyt9v5pdMkuyRzQ9
# j6CeyIF3eBOu9k7QSt9VwdPK511CU0396DwwbXSg3FJLapCyzZ/4AKWuJLXZ//KQ
# OEMxpIQAhbz9nESOHit+v+zmTRyv1/yFxdnzbFMTy8afd2WcDHH+EOJJ0TvUxnpK
# 8AKpJTnOdcr5DhSV0v7Y8WnQnjJNJ+OuyZWBT1J0U1MbNKhJ4m3knuur6xx9qmap
# oxYZPLcAoqauA45vUSco6aGCF3cwghdzBgorBgEEAYI3AwMBMYIXYzCCF18GCSqG
# SIb3DQEHAqCCF1AwghdMAgEDMQ8wDQYJYIZIAWUDBAIBBQAweAYLKoZIhvcNAQkQ
# AQSgaQRnMGUCAQEGCWCGSAGG/WwHATAxMA0GCWCGSAFlAwQCAQUABCD87bEppMQj
# pwc/K9YVoO88vDmdpfs5Hv9iwIkdQEAVkAIRAKz+T+y/mVJcYwVWfc54VOIYDzIw
# MjYxMDIyMjEzMDQyWqCCEzowggbtMIIE1aADAgECAhAIT9wzT35FTtvDD4/5khg1
# MA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2Vy
# dCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBp
# bmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTEwHhcNMjYwODA1MDAwMDAwWhcNMzcx
# MTA0MjM1OTU5WjBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIElu
# Yy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFNIQTI1NiBSU0E0MDk2IFRpbWVzdGFtcCBS
# ZXNwb25kZXIgMjAyNiAxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
# tnum8sn+zUr41JtMZbP9OMYw+HwJDpG5xkIu/lqcfNYmMX81YmsUiHLbh9ykpeWB
# GKTLhYBrAN9Tdg/QEzG32XcObmgIblnr0CoQ3WSAeDZ6nH6X6VkFyYkJw3QBJREw
# vm4UhLzSxmwPA7cFKRTEOMsmEEj6qJk/dqLEAL+oQYuOwE2UuiX1Vnul8YReIyWd
# 4kgLn9gq6LNXM0UplkR6jL/QHxmb6fMoGBJYbnaUI7XD6cKDpekK2SVMld4iDbze
# HDtOaaxldH5IxuNusQ69nd8/ZXEiB5Hbxj3RlK13cX1W4DlFXKdv/CEhM8Cj1vvl
# mvhNroyPdRGbbpBlgyf8Wdu5N6ByhFwURn0U6ozlPoxN22v+fviUhP+6DR547OZn
# pBMWDfei1f5sVGwiiW/KQTWOK97g+4RJpPzPNV4VYMAwO2jM2Aty2QYPVmOQTJm0
# msuXnJrSbl2gf9JylpkJlWXqk1Q4LJsxz+TELoQCZIljbgvTJgoPU2R12ydv8i1U
# qL/adelA0y7U9Pmmtbze9Xx3rtajC5SzQd1jgfwAwsa90v9YcSPdmeoyoBBA/27c
# CL237l5DTYYPDLQ4ON3OLTGWnvRb6jDrf/T75gMRfUzSLCBQfBusm9+mSWRlC/Df
# 6S/e9Q8i13CuhzOT2Jx+V/nlbXM4QoBwlUAhelwwJT0CAwEAAaOCAZUwggGRMAwG
# A1UdEwEB/wQCMAAwHQYDVR0OBBYEFBTJY4owLtRK+26U8+bjQH717M3iMB8GA1Ud
# IwQYMBaAFO9vU0rp5AZ8esrikFb2L9RJ7MtOMA4GA1UdDwEB/wQEAwIHgDAWBgNV
# HSUBAf8EDDAKBggrBgEFBQcDCDCBlQYIKwYBBQUHAQEEgYgwgYUwJAYIKwYBBQUH
# MAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBdBggrBgEFBQcwAoZRaHR0cDov
# L2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0VGltZVN0YW1w
# aW5nUlNBNDA5NlNIQTI1NjIwMjVDQTEuY3J0MF8GA1UdHwRYMFYwVKBSoFCGTmh0
# dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFt
# cGluZ1JTQTQwOTZTSEEyNTYyMDI1Q0ExLmNybDAgBgNVHSAEGTAXMAgGBmeBDAEE
# AjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIBAI3FOmEenVIK35msCYB+
# fShAsWvSYvLBItoNdAgQ2jIqrGsVsluXMJU/+mRebBc52s6lbKAvOVPXaizmKkML
# LflEEKDZQx4CkS2t8aHPjkXha3hYZ010htFa3dhNgmalH5vuWvh3tTCf4frTS7gP
# tGc4Z/xaPhQ2AB1mR8eEe/WbH0RWHvVIl6VwQ3+g5FKNfN2N/DWJkf13w2H+2Gfq
# Efbd35Ww8CvoYBjLNIDTadcPWdgsjsiOaK/7EsKJgLjUNIVgvcaFOLLQ/GlrA+0Z
# HJoFUbOr5SJN8zykPspXIXlpDJY/gqFUZRROeab9GVgmhbdOJcD/63RhxPahFUGb
# ckRONqMe6DYAv6/mOG0pWd3cPStsdcS7buj5DyniwRY8yooMH6ptx5vpP/pZzBPB
# eZD2U4IsthyxB5Jaa8qrOkB5z160TXiM5ADMspZ0TfD9MJoq0tFpFPssKRFhWeED
# YPvcUuN7U7lvcdHl4ezQ3NT/7Ffs1sR1yh/LRbdZ3B3Vc6q2WmD8mDC0p9kzl2o7
# 3iVtS946IkEj7FkRsZGww1teYxERROC745xrtjvcw9ZyyUjHZWGRIpJeMNsPquCD
# f0fkyHtB+J4AiNZqCQk23rxh+KbpyMTNVKItJ5l92Svl20U9NbqMBOVYl1h54NEY
# LJq1/xHWFKPNK903zJZA9P2DMIIGtDCCBJygAwIBAgIQDcesVwX/IZkuQEMiDDpJ
# hjANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNl
# cnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdp
# Q2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjUwNTA3MDAwMDAwWhcNMzgwMTE0MjM1
# OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/
# BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYg
# U0hBMjU2IDIwMjUgQ0ExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
# tHgx0wqYQXK+PEbAHKx126NGaHS0URedTa2NDZS1mZaDLFTtQ2oRjzUXMmxCqvkb
# sDpz4aH+qbxeLho8I6jY3xL1IusLopuW2qftJYJaDNs1+JH7Z+QdSKWM06qchUP+
# AbdJgMQB3h2DZ0Mal5kYp77jYMVQXSZH++0trj6Ao+xh/AS7sQRuQL37QXbDhAkt
# VJMQbzIBHYJBYgzWIjk8eDrYhXDEpKk7RdoX0M980EpLtlrNyHw0Xm+nt5pnYJU3
# Gmq6bNMI1I7Gb5IBZK4ivbVCiZv7PNBYqHEpNVWC2ZQ8BbfnFRQVESYOszFI2Wv8
# 2wnJRfN20VRS3hpLgIR4hjzL0hpoYGk81coWJ+KdPvMvaB0WkE/2qHxJ0ucS638Z
# xqU14lDnki7CcoKCz6eum5A19WZQHkqUJfdkDjHkccpL6uoG8pbF0LJAQQZxst7V
# vwDDjAmSFTUms+wV/FbWBqi7fTJnjq3hj0XbQcd8hjj/q8d6ylgxCZSKi17yVp2N
# L+cnT6Toy+rN+nM8M7LnLqCrO2JP3oW//1sfuZDKiDEb1AQ8es9Xr/u6bDTnYCTK
# IsDq1BtmXUqEG1NqzJKS4kOmxkYp2WyODi7vQTCBZtVFJfVZ3j7OgWmnhFr4yUoz
# ZtqgPrHRVHhGNKlYzyjlroPxul+bgIspzOwbtmsgY1MCAwEAAaOCAV0wggFZMBIG
# A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFO9vU0rp5AZ8esrikFb2L9RJ7MtO
# MB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9PMA4GA1UdDwEB/wQEAwIB
# hjATBgNVHSUEDDAKBggrBgEFBQcDCDB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUH
# MAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDov
# L2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcnQw
# QwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lD
# ZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZI
# AYb9bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQAXzvsWgBz+Bz0RdnEwvb4LyLU0pn/N
# 0IfFiBowf0/Dm1wGc/Do7oVMY2mhXZXjDNJQa8j00DNqhCT3t+s8G0iP5kvN2n7J
# d2E4/iEIUBO41P5F448rSYJ59Ib61eoalhnd6ywFLerycvZTAz40y8S4F3/a+Z1j
# EMK/DMm/axFSgoR8n6c3nuZB9BfBwAQYK9FHaoq2e26MHvVY9gCDA/JYsq7pGdog
# P8HRtrYfctSLANEBfHU16r3J05qX3kId+ZOczgj5kjatVB+NdADVZKON/gnZruMv
# NYY2o1f4MXRJDMdTSlOLh0HCn2cQLwQCqjFbqrXuvTPSegOOzr4EWj7PtspIHBld
# NE2K9i697cvaiIo2p61Ed2p8xMJb82Yosn0z4y25xUbI7GIN/TpVfHIqQ6Ku/qjT
# Y6hc3hsXMrS+U0yy+GWqAXam4ToWd2UQ1KYT70kZjE4YtL8Pbzg0c1ugMZyZZd/B
# dHLiRu7hAWE6bTEm4XYRkA6Tl4KSFLFk43esaUeqGkH/wyW4N7OigizwJWeukcyI
# PbAvjSabnf7+Pu0VrFgoiovRDiyx3zEdmcif/sYQsfch28bZeUz2rtY/9TCA6TD8
# dC3JE3rYkrhLULy7Dc90G6e8BlqmyIjlgp2+VqsS9/wQD7yFylIz0scmbKvFoW2j
# NrbM1pD2T7m3XDCCBY0wggR1oAMCAQICEA6bGI750C3n79tQ4ghAGFowDQYJKoZI
# hvcNAQEMBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ
# MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNz
# dXJlZCBJRCBSb290IENBMB4XDTIyMDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1OVow
# YjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ
# d3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290
# IEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAv+aQc2jeu+RdSjww
# IjBpM+zCpyUuySE98orYWcLhKac9WKt2ms2uexuEDcQwH/MbpDgW61bGl20dq7J5
# 8soR0uRf1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNwwrK6dZlqczKU0RBEEC7fgvMH
# hOZ0O21x4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs06wXGXuxbGrzryc/NrDRAX7F6
# Zu53yEioZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e5TXnMcvak17cjo+A2raRmECQ
# ecN4x7axxLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtVgkEy19sEcypukQF8IUzUvK4b
# A3VdeGbZOjFEmjNAvwjXWkmkwuapoGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2mHY9
# WV1CdoeJl2l6SPDgohIbZpp0yt5LHucOY67m1O+SkjqePdwA5EUlibaaRBkrfsCU
# tNJhbesz2cXfSwQAzH0clcOP9yGyshG3u3/y1YxwLEFgqrFjGESVGnZifvaAsPvo
# ZKYz0YkH4b235kOkGLimdwHhD5QMIR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxfjT/J
# vNNBERJb5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFrb7GrhotPwtZFX50g/KEexcCP
# orF+CiaZ9eRpL5gdLfXZqbId5RsCAwEAAaOCATowggE2MA8GA1UdEwEB/wQFMAMB
# Af8wHQYDVR0OBBYEFOzX44LScV1kTN8uZz/nupiuHA9PMB8GA1UdIwQYMBaAFEXr
# oq/0ksuCMS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQEAwIBhjB5BggrBgEFBQcBAQRt
# MGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEF
# BQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJl
# ZElEUm9vdENBLmNydDBFBgNVHR8EPjA8MDqgOKA2hjRodHRwOi8vY3JsMy5kaWdp
# Y2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMBEGA1UdIAQKMAgw
# BgYEVR0gADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3v1cH
# vZqsoYcs7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy3iS8
# UgPITtAq3votVs/59PesMHqai7Je1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cnRNTn
# f+hZqPC/Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4oVaO7KTVPeix3P0c2PR3WlxU
# jG/voVA9/HYJaISfb8rbII01YBwCA8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2zm8j
# LfR+cWojayL/ErhULSd+2DrZ8LaHlv1b0VysGMNNn3O3AamfV6peKOK5lDGCA3ww
# ggN4AgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu
# MUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IFRpbWVTdGFtcGluZyBSU0E0
# MDk2IFNIQTI1NiAyMDI1IENBMQIQCE/cM09+RU7bww+P+ZIYNTANBglghkgBZQME
# AgEFAKCB0TAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkF
# MQ8XDTI2MTAyMjIxMzA0MlowKwYLKoZIhvcNAQkQAgwxHDAaMBgwFgQUUdmr2gNJ
# c9hPQmaspIJI5rNpxDkwLwYJKoZIhvcNAQkEMSIEIDLdTlQXslig/6Zju3dugg+X
# DBelvjqWGhZxw885XxYAMDcGCyqGSIb3DQEJEAIvMSgwJjAkMCIEIC2gnaf0Ex+f
# 5y22xebpyWVnVa8EPx6nQswNISDhQev8MA0GCSqGSIb3DQEBAQUABIICADAEfSnF
# UE3n63gZjSTMRWWh5wgi2ZKTXkwbfx7Q4Bex8M2qce+gEbjyVtXeCQR4V33TM4qq
# 6QB/7KFFhf19c/UNrRrI8fnHwRewt5/4gRDucRREBRqRe4j/GP7yl+K9FlzwkTK7
# D+VIXa3seLdAjjSUu7zJzsuSg5eaxAAyuvJYtiVoC3viTwhNz90nrCcYdId2Qwnd
# T8Wga/lq9twT7hCiXbaNMmkkobUmSVLKnYSvy1EEvKhoq3K9MzpaxYwFT0pQhaZX
# tdJY5+rnxxAXhU8HV8qAGbKDKgGb2a2KokmibarxQWY37SSUmhmvGXx2GJBa1/Rn
# 2MEfPchA4X2lz2NoDDoZtqfpJmmBGHLj0FLNQCO9pWTl7WEMX8K8yT49n28RM7x2
# bpsohURXi3nistY30sgYJ7ttBQjdrnowAA8d5XsWu2k+fHiakM3vwOOZOGmFnB0g
# rbjfAUNOo0nyAw5au11DKU+THsm+Rjhf3ifNiaWmCqY5bVAn2jC1h5U8u673JnaV
# xF6Q3r6jpeR1hCw1iVXnG0lplXNt28f2TsXYThsFvY2Y6CEUU7lvtCJYHp4JFh6E
# +69Xtf4INRR9BMVMeYN3VMM9QUFKCbIEYC7p6Wc5SPTx+Ht0KAj6fLftAcaagjIB
# 9cP3AEIfV4DSk4ERnRo5W58FcXPXcElMa0Lo
# SIG # End signature block