Modules/Orchestrator.psm1

using module 'ScubaConfig\ScubaConfig.psm1'

function Invoke-SCuBA {
    <#
    .SYNOPSIS
    Execute the SCuBAGear tool security baselines for specified M365 products.
    .Description
    This is the main function that runs the Providers, Rego, and Report creation all in one PowerShell script call.
    .Parameter ProductNames
    A list of one or more M365 shortened product names that the tool will assess when it is executed. Acceptable product name values are listed below.
    To assess Azure Active Directory you would enter the value aad.
    To assess Exchange Online you would enter exo and so forth.
    - Azure Active Directory: aad
    - Defender for Office 365: defender
    - Exchange Online: exo
    - MS Power Platform: powerplatform
    - SharePoint Online: sharepoint
    - MS Teams: teams.
    Use '*' to run all baselines.
    .Parameter M365Environment
    This parameter is used to authenticate to the different commercial/government environments.
    Valid values include "commercial", "gcc", "gcchigh", or "dod".
    - For M365 tenants with E3/E5 licenses enter the value **"commercial"**.
    - For M365 Government Commercial Cloud tenants with G3/G5 licenses enter the value **"gcc"**.
    - For M365 Government Commercial Cloud High tenants enter the value **"gcchigh"**.
    - For M365 Department of Defense tenants enter the value **"dod"**.
    Default value is 'commercial'.
    .Parameter OPAPath
    The folder location of the OPA Rego executable file.
    The OPA Rego executable embedded with this project is located in the project's root folder.
    If you want to execute the tool using a version of OPA Rego located in another folder,
    then customize the variable value with the full path to the alternative OPA Rego exe file.
    .Parameter LogIn
    A `$true` or `$false` variable that if set to `$true`
    will prompt you to provide credentials if you want to establish a connection
    to the specified M365 products in the **$ProductNames** variable.
    For most use cases, leave this variable to be `$true`.
    A connection is established in the current PowerShell terminal session with the first authentication.
    If you want to run another verification in the same PowerShell session simply set
    this variable to be `$false` to bypass the reauthenticating in the same session. Default is $true.
    Note: defender will ask for authentication even if this variable is set to `$false`
    .Parameter Version
    Will output the current ScubaGear version to the terminal without running this cmdlet.
    .Parameter AppID
    The application ID of the service principal that's used during certificate based
    authentication. A valid value is the GUID of the application ID (service principal).
    .Parameter CertificateThumbprint
    The thumbprint value specifies the certificate that's used for certificate base authentication.
    The underlying PowerShell modules retrieve the certificate from the user's certificate store.
    As such, a copy of the certificate must be located there.
    .Parameter Organization
    Specify the organization that's used in certificate based authentication.
    Use the tenant's tenantname.onmicrosoft.com domain for the parameter value.
    .Parameter OutPath
    The folder path where both the output JSON and the HTML report will be created.
    The folder will be created if it does not exist. Defaults to current directory.
    .Parameter OutFolderName
    The name of the folder in OutPath where both the output JSON and the HTML report will be created.
    Defaults to "M365BaselineConformance". The client's local timestamp will be appended.
    .Parameter OutProviderFileName
    The name of the Provider output JSON created in the folder created in OutPath.
    Defaults to "ProviderSettingsExport".
    .Parameter OutRegoFileName
    The name of the Rego output JSON and CSV created in the folder created in OutPath.
    Defaults to "TestResults".
    .Parameter OutReportName
    The name of the main html file page created in the folder created in OutPath.
    Defaults to "BaselineReports".
    .Parameter MergeJson
    Set switch to merge all json output into a single file and delete the individual files
    after merging.
    .Parameter OutJsonFileName
    If MergeJson is set, the name of the consolidated json created in the folder
    created in OutPath. Defaults to "ScubaResults".
    .Parameter DisconnectOnExit
    Set switch to disconnect all active connections on exit from ScubaGear (default: $false)
    .Parameter ConfigFilePath
    Local file path to a JSON or YAML formatted configuration file.
    Configuration file parameters can be used in place of command-line
    parameters. Additional parameters and variables not available on the
    command line can also be included in the file that will be provided to the
    tool for use in specific tests.
    .Parameter DarkMode
    Set switch to enable report dark mode by default.
    .Parameter Quiet
    Do not launch external browser for report.
    .Example
    Invoke-SCuBA
    Run an assessment against by default a commercial M365 Tenant against the
    Azure Active Directory, Exchange Online, Microsoft Defender, One Drive, SharePoint Online, and Microsoft Teams
    security baselines. The output will stored in the current directory in a folder called M365BaselineConformaance_*.
    .Example
    Invoke-SCuBA -Version
    This example returns the version of SCuBAGear.
    .Example
    Invoke-SCuBA -ConfigFilePath MyConfig.json
    This example uses the specified configuration file when executing SCuBAGear.
    .Example
    Invoke-SCuBA -ProductNames aad, defender -OPAPath . -OutPath .
    The example will run the tool against the Azure Active Directory, and Defender security
    baselines.
    .Example
    Invoke-SCuBA -ProductNames * -M365Environment dod -OPAPath . -OutPath .
    This example will run the tool against all available security baselines with the
    'dod' teams endpoint.
    .Example
    Invoke-SCuBA -ProductNames aad,exo -M365Environment gcc -OPAPath . -OutPath . -DisconnectOnExit
    Run the tool against Azure Active Directory and Exchange Online security
    baselines, disconnecting connections for those products when complete.
    .Example
    Invoke-SCuBA -ProductNames * -CertificateThumbprint <insert-thumbprint> -AppID <insert-appid> -Organization "tenant.onmicrosoft.com"
    This example will run the tool against all available security baselines while authenticating using a Service Principal with the CertificateThumprint bundle of parameters.
    .Functionality
    Public
    #>

    [CmdletBinding(DefaultParameterSetName='Report')]
    param (
        [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)]
        [string[]]
        $ProductNames = [ScubaConfig]::ScubaDefault('DefaultProductNames'),

        [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)]
        [ValidateNotNullOrEmpty()]
        [string]
        $M365Environment = [ScubaConfig]::ScubaDefault('DefaultM365Environment'),

        [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateScript({Test-Path -PathType Container $_})]
        [string]
        $OPAPath = [ScubaConfig]::ScubaDefault('DefaultOPAPath'),

        [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [ValidateSet($true, $false)]
        [boolean]
        $LogIn = [ScubaConfig]::ScubaDefault('DefaultLogIn'),

        [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [switch]
        $DisconnectOnExit,

        [Parameter(ParameterSetName = 'VersionOnly')]
        [ValidateNotNullOrEmpty()]
        [switch]
        $Version,

        [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [string]
        $AppID,

        [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [string]
        $CertificateThumbprint,

        [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [string]
        $Organization,

        [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutPath = [ScubaConfig]::ScubaDefault('DefaultOutPath'),

        [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutFolderName = [ScubaConfig]::ScubaDefault('DefaultOutFolderName'),

        [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutProviderFileName = [ScubaConfig]::ScubaDefault('DefaultOutProviderFileName'),

        [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutRegoFileName = [ScubaConfig]::ScubaDefault('DefaultOutRegoFileName'),

        [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutReportName = [ScubaConfig]::ScubaDefault('DefaultOutReportName'),

        [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [switch]
        $MergeJson,

        [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutJsonFileName = [ScubaConfig]::ScubaDefault('DefaultOutJsonFileName'),

        [Parameter(Mandatory = $true, ParameterSetName = 'Configuration')]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            if (-Not ($_ | Test-Path)){
                throw "SCuBA configuration file or folder does not exist. $_"
            }
            if (-Not ($_ | Test-Path -PathType Leaf)){
                throw "SCuBA configuration Path argument must be a file."
            }
            return $true
        })]
        [System.IO.FileInfo]
        $ConfigFilePath,

        [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [switch]
        $DarkMode,

        [Parameter(Mandatory = $false, ParameterSetName = 'Configuration')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [switch]
        $Quiet
    )
    process {
        # Retrive ScubaGear Module versions
        $ParentPath = Split-Path $PSScriptRoot -Parent -ErrorAction 'Stop'
        $ScubaManifest = Import-PowerShellDataFile (Join-Path -Path $ParentPath -ChildPath 'ScubaGear.psd1' -Resolve) -ErrorAction 'Stop'
        $ModuleVersion = $ScubaManifest.ModuleVersion
        if ($Version) {
            Write-Output("SCuBA Gear v$ModuleVersion")
            return
        }

        # Transform ProductNames into list of all products if it contains wildcard
        if ($ProductNames.Contains('*')){
            $ProductNames = $PSBoundParameters['ProductNames'] = "aad", "defender", "exo", "powerplatform", "sharepoint", "teams"
            Write-Debug "Setting ProductName to all products because of wildcard"
        }

        # Default execution ParameterSet
        if ($PSCmdlet.ParameterSetName -eq 'Report'){

            $ProvidedParameters = @{
                'ProductNames' = $ProductNames | Sort-Object -Unique
                'M365Environment' = $M365Environment
                'OPAPath' = $OPAPath
                'LogIn' = $LogIn
                'DisconnectOnExit' = $DisconnectOnExit
                'OutPath' = $OutPath
                'OutFolderName' = $OutFolderName
                'OutProviderFileName' = $OutProviderFileName
                'OutRegoFileName' = $OutRegoFileName
                'OutReportName' = $OutReportName
                'MergeJson' = $MergeJson
                'OutJsonFileName' = $OutJsonFileName
            }

            $ScubaConfig = New-Object -Type PSObject -Property $ProvidedParameters
        }

        Remove-Resources # Unload helper modules if they are still in the PowerShell session
        Import-Resources # Imports Providers, RunRego, CreateReport, Connection

        # Loads and executes parameters from a Configuration file
        if ($PSCmdlet.ParameterSetName -eq 'Configuration'){
            [ScubaConfig]::ResetInstance()
            if (-Not ([ScubaConfig]::GetInstance().LoadConfig($ConfigFilePath))){
                Write-Error -Message "The config file failed to load: $ConfigFilePath"
            }
            else {
                $ScubaConfig = [ScubaConfig]::GetInstance().Configuration
            }

            # Authentications parameters use below
            $SPparams = 'AppID', 'CertificateThumbprint', 'Organization'

            # Bound parameters indicate a parameter has been passed in.
            # However authentication parameters are special and are not handled within
            # the config module (since you can't make a default). If an authentication
            # parameter is set in the config file but not supplied on the command line
            # set the Bound parameters value which make it appear as if it was supplied on the
            # command line

            foreach ( $value in $SPparams )
            {
                if  ( $ScubaConfig[$value] -and  (-not  $PSBoundParameters[$value] )) {
                    $PSBoundParameters.Add($value, $ScubaConfig[$value])
                }
            }

            # Now the bound parameters contain the following
            # 1) Non Authentication Parameters explicitly passed in
            # 2) Authentication parameters ( passed in or from the config file as per code above )
            #
            # So to provide for a command line override of config values just set the corresponding
            # config value from the bound parameters to override. This is redundant copy for
            # the authentication parameters ( but keeps the logic simpler)
            # We do not allow ConfigFilePath to be copied as it will be propagated to the
            # config module by reference and causes issues
            #
            foreach ( $value in $PSBoundParameters.keys ) {
                if ( $value -ne "ConfigFilePath" )
                {
                    $ScubaConfig[$value] = $PSBoundParameters[$value]
                }
            }
        }

        # Creates the output folder
        $Date = Get-Date -ErrorAction 'Stop'
        $FormattedTimeStamp = $Date.ToString("yyyy_MM_dd_HH_mm_ss")
        $OutFolderPath = $ScubaConfig.OutPath
        $FolderName = "$($ScubaConfig.OutFolderName)_$($FormattedTimeStamp)"
        New-Item -Path $OutFolderPath -Name $($FolderName) -ItemType Directory -ErrorAction 'Stop' | Out-Null
        $OutFolderPath = Join-Path -Path $OutFolderPath -ChildPath $FolderName -ErrorAction 'Stop'

        # Product Authentication
        $ConnectionParams = @{
            'LogIn' = $ScubaConfig.LogIn;
            'ProductNames' = $ScubaConfig.ProductNames;
            'M365Environment' = $ScubaConfig.M365Environment;
            'BoundParameters' = $PSBoundParameters;
        }
        $ProdAuthFailed = Invoke-Connection @ConnectionParams
        if ($ProdAuthFailed.Count -gt 0) {
            $ScubaConfig.ProductNames = Compare-ProductList -ProductNames $ScubaConfig.ProductNames `
            -ProductsFailed $ProdAuthFailed `
            -ExceptionMessage 'All indicated Products were unable to authenticate'
        }

        # Tenant Metadata for the Report
        $TenantDetails = Get-TenantDetail -ProductNames $ScubaConfig.ProductNames -M365Environment $ScubaConfig.M365Environment

        try {
            # Provider Execution
            $ProviderParams = @{
                'ProductNames' = $ScubaConfig.ProductNames;
                'M365Environment' = $ScubaConfig.M365Environment;
                'TenantDetails' = $TenantDetails;
                'ModuleVersion' = $ModuleVersion;
                'OutFolderPath' = $OutFolderPath;
                'OutProviderFileName' = $ScubaConfig.OutProviderFileName;
                'BoundParameters' = $PSBoundParameters;
            }
            $ProdProviderFailed = Invoke-ProviderList @ProviderParams
            if ($ProdProviderFailed.Count -gt 0) {
                $ScubaConfig.ProductNames = Compare-ProductList -ProductNames $ScubaConfig.ProductNames `
                 -ProductsFailed $ProdProviderFailed `
                 -ExceptionMessage 'All indicated Product Providers failed to execute'
            }

            # OPA Rego invocation
            $RegoParams = @{
                'ProductNames' = $ScubaConfig.ProductNames;
                'OPAPath' = $ScubaConfig.OPAPath;
                'ParentPath' = $ParentPath;
                'OutFolderPath' = $OutFolderPath;
                'OutProviderFileName' = $ScubaConfig.OutProviderFileName;
                'OutRegoFileName' = $ScubaConfig.OutRegoFileName;
            }
            $ProdRegoFailed = Invoke-RunRego @RegoParams
            if ($ProdRegoFailed.Count -gt 0) {
                $ScubaConfig.ProductNames = Compare-ProductList -ProductNames $ScubaConfig.ProductNames `
                -ProductsFailed  $ProdRegoFailed `
                -ExceptionMessage 'All indicated Product Rego invocations failed'
            }

            # Report Creation
            # Converted back from JSON String for PS Object use
            $TenantDetails = $TenantDetails | ConvertFrom-Json
            $ReportParams = @{
                'ProductNames' = $ScubaConfig.ProductNames
                'TenantDetails' = $TenantDetails
                'ModuleVersion' = $ModuleVersion
                'OutFolderPath' = $OutFolderPath
                'OutProviderFileName' = $ScubaConfig.OutProviderFileName
                'OutRegoFileName' = $ScubaConfig.OutRegoFileName
                'OutReportName' = $ScubaConfig.OutReportName
                'DarkMode' = $DarkMode
                'Quiet' = $Quiet
            }
            Invoke-ReportCreation @ReportParams

            if ($MergeJson) {
                # Craft the complete json version of the output
                $JsonParams = @{
                    'ProductNames' = $ScubaConfig.ProductNames;
                    'OutFolderPath' = $OutFolderPath;
                    'OutProviderFileName' = $ScubaConfig.OutProviderFileName;
                    'TenantDetails' = $TenantDetails;
                    'ModuleVersion' = $ModuleVersion;
                    'OutJsonFileName' = $ScubaConfig.OutJsonFileName;
                }
                Merge-JsonOutput @JsonParams
            }
        }
        finally {
            if ($ScubaConfig.DisconnectOnExit) {
                if ($VerbosePreference -eq "Continue") {
                    Disconnect-SCuBATenant -ProductNames $ScubaConfig.ProductNames -ErrorAction SilentlyContinue -Verbose
                }
                else {
                    Disconnect-SCuBATenant -ProductNames $ScubaConfig.ProductNames -ErrorAction SilentlyContinue
                }
            }
            [ScubaConfig]::ResetInstance()
        }
    }
}

$ArgToProd = @{
    teams = "Teams";
    exo = "EXO";
    defender = "Defender";
    aad = "AAD";
    powerplatform = "PowerPlatform";
    sharepoint = "SharePoint";
}

$ProdToFullName = @{
    Teams = "Microsoft Teams";
    EXO = "Exchange Online";
    Defender = "Microsoft 365 Defender";
    AAD = "Azure Active Directory";
    PowerPlatform = "Microsoft Power Platform";
    SharePoint = "SharePoint Online";
}

$IndividualReportFolderName = "IndividualReports"

function Get-FileEncoding{
    <#
    .Description
    This function returns encoding type for setting content.
    .Functionality
    Internal
    #>

    $PSVersion = $PSVersionTable.PSVersion

    $Encoding = 'utf8'

    if ($PSVersion -ge '6.0'){
        $Encoding = 'utf8NoBom'
    }

    return $Encoding
}

function Invoke-ProviderList {
    <#
    .Description
    This function runs the various providers modules stored in the Providers Folder
    Output will be stored as a ProviderSettingsExport.json in the OutPath Folder
    .Functionality
    Internal
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)]
        [string[]]
        $ProductNames,

        [Parameter(Mandatory = $true)]
        [ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)]
        [ValidateNotNullOrEmpty()]
        [string]
        $M365Environment,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $TenantDetails,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $ModuleVersion,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutFolderPath,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutProviderFileName,

        [Parameter(Mandatory = $true)]
        [hashtable]
        $BoundParameters
    )
    process {
        try {
            # yes the syntax has to be like this
            # fixing the spacing causes PowerShell interpreter errors
            $ProviderJSON = @"
"@

            $N = 0
            $Len = $ProductNames.Length
            $ProdProviderFailed = @()
            $ConnectTenantParams = @{
                'M365Environment' = $M365Environment
            }
            $SPOProviderParams = @{
                'M365Environment' = $M365Environment
            }

            $PnPFlag = $false
            if ($BoundParameters.AppID) {
                $ServicePrincipalParams = Get-ServicePrincipalParams -BoundParameters $BoundParameters
                $ConnectTenantParams += @{ServicePrincipalParams = $ServicePrincipalParams; }
                $PnPFlag = $true
                $SPOProviderParams += @{PnPFlag = $PnPFlag }
            }

            foreach ($Product in $ProductNames) {
                $BaselineName = $ArgToProd[$Product]
                $N += 1
                $Percent = $N * 100 / $Len
                $Status = "Running the $($BaselineName) Provider; $($N) of $($Len) Product settings extracted"
                $ProgressParams = @{
                    'Activity' = "Running the provider for each baseline";
                    'Status' = $Status;
                    'PercentComplete' = $Percent;
                    'Id' = 1;
                    'ErrorAction' = 'Stop';
                }
                Write-Progress @ProgressParams
                try {
                    $RetVal = ""
                    switch ($Product) {
                        "aad" {
                            $RetVal = Export-AADProvider | Select-Object -Last 1
                        }
                        "exo" {
                            $RetVal = Export-EXOProvider | Select-Object -Last 1
                        }
                        "defender" {
                            $RetVal = Export-DefenderProvider @ConnectTenantParams  | Select-Object -Last 1
                        }
                        "powerplatform" {
                            $RetVal = Export-PowerPlatformProvider -M365Environment $M365Environment | Select-Object -Last 1
                        }
                        "sharepoint" {
                            $RetVal = Export-SharePointProvider @SPOProviderParams | Select-Object -Last 1
                        }
                        "teams" {
                            $RetVal = Export-TeamsProvider | Select-Object -Last 1
                        }
                        default {
                            Write-Error -Message "Invalid ProductName argument"
                        }
                    }
                    $ProviderJSON += $RetVal
                }
                catch {
                    Write-Error "Error with the $($BaselineName) Provider. See the exception message for more details: $($_)"
                    $ProdProviderFailed += $Product
                    Write-Warning "$($Product) will be omitted from the output because of the failure above `n`n"
                }
            }

            $ProviderJSON = $ProviderJSON.TrimEnd(",")
            $TimeZone = ""
            $CurrentDate = Get-Date -ErrorAction 'Stop'
            $TimestampZulu = $CurrentDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
            $GetTimeZone = Get-TimeZone -ErrorAction 'Stop'
            if (($CurrentDate).IsDaylightSavingTime()) {
                $TimeZone = ($GetTimeZone).DaylightName
            }
            else {
                $TimeZone = ($GetTimeZone).StandardName
            }

        $ConfigDetails = @(ConvertTo-Json -Depth 100 $([ScubaConfig]::GetInstance().Configuration))
        if(! $ConfigDetails) {
            $ConfigDetails = "{}"
        }

        $BaselineSettingsExport = @"
        {
                "baseline_version": "1",
                "module_version": "$ModuleVersion",
                "date": "$($CurrentDate) $($TimeZone)",
                "timestamp_zulu": "$($TimestampZulu)",
                "tenant_details": $($TenantDetails),
                "scuba_config": $($ConfigDetails),
 
                $ProviderJSON
        }
"@


            # Strip the character sequences that Rego tries to interpret as escape sequences,
            # resulting in the error "unable to parse input: yaml: line x: found unknown escape character"
            # "\/", ex: "\/Date(1705651200000)\/"
            $BaselineSettingsExport = $BaselineSettingsExport.replace("\/", "")
            # "\B", ex: "Removed an entry in Tenant Allow\Block List"
            $BaselineSettingsExport = $BaselineSettingsExport.replace("\B", "/B")

            $FinalPath = Join-Path -Path $OutFolderPath -ChildPath "$($OutProviderFileName).json" -ErrorAction 'Stop'
            $BaselineSettingsExport | Set-Content -Path $FinalPath -Encoding $(Get-FileEncoding) -ErrorAction 'Stop'
            $ProdProviderFailed
        }
        catch {
            $InvokeProviderListErrorMessage = "Fatal Error involving the Provider functions. `
            Ending ScubaGear execution. See the exception message for more details: $($_)"

            throw $InvokeProviderListErrorMessage
        }
    }
}

function Invoke-RunRego {
    <#
    .Description
    This function runs the RunRego module.
    Which runs the various rego files against the
    ProviderSettings.json using the specified OPA executable
    Output will be stored as a TestResults.json in the OutPath Folder
    .Functionality
    Internal
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)]
        [string[]]
        $ProductNames,

        [ValidateNotNullOrEmpty()]
        [string]
        $OPAPath = [ScubaConfig]::ScubaDefault('DefaultOPAPath'),

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $ParentPath,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $OutFolderPath,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $OutProviderFileName,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutRegoFileName
    )
    process {
        try {
            $ProdRegoFailed = @()
            $TestResults = @()
            $N = 0
            $Len = $ProductNames.Length
            foreach ($Product in $ProductNames) {
                $BaselineName = $ArgToProd[$Product]
                $N += 1
                $Percent = $N * 100 / $Len

                $Status = "Running the $($BaselineName) Rego Verification; $($N) of $($Len) Rego verifications completed"
                $ProgressParams = @{
                    'Activity' = "Running the rego for each baseline";
                    'Status' = $Status;
                    'PercentComplete' = $Percent;
                    'Id' = 1;
                    'ErrorAction' = 'Stop';
                }
                Write-Progress @ProgressParams
                $InputFile = Join-Path -Path $OutFolderPath "$($OutProviderFileName).json" -ErrorAction 'Stop'
                $RegoFile = Join-Path -Path $ParentPath -ChildPath "Rego" -ErrorAction 'Stop'
                $RegoFile = Join-Path -Path $RegoFile -ChildPath "$($BaselineName)Config.rego" -ErrorAction 'Stop'
                $params = @{
                    'InputFile' = $InputFile;
                    'RegoFile' = $RegoFile;
                    'PackageName' = $Product;
                    'OPAPath' = $OPAPath
                }
                try {
                    $RetVal = Invoke-Rego @params
                    $TestResults += $RetVal
                }
                catch {
                    Write-Error "Error with the $($BaselineName) Rego invocation. See the exception message for more details: $($_)"
                    $ProdRegoFailed += $Product
                    Write-Warning "$($Product) will be omitted from the output because of the failure above"
                }
            }

            $TestResultsJson = $TestResults | ConvertTo-Json -Depth 5 -ErrorAction 'Stop'
            $FileName = Join-Path -Path $OutFolderPath "$($OutRegoFileName).json" -ErrorAction 'Stop'
            $TestResultsJson | Set-Content -Path $FileName -Encoding $(Get-FileEncoding) -ErrorAction 'Stop'

            foreach ($Product in $TestResults) {
                foreach ($Test in $Product) {
                    # ConvertTo-Csv struggles with the nested nature of the ActualValue
                    # and Commandlet fields. Explicitly convert these to json strings before
                    # calling ConvertTo-Csv
                    $Test.ActualValue = $Test.ActualValue | ConvertTo-Json -Depth 3 -Compress -ErrorAction 'Stop'
                    $Test.Commandlet = $Test.Commandlet -Join ", "
                }
            }
            $TestResultsCsv = $TestResults | ConvertTo-Csv -NoTypeInformation -ErrorAction 'Stop'
            $CSVFileName = Join-Path -Path $OutFolderPath "$($OutRegoFileName).csv" -ErrorAction 'Stop'
            $TestResultsCsv | Set-Content -Path $CSVFileName -Encoding $(Get-FileEncoding) -ErrorAction 'Stop'
            $ProdRegoFailed
        }
        catch {
            $InvokeRegoErrorMessage = "Fatal Error involving the OPA output function. `
            Ending ScubaGear execution. See the exception message for more details: $($_)"

            throw $InvokeRegoErrorMessage
        }
    }
}

function Pluralize {
    <#
    .Description
    This function whether the singular or plural version of the noun
    is needed and returns the appropriate version.
    .Functionality
    Internal
    #>

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

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $PluralNoun,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [int]
        $Count
    )
    process {
        if ($Count -gt 1) {
            $PluralNoun
        }
        else {
            $SingularNoun
        }
    }
}

function Merge-JsonOutput {
    <#
    .Description
    This function packages all the json output created into a single json file.
    .Functionality
    Internal
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)]
        [string[]]
        $ProductNames,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutFolderPath,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutProviderFileName,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [object]
        $TenantDetails,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $ModuleVersion,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutJsonFileName
    )
    process {
        try {
            # Files to delete at the end if no errors are encountered
            $DeletionList = @()

            # Load the raw provider output
            $SettingsExportPath = Join-Path $OutFolderPath -ChildPath "$($OutProviderFileName).json"
            $DeletionList += $SettingsExportPath
            $SettingsExport =  Get-Content $SettingsExportPath -Raw
            $TimestampZulu = $(ConvertFrom-Json $SettingsExport).timestamp_zulu

            # Get a list and abbreviation mapping of the products assessed
            $FullNames = @()
            $ProductAbbreviationMapping = @{}
            foreach ($ProductName in $ProductNames) {
                $BaselineName = $ArgToProd[$ProductName]
                $FullNames += $ProdToFullName[$BaselineName]
                $ProductAbbreviationMapping[$ProdToFullName[$BaselineName]] = $BaselineName
            }

            # Extract the metadata
            $Results = [pscustomobject]@{}
            $Summary = [pscustomobject]@{}
            $MetaData = [pscustomobject]@{
                "TenantId" = $TenantDetails.TenantId;
                "DisplayName" = $TenantDetails.DisplayName;
                "DomainName" = $TenantDetails.DomainName;
                "ProductSuite" = "Microsoft 365";
                "ProductsAssessed" = $FullNames;
                "ProductAbbreviationMapping" = $ProductAbbreviationMapping
                "Tool" = "ScubaGear";
                "ToolVersion" = $ModuleVersion;
                "TimestampZulu" = $TimestampZulu;
            }


            # Aggregate the report results and summaries
            $IndividualReportPath = Join-Path -Path $OutFolderPath $IndividualReportFolderName -ErrorAction 'Stop'
            foreach ($Product in $ProductNames) {
                $BaselineName = $ArgToProd[$Product]
                $FileName = Join-Path $IndividualReportPath "$($BaselineName)Report.json"
                $DeletionList += $FileName
                $IndividualResults = Get-Content $FileName | ConvertFrom-Json

                $Results | Add-Member -NotePropertyName $BaselineName `
                    -NotePropertyValue $IndividualResults.Results

                # The date is listed under the metadata, no need to include it in the summary as well
                $IndividualResults.ReportSummary.PSObject.Properties.Remove('Date')

                $Summary | Add-Member -NotePropertyName $BaselineName `
                    -NotePropertyValue $IndividualResults.ReportSummary
            }

            # Convert the output a json string
            $MetaData = ConvertTo-Json $MetaData -Depth 3
            $Results = ConvertTo-Json $Results -Depth 5
            $Summary = ConvertTo-Json $Summary -Depth 3
            $ReportJson = @"
{
    "MetaData": $MetaData,
    "Summary": $Summary,
    "Results": $Results,
    "Raw": $SettingsExport
}
"@


            # ConvertTo-Json for some reason converts the <, >, and ' characters into unicode escape sequences.
            # Convert those back to ASCII.
            $ReportJson = $ReportJson.replace("\u003c", "<")
            $ReportJson = $ReportJson.replace("\u003e", ">")
            $ReportJson = $ReportJson.replace("\u0027", "'")

            # Save the file
            $JsonFileName = Join-Path -Path $OutFolderPath "$($OutJsonFileName).json" -ErrorAction 'Stop'
            $ReportJson | Set-Content -Path $JsonFileName -Encoding $(Get-FileEncoding) -ErrorAction 'Stop'

            # Delete the now redundant files
            foreach ($File in $DeletionList) {
                Remove-Item $File
            }
        }
        catch {
            $MergeJsonErrorMessage = "Fatal Error involving the Json reports aggregation. `
            Ending ScubaGear execution. See the exception message for more details: $($_)"

            throw $MergeJsonErrorMessage
        }
    }
}

function Invoke-ReportCreation {
    <#
    .Description
    This function runs the CreateReport Module
    which creates an HTML report using the TestResults.json.
    Output will be stored as various HTML files in the OutPath Folder.
    The report Home page will be named BaselineReports.html
    .Functionality
    Internal
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)]
        [string[]]
        $ProductNames,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [object]
        $TenantDetails,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $ModuleVersion,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutFolderPath,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutProviderFileName,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutRegoFileName,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutReportName,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [switch]
        $Quiet,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [switch]
        $DarkMode
    )
    process {
        try {
            $N = 0
            $Len = $ProductNames.Length
            $Fragment = @()
            $IndividualReportPath = Join-Path -Path $OutFolderPath -ChildPath $IndividualReportFolderName
            New-Item -Path $IndividualReportPath -ItemType "Directory" -ErrorAction "SilentlyContinue" | Out-Null

            $ReporterPath = Join-Path -Path $PSScriptRoot -ChildPath "CreateReport" -ErrorAction 'Stop'
            $Images = Join-Path -Path $ReporterPath -ChildPath "images" -ErrorAction 'Stop'
            Copy-Item -Path $Images -Destination $IndividualReportPath -Force -Recurse -ErrorAction 'Stop'

            $SecureBaselines =  Import-SecureBaseline -ProductNames $ProductNames

            foreach ($Product in $ProductNames) {
                $BaselineName = $ArgToProd[$Product]
                $N += 1
                $Percent = $N*100/$Len
                $Status = "Running the $($BaselineName) Report creation; $($N) of $($Len) Baselines Reports created";
                $ProgressParams = @{
                    'Activity' = "Creating the reports for each baseline";
                    'Status' = $Status;
                    'PercentComplete' = $Percent;
                    'Id' = 1;
                    'ErrorAction' = 'Stop';
                }
                Write-Progress @ProgressParams

                $FullName = $ProdToFullName[$BaselineName]

                $CreateReportParams = @{
                    'BaselineName' = $BaselineName;
                    'FullName' = $FullName;
                    'IndividualReportPath' = $IndividualReportPath;
                    'OutPath' = $OutFolderPath;
                    'OutProviderFileName' = $OutProviderFileName;
                    'OutRegoFileName' = $OutRegoFileName;
                    'DarkMode' = $DarkMode;
                    'SecureBaselines' = $SecureBaselines
                }

                $Report = New-Report @CreateReportParams
                $LinkPath = "$($IndividualReportFolderName)/$($BaselineName)Report.html"
                $LinkClassName = '"individual_reports"' # uses no escape characters
                $Link = "<a class=$($LinkClassName) href='$($LinkPath)'>$($FullName)</a>"

                $PassesSummary = "<div class='summary pass'>$($Report.Passes) tests passed</div>"
                $WarningsSummary = "<div class='summary'></div>"
                $FailuresSummary = "<div class='summary'></div>"
                $BaselineURL = "<a href= `"https://github.com/cisagov/ScubaGear/blob/v$($ModuleVersion)/baselines`" target=`"_blank`"><h3 style=`"width: 100px;`">Baseline Documents</h3></a>"
                $ManualSummary = "<div class='summary'></div>"
                $ErrorSummary = "<div class='summary'></div>"

                if ($Report.Warnings -gt 0) {
                    $Noun = Pluralize -SingularNoun "warning" -PluralNoun "warnings" -Count $Report.Warnings
                    $WarningsSummary = "<div class='summary warning'>$($Report.Warnings) $($Noun)</div>"
                }

                if ($Report.Failures -gt 0) {
                    $Noun = Pluralize -SingularNoun "test" -PluralNoun "tests" -Count $Report.Failures
                    $FailuresSummary = "<div class='summary failure'>$($Report.Failures) $($Noun) failed</div>"
                }

                if ($Report.Manual -gt 0) {
                    $Noun = Pluralize -SingularNoun "check" -PluralNoun "checks" -Count $Report.Manual
                    $ManualSummary = "<div class='summary manual'>$($Report.Manual) manual $($Noun) needed</div>"
                }

                if ($Report.Errors -gt 0) {
                    $Noun = Pluralize -SingularNoun "error" -PluralNoun "errors" -Count $Report.Errors
                    $ErrorSummary = "<div class='summary error'>$($Report.Errors) $($Noun)</div>"
                }

                $Fragment += [pscustomobject]@{
                "Baseline Conformance Reports" = $Link;
                "Details" = "$($PassesSummary) $($WarningsSummary) $($FailuresSummary) $($ManualSummary) $($ErrorSummary)"
                }
            }
            $TenantMetaData += [pscustomobject]@{
                "Tenant Display Name" = $TenantDetails.DisplayName;
                "Tenant Domain Name" = $TenantDetails.DomainName
                "Tenant ID" = $TenantDetails.TenantId;
                "Report Date" = $Report.Date;
            }
            $TenantMetaData = $TenantMetaData | ConvertTo-Html -Fragment -ErrorAction 'Stop'
            $TenantMetaData = $TenantMetaData -replace '^(.*?)<table>','<table class ="tenantdata" style = "text-align:center;">'
            $Fragment = $Fragment | ConvertTo-Html -Fragment -ErrorAction 'Stop'

            $ReportHtmlPath = Join-Path -Path $ReporterPath -ChildPath "ParentReport" -ErrorAction 'Stop'
            $ReportHTML = (Get-Content $(Join-Path -Path $ReportHtmlPath -ChildPath "ParentReport.html") -ErrorAction 'Stop') -Join "`n"
            $ReportHTML = $ReportHTML.Replace("{TENANT_DETAILS}", $TenantMetaData)
            $ReportHTML = $ReportHTML.Replace("{TABLES}", $Fragment)
            $ReportHTML = $ReportHTML.Replace("{MODULE_VERSION}", "v$ModuleVersion")
            $ReportHTML = $ReportHTML.Replace("{BASELINE_URL}", $BaselineURL)

            $CssPath = Join-Path -Path $ReporterPath -ChildPath "styles" -ErrorAction 'Stop'
            $MainCSS = (Get-Content $(Join-Path -Path $CssPath -ChildPath "main.css") -ErrorAction 'Stop') -Join "`n"
            $ReportHTML = $ReportHTML.Replace("{MAIN_CSS}", "<style>$($MainCSS)</style>")

            $ParentCSS = (Get-Content $(Join-Path -Path $CssPath -ChildPath "ParentReportStyle.css") -ErrorAction 'Stop') -Join "`n"
            $ReportHTML = $ReportHTML.Replace("{PARENT_CSS}", "<style>$($ParentCSS)</style>")

            $ScriptsPath = Join-Path -Path $ReporterPath -ChildPath "scripts" -ErrorAction 'Stop'
            $ParentReportJS = (Get-Content $(Join-Path -Path $ScriptsPath -ChildPath "ParentReport.js") -ErrorAction 'Stop') -Join "`n"
            $UtilsJS = (Get-Content $(Join-Path -Path $ScriptsPath -ChildPath "utils.js") -ErrorAction 'Stop') -Join "`n"
            $ParentReportJS = "$($ParentReportJS)`n$($UtilsJS)"
            $ReportHTML = $ReportHTML.Replace("{MAIN_JS}", "<script>
                let darkMode = $($DarkMode.ToString().ToLower());
                $($ParentReportJS)
            </script>"
)

            Add-Type -AssemblyName System.Web -ErrorAction 'Stop'
            $ReportFileName = Join-Path -Path $OutFolderPath "$($OutReportName).html" -ErrorAction 'Stop'
            [System.Web.HttpUtility]::HtmlDecode($ReportHTML) | Out-File $ReportFileName -ErrorAction 'Stop'
            if (-Not $Quiet) {
                Invoke-Item $ReportFileName
            }
        }
        catch {
            $InvokeReportErrorMessage = "Fatal Error involving the Report Creation. `
            Ending ScubaGear execution. See the exception message for more details: $($_)"

            throw $InvokeReportErrorMessage
        }
    }
}

function Get-TenantDetail {
    <#
    .Description
    This function gets the details of the M365 Tenant using
    the various M365 PowerShell modules
    .Functionality
    Internal
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", IgnoreCase = $false)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $ProductNames,

        [Parameter(Mandatory = $true)]
        [ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)]
        [ValidateNotNullOrEmpty()]
        [string]
        $M365Environment
    )

    # organized by best tenant details information
    if ($ProductNames.Contains("aad")) {
        Get-AADTenantDetail
    }
    elseif ($ProductNames.Contains("sharepoint")) {
        Get-AADTenantDetail
    }
    elseif ($ProductNames.Contains("teams")) {
        Get-TeamsTenantDetail -M365Environment $M365Environment
    }
    elseif ($ProductNames.Contains("powerplatform")) {
        Get-PowerPlatformTenantDetail -M365Environment $M365Environment
    }
    elseif ($ProductNames.Contains("exo")) {
        Get-EXOTenantDetail -M365Environment $M365Environment
    }
    elseif ($ProductNames.Contains("defender")) {
        Get-EXOTenantDetail -M365Environment $M365Environment
    }
    else {
        $TenantInfo = @{
            "DisplayName" = "Orchestrator Error retrieving Display name";
            "DomainName" = "Orchestrator Error retrieving Domain name";
            "TenantId" = "Orchestrator Error retrieving Tenant ID";
            "AdditionalData" = "Orchestrator Error retrieving additional data";
        }
        $TenantInfo = $TenantInfo | ConvertTo-Json -Depth 3
        $TenantInfo
    }
}

function Invoke-Connection {
    <#
    .Description
    This function uses the Connection.psm1 module
    which uses the various PowerShell modules to establish
    a connection to an M365 Tenant associated with provided
    credentials
    .Functionality
    Internal
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [boolean]
        $LogIn,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)]
        [string[]]
        $ProductNames,

        [ValidateSet("commercial", "gcc", "gcchigh", "dod")]
        [ValidateNotNullOrEmpty()]
        [string]
        $M365Environment = "commercial",

        [Parameter(Mandatory=$true)]
        [hashtable]
        $BoundParameters
    )

    $ConnectTenantParams = @{
        'ProductNames' = $ProductNames;
        'M365Environment' = $M365Environment
    }

    if ($BoundParameters.AppID) {
        $ServicePrincipalParams = Get-ServicePrincipalParams -BoundParameters $BoundParameters
        $ConnectTenantParams += @{ServicePrincipalParams = $ServicePrincipalParams;}
    }

    if ($LogIn) {
        $AnyFailedAuth = Connect-Tenant @ConnectTenantParams
        $AnyFailedAuth
    }
}

function Compare-ProductList {
    <#
    .Description
    Compares two ProductNames Lists and returns the Diff between them
    Used to compare a failed execution list with the original list
    .Functionality
    Internal
    #>

    param(

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)]
        [string[]]
        $ProductNames,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)]
        [string[]]
        $ProductsFailed,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $ExceptionMessage
    )

    $Difference = Compare-Object $ProductNames -DifferenceObject $ProductsFailed -PassThru
    if (-not $Difference) {
        throw "$($ExceptionMessage); aborting ScubaGear execution"
    }
    else {
        $Difference
    }
}

function Get-ServicePrincipalParams {
    <#
    .Description
    Returns a valid a hastable of parameters for authentication via
    Service Principal. Throws an error if there are none.
    .Functionality
    Internal
    #>

    [CmdletBinding()]
    param(
    [Parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [hashtable]
    $BoundParameters
    )

    $ServicePrincipalParams = @{}

    $CheckThumbprintParams = ($BoundParameters.CertificateThumbprint) `
    -and ($BoundParameters.AppID) -and ($BoundParameters.Organization)

    if ($CheckThumbprintParams) {
        $CertThumbprintParams = @{
            CertificateThumbprint = $BoundParameters.CertificateThumbprint;
            AppID = $BoundParameters.AppID;
            Organization = $BoundParameters.Organization;
        }
        $ServicePrincipalParams += @{CertThumbprintParams = $CertThumbprintParams}
    }
    else {
        throw "Missing parameters required for authentication with Service Principal Auth; Run Get-Help Invoke-Scuba for details on correct arguments"
    }
    $ServicePrincipalParams
}

function Import-Resources {
    <#
    .Description
    This function imports all of the various helper Provider,
    Rego, and Reporter modules to the runtime
    .Functionality
    Internal
    #>

    [CmdletBinding()]
    param()
    try {
        $ProvidersPath = Join-Path -Path $PSScriptRoot `
        -ChildPath "Providers" `
        -Resolve `
        -ErrorAction 'Stop'
        $ProviderResources = Get-ChildItem $ProvidersPath -Recurse | Where-Object { $_.Name -like 'Export*.psm1' }
        if (!$ProviderResources)
        {
            throw "Provider files were not found, aborting this run"
        }

        foreach ($Provider in $ProviderResources.Name) {
            $ProvidersPath = Join-Path -Path $PSScriptRoot -ChildPath "Providers" -ErrorAction 'Stop'
            $ModulePath = Join-Path -Path $ProvidersPath -ChildPath $Provider -ErrorAction 'Stop'
            Import-Module $ModulePath
        }

        @('Connection', 'RunRego', 'CreateReport', 'ScubaConfig', 'Support') | ForEach-Object {
            $ModulePath = Join-Path -Path $PSScriptRoot -ChildPath $_ -ErrorAction 'Stop'
            Write-Debug "Importing $_ module"
            Import-Module -Name $ModulePath
        }
    }
    catch {
        $ImportResourcesErrorMessage = "Fatal Error involving importing PowerShell modules. `
            Ending ScubaGear execution. See the exception message for more details: $($_)"

            throw $ImportResourcesErrorMessage
    }
}

function Remove-Resources {
    <#
    .Description
    This function cleans up all of the various imported modules
    Mostly meant for dev work
    .Functionality
    Internal
    #>

    [CmdletBinding()]
    $Providers = @("ExportPowerPlatform", "ExportEXOProvider", "ExportAADProvider",
    "ExportDefenderProvider", "ExportTeamsProvider", "ExportSharePointProvider")
    foreach ($Provider in $Providers) {
        Remove-Module $Provider -ErrorAction "SilentlyContinue"
    }

    Remove-Module "ScubaConfig" -ErrorAction "SilentlyContinue"
    Remove-Module "RunRego" -ErrorAction "SilentlyContinue"
    Remove-Module "CreateReport" -ErrorAction "SilentlyContinue"
    Remove-Module "Connection" -ErrorAction "SilentlyContinue"
}

function Invoke-RunCached {
    <#
    .SYNOPSIS
    Specially execute the SCuBAGear tool security baselines for specified M365 products.
    Can be executed on static provider JSON.
    .Description
    This is the function for running the tool provider JSON that has already been extracted.
    This functions comes with the extra ExportProvider parameter to omit exporting the provider
    if set to $false.
    The rego will be run on a static provider JSON in the specified OutPath.
    <#
    .Parameter ExportProvider
    This parameter will when set to $true export the provider and act like Invoke-Scuba.
    When set to $false will instead omit authentication plus pulling the provider and will
    instead look in OutPath and run just the Rego verification and Report creation.
    .Parameter ProductNames
    A list of one or more M365 shortened product names that the tool will assess when it is executed. Acceptable product name values are listed below.
    To assess Azure Active Directory you would enter the value aad.
    To assess Exchange Online you would enter exo and so forth.
    - Azure Active Directory: aad
    - Defender for Office 365: defender
    - Exchange Online: exo
    - MS Power Platform: powerplatform
    - SharePoint Online: sharepoint
    - MS Teams: teams.
    Use '*' to run all baselines.
    .Parameter M365Environment
    This parameter is used to authenticate to the different commercial/government environments.
    Valid values include "commercial", "gcc", "gcchigh", or "dod".
    For M365 tenants with E3/E5 licenses enter the value **"commercial"**.
    For M365 Government Commercial Cloud tenants with G3/G5 licenses enter the value **"gcc"**.
    For M365 Government Commercial Cloud High tenants enter the value **"gcchigh"**.
    For M365 Department of Defense tenants enter the value **"dod"**.
    Default is 'commercial'.
    .Parameter OPAPath
    The folder location of the OPA Rego executable file.
    The OPA Rego executable embedded with this project is located in the project's root folder.
    If you want to execute the tool using a version of OPA Rego located in another folder,
    then customize the variable value with the full path to the alternative OPA Rego exe file.
    .Parameter LogIn
    A `$true` or `$false` variable that if set to `$true`
    will prompt you to provide credentials if you want to establish a connection
    to the specified M365 products in the **$ProductNames** variable.
    For most use cases, leave this variable to be `$true`.
    A connection is established in the current PowerShell terminal session with the first authentication.
    If you want to run another verification in the same PowerShell session simply set
    this variable to be `$false` to bypass the reauthenticating in the same session. Default is $true.
    .Parameter Version
    Will output the current ScubaGear version to the terminal without running this cmdlet.
    .Parameter AppID
    The application ID of the service principal that's used during certificate based
    authentication. A valid value is the GUID of the application ID (service principal).
    .Parameter CertificateThumbprint
    The thumbprint value specifies the certificate that's used for certificate base authentication.
    The underlying PowerShell modules retrieve the certificate from the user's certificate store.
    As such, a copy of the certificate must be located there.
    .Parameter Organization
    Specify the organization that's used in certificate based authentication.
    Use the tenant's tenantname.onmicrosoft.com domain for the parameter value.
    The folder path where both the output JSON and the HTML report will be created.
    The folder will be created if it does not exist. Defaults to current directory.
    .Parameter OutFolderName
    The name of the folder in OutPath where both the output JSON and the HTML report will be created.
    Defaults to "M365BaselineConformance". The client's local timestamp will be appended.
    .Parameter OutProviderFileName
    The name of the Provider output JSON created in the folder created in OutPath.
    Defaults to "ProviderSettingsExport".
    .Parameter OutRegoFileName
    The name of the Rego output JSON and CSV created in the folder created in OutPath.
    Defaults to "TestResults".
    .Parameter OutReportName
    The name of the main html file page created in the folder created in OutPath.
    Defaults to "BaselineReports".
    .Parameter MergeJson
    Set switch to merge all json output into a single file and delete the individual files
    after merging.
    .Parameter OutJsonFileName
    If MergeJson is set, the name of the consolidated json created in the folder
    created in OutPath. Defaults to "ScubaResults".
    .Parameter DarkMode
    Set switch to enable report dark mode by default.
    .Example
    Invoke-RunCached
    Run an assessment against by default a commercial M365 Tenant against the
    Azure Active Directory, Exchange Online, Microsoft Defender, One Drive, SharePoint Online, and Microsoft Teams
    security baselines. The output will stored in the current directory in a folder called M365BaselineConformaance_*.
    .Example
    Invoke-RunCached -Version
    This example returns the version of SCuBAGear.
    .Example
    Invoke-RunCached -ProductNames aad, defender -OPAPath . -OutPath .
    The example will run the tool against the Azure Active Directory, and Defender security
    baselines.
    .Example
    Invoke-RunCached -ProductNames * -M365Environment dod -OPAPath . -OutPath .
    This example will run the tool against all available security baselines with the
    'dod' teams endpoint.
    .Example
    Invoke-SCuBA -ProductNames * -CertificateThumbprint <insert-thumbprint> -AppID <insert-appid> -Organization "tenant.onmicrosoft.com"
    This example will run the tool against all available security baselines while authenticating using a Service Principal with the CertificateThumprint bundle of parameters.
    .Functionality
    Public
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [boolean]
        $ExportProvider = $true,

        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)]
        [string[]]
        $ProductNames = [ScubaConfig]::ScubaDefault('AllProductNames'),

        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateSet("commercial", "gcc", "gcchigh", "dod")]
        [ValidateNotNullOrEmpty()]
        [string]
        $M365Environment = [ScubaConfig]::ScubaDefault('DefaultM365Environment'),

        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateScript({Test-Path -PathType Container $_})]
        [ValidateNotNullOrEmpty()]
        [string]
        $OPAPath = [ScubaConfig]::ScubaDefault('DefaultOPAPath'),

        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [ValidateSet($true, $false)]
        [boolean]
        $LogIn = [ScubaConfig]::ScubaDefault('DefaultLogIn'),

        [Parameter(ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [switch]
        $Version,

        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [string]
        $AppID,

        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [string]
        $CertificateThumbprint,

        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [string]
        $Organization,

        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutPath = [ScubaConfig]::ScubaDefault('DefaultOutPath'),

        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutProviderFileName = [ScubaConfig]::ScubaDefault('DefaultOutProviderFileName'),

        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutRegoFileName = [ScubaConfig]::ScubaDefault('DefaultOutRegoFileName'),

        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutReportName = [ScubaConfig]::ScubaDefault('DefaultOutReportName'),

        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [switch]
        $MergeJson,

        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutJsonFileName = [ScubaConfig]::ScubaDefault('DefaultOutJsonFileName'),

        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [ValidateNotNullOrEmpty()]
        [ValidateSet($true, $false)]
        [switch]
        $Quiet,

        [Parameter(Mandatory = $false, ParameterSetName = 'Report')]
        [switch]
        $DarkMode
        )
        process {
            $ParentPath = Split-Path $PSScriptRoot -Parent
            $ScubaManifest = Import-PowerShellDataFile (Join-Path -Path $ParentPath -ChildPath 'ScubaGear.psd1' -Resolve)
            $ModuleVersion = $ScubaManifest.ModuleVersion

            if ($Version) {
                Write-Output("SCuBA Gear v$ModuleVersion")
                return
            }

            if ($ProductNames -eq '*'){
                $ProductNames = "teams", "exo", "defender", "aad", "sharepoint", "powerplatform"
            }

            # Create outpath if $Outpath does not exist
            if(-not (Test-Path -PathType "container" $OutPath))
            {
                New-Item -ItemType "Directory" -Path $OutPath | Out-Null
            }
            $OutFolderPath = $OutPath
            $ProductNames = $ProductNames | Sort-Object -Unique

            Remove-Resources
            Import-Resources # Imports Providers, RunRego, CreateReport, Connection

            # Authenticate
            $ConnectionParams = @{
                'LogIn' = $LogIn;
                'ProductNames' = $ProductNames;
                'M365Environment' = $M365Environment;
                'BoundParameters' = $PSBoundParameters;
            }

            # Rego Testing failsafe
            $TenantDetails = @{"DisplayName"="Rego Testing";}
            $TenantDetails = $TenantDetails | ConvertTo-Json -Depth 3
            if ($ExportProvider) {
                $ProdAuthFailed = Invoke-Connection @ConnectionParams
                if ($ProdAuthFailed.Count -gt 0) {
                    $Difference = Compare-Object $ProductNames -DifferenceObject $ProdAuthFailed -PassThru
                    if (-not $Difference) {
                        throw "All products were unable to establish a connection aborting execution"
                    }
                    else {
                        $ProductNames = $Difference
                    }
                }
                $TenantDetails = Get-TenantDetail -ProductNames $ProductNames -M365Environment $M365Environment
                $ProviderParams = @{
                    'ProductNames' = $ProductNames;
                    'M365Environment' = $M365Environment;
                    'TenantDetails' = $TenantDetails;
                    'ModuleVersion' = $ModuleVersion;
                    'OutFolderPath' = $OutFolderPath;
                    'OutProviderFileName' = $OutProviderFileName;
                    'BoundParameters' = $PSBoundParameters;
                }
                Invoke-ProviderList @ProviderParams
            }
            $FileName = Join-Path -Path $OutPath -ChildPath "$($OutProviderFileName).json"
            $SettingsExport = Get-Content $FileName | ConvertFrom-Json
            $TenantDetails = $SettingsExport.tenant_details
            $RegoParams = @{
                'ProductNames' = $ProductNames;
                'OPAPath' = $OPAPath;
                'ParentPath' = $ParentPath;
                'OutFolderPath' = $OutFolderPath;
                'OutProviderFileName' = $OutProviderFileName;
                'OutRegoFileName' = $OutRegoFileName;
            }
            $ReportParams = @{
                'ProductNames' = $ProductNames;
                'TenantDetails' = $TenantDetails;
                'ModuleVersion' = $ModuleVersion;
                'OutFolderPath' = $OutFolderPath;
                'OutProviderFileName' = $OutProviderFileName;
                'OutRegoFileName' = $OutRegoFileName;
                'OutReportName' = $OutReportName;
                'Quiet' = $Quiet;
                'DarkMode' = $DarkMode;
            }
            Invoke-RunRego @RegoParams
            Invoke-ReportCreation @ReportParams

            if ($MergeJson) {
                # Craft the complete json version of the output
                $JsonParams = @{
                    'ProductNames' = $ProductNames;
                    'OutFolderPath' = $OutFolderPath;
                    'OutProviderFileName' = $OutProviderFileName;
                    'TenantDetails' = $TenantDetails;
                    'ModuleVersion' = $ModuleVersion;
                    'OutJsonFileName' = $OutJsonFileName;
                }
                Merge-JsonOutput @JsonParams
            }
        }
    }

Export-ModuleMember -Function @(
    'Invoke-SCuBA',
    'Invoke-RunCached'
)

# SIG # Begin signature block
# MIIuwAYJKoZIhvcNAQcCoIIusTCCLq0CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAmuFGKFtrFXFdr
# idKQRf3DsnmJA1bEHKbEwIQXU4Z5A6CCE6MwggWQMIIDeKADAgECAhAFmxtXno4h
# 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
# eE4wggdXMIIFP6ADAgECAhANkQ8dPvvR0q3Ytt4H0T3aMA0GCSqGSIb3DQEBCwUA
# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE
# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz
# ODQgMjAyMSBDQTEwHhcNMjQwMTMwMDAwMDAwWhcNMjUwMTI5MjM1OTU5WjBfMQsw
# CQYDVQQGEwJVUzEdMBsGA1UECBMURGlzdHJpY3Qgb2YgQ29sdW1iaWExEzARBgNV
# BAcTCldhc2hpbmd0b24xDTALBgNVBAoTBENJU0ExDTALBgNVBAMTBENJU0EwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCT1y7uJCQax8JfiDEYgpiU9URj
# EXCTRqtZbDALM9rPUudiuM3mj6A1SUSAAWYv6DTsvGPvxyMI2Idg0mQunl4Ms9DJ
# yVwe5k4+Anj/73Nx1AbOPYP8xRZcD10FkctKGhV0PzvrDcwU15hsQWtiepFgg+bX
# fHkGMeu426oc69f43vKE43DiqKTf0/UBX/qgpj3JZvJ3zc1kilBOv4sBCksfCjbW
# tLZD0tqAgBsNPo3Oy5mQG31E1eZdTNvrdTnEXacSwb3k615z7mHy7nqBUkOruZ9E
# tnvC2qla+uL3ks91O/e/LnKzH9Lj1JmEBf6jwPN/MYR9Dymni4Mi3AQ8mpQMyFmi
# XcSHymibSNbtTMavpdBWjFfrcvPETX7krROUOoLzMQmNgHArceSh55tgvDRdSU5c
# WK3BTvK3l3mgCdgjre7XGYxV3W8apyxk5+RKfHdbv9cpRwpSuDnI8sHeqmB3fnfo
# Cr1PPu4WhKegt20CobhDVybiBdhDVqUdR53ful4N/coQOEHDrIExB5nJf9Pvdrza
# DyIGKAMIXD79ba5/rQEo+2cA66oJkPlvB5hEGI/jtDcYwDBgalbwB7Kc8zAAhl6+
# JvHfYpXOkppSfEQbaRXZI+LGXWQAFa5pJDfDEAyZSXprStgw594sWUOysp+UOxFe
# kSA4mBr0o1jVpdaulwIDAQABo4ICAzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8R
# hvv+YXsIiGX0TkIwHQYDVR0OBBYEFAmyTB5bcWyA+8+rq540jPRLJ1nYMD4GA1Ud
# IAQ3MDUwMwYGZ4EMAQQBMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNl
# cnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMw
# gbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0Rp
# Z2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5j
# cmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0
# ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3JsMIGUBggrBgEF
# BQcBAQSBhzCBhDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29t
# MFwGCCsGAQUFBzAChlBodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNl
# cnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJ
# BgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQAh2Jnt9IPoBvOQlYQUlCP9iJ5y
# XAvEWe1camOwedqMZsHEPpT2yd6+fMzPZmV3/bYJgaN2OrDS1snf62S7yc+AulVw
# PAXSp1lSAiFEbZ6PFEdEBIag9B65Mp/cvRtJsIWQIc//jWqFMHpkU6r3MW9YARRu
# vaIf5/0qlM4VEwn3lTf+jJdxhhyoOFTWWd3BrlMPcT06z6F6hFfyycQkZ3Y9wEJ3
# uOU9bCNLZL1HCjlKT+oI0WsgeRdbe2sYrnvv9NmDY9oEi8PEq+DGjiTgLbY5OcAX
# uUogPPw6gbcuNn8Hq6FFKPIQxaksB8dF8Gw4m2lQoUWESPRF8Zaq9lmZN3+QzA79
# yskfJtAFqz3gUP5wJBdNfi/u1sGbLI0QnJQkIKfFuz7DfDPldw0gIl05BIYwZBmj
# TpFRu1/+gIlP1Ul4L/wt9Lxk6pglObLsdxHP2UQrG30JaUN0gv3xZMBBByHGVVTe
# cyU4qwJ0ulMdv/kjHwh+m58uOF8gHXLfyBmOjYpohN3+l0rS0qdArZMNSmLTA7N8
# n3V3AZLKB//1yhPt++gR4pCFdXmgwYDDLRxjlV0cMsG1UeSQUdI0aieh/grg5TQO
# CergVXS5h3sz5U0ZQPWND41LJhA0gF2OGZNHdUc9+0dwTsfxAERrjaTdeZp0/rdZ
# 9iGBoiRsS4U86S8xkDGCGnMwghpvAgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNV
# BAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0
# IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENBMQIQDZEPHT770dKt
# 2LbeB9E92jANBglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgACh
# AoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAM
# BgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCAuuty8SszBOw9GUcvDDIcsqCef
# mOr1BV+QaMYtrJveezANBgkqhkiG9w0BAQEFAASCAgAOQ+OFSqMDsxoOERQ6/dSe
# /sbxJieeKJlkOKcs4euHW2HONhTmPFZgScsTWjMroRBT9lLRZRrXgWW4s2Lfgdoa
# O6uZeVqBLsVKxeBSMKXcKNRB4Bu6or7m4M5LR+dhe1/j7jd9nTDTrE6XiqX64mi8
# QRi6w8R9sJKWC/73vc7l+gAHGwkkP5cpkPzdqmQH3DiPaxvsYyPdVHPMXZzhvS3R
# q7Iip9CnzI4HG7wICH64m9NDX7BYyPCm+360wdq5eYeJ7P7hAcIJq3Z43wVSeju6
# iMt12PQqgAw4hJrilWepkGPmBggYQMdB5UcH3mUWKKQiMHvFVNersavwOEscJ50D
# QvRC30tPPQwoqq++pg66mdcgyXQ+8g8ERsLnJ8USNq90pKy93MWmorgTs9qPWgKm
# 8UvZYizao9Bw58i2++hVUcQkUz8XHAlom5+3Aa9CE//SrFfa9QBKhvWh0Mn7/50b
# eOJGdZccETnSwVZL74foirHAtIrzqnnuuwPtjq4q9xJBzpl7bLEG0QsRnwmL8ali
# OqB+7s6tKSZ1teoWkdAh/xVTI9MLd+uStpwGOpdknYnmzP10zSclY3t8u5FBlsFC
# Nld6G3Smv1IQX5Vm4NheJeLrWumJNWUTDIrmYleW2IlKltLTCtKc8aL6pAmRUler
# KrRkAozVkNzfQ0kbiKqgA6GCF0Awghc8BgorBgEEAYI3AwMBMYIXLDCCFygGCSqG
# SIb3DQEHAqCCFxkwghcVAgEDMQ8wDQYJYIZIAWUDBAIBBQAweAYLKoZIhvcNAQkQ
# AQSgaQRnMGUCAQEGCWCGSAGG/WwHATAxMA0GCWCGSAFlAwQCAQUABCBICP+nYKZz
# DmNGWpvKzqVe2Kq6J8cAuOC4F8eua2h8TQIRAMXXTMpEzz0BuNonLrkVV8gYDzIw
# MjQwNjEyMTMxNDQxWqCCEwkwggbCMIIEqqADAgECAhAFRK/zlJ0IOaa/2z9f5WEW
# MA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2Vy
# dCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNI
# QTI1NiBUaW1lU3RhbXBpbmcgQ0EwHhcNMjMwNzE0MDAwMDAwWhcNMzQxMDEzMjM1
# OTU5WjBIMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xIDAe
# BgNVBAMTF0RpZ2lDZXJ0IFRpbWVzdGFtcCAyMDIzMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEAo1NFhx2DjlusPlSzI+DPn9fl0uddoQ4J3C9Io5d6Oyqc
# Z9xiFVjBqZMRp82qsmrdECmKHmJjadNYnDVxvzqX65RQjxwg6seaOy+WZuNp52n+
# W8PWKyAcwZeUtKVQgfLPywemMGjKg0La/H8JJJSkghraarrYO8pd3hkYhftF6g1h
# bJ3+cV7EBpo88MUueQ8bZlLjyNY+X9pD04T10Mf2SC1eRXWWdf7dEKEbg8G45lKV
# tUfXeCk5a+B4WZfjRCtK1ZXO7wgX6oJkTf8j48qG7rSkIWRw69XloNpjsy7pBe6q
# 9iT1HbybHLK3X9/w7nZ9MZllR1WdSiQvrCuXvp/k/XtzPjLuUjT71Lvr1KAsNJvj
# 3m5kGQc3AZEPHLVRzapMZoOIaGK7vEEbeBlt5NkP4FhB+9ixLOFRr7StFQYU6mII
# E9NpHnxkTZ0P387RXoyqq1AVybPKvNfEO2hEo6U7Qv1zfe7dCv95NBB+plwKWEwA
# PoVpdceDZNZ1zY8SdlalJPrXxGshuugfNJgvOuprAbD3+yqG7HtSOKmYCaFxsmxx
# rz64b5bV4RAT/mFHCoz+8LbH1cfebCTwv0KCyqBxPZySkwS0aXAnDU+3tTbRyV8I
# pHCj7ArxES5k4MsiK8rxKBMhSVF+BmbTO77665E42FEHypS34lCh8zrTioPLQHsC
# AwEAAaOCAYswggGHMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBYGA1Ud
# JQEB/wQMMAoGCCsGAQUFBwMIMCAGA1UdIAQZMBcwCAYGZ4EMAQQCMAsGCWCGSAGG
# /WwHATAfBgNVHSMEGDAWgBS6FtltTYUvcyl2mi91jGogj57IbzAdBgNVHQ4EFgQU
# pbbvE+fvzdBkodVWqWUxo97V40kwWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDovL2Ny
# bDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0UlNBNDA5NlNIQTI1NlRp
# bWVTdGFtcGluZ0NBLmNybDCBkAYIKwYBBQUHAQEEgYMwgYAwJAYIKwYBBQUHMAGG
# GGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBYBggrBgEFBQcwAoZMaHR0cDovL2Nh
# Y2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0UlNBNDA5NlNIQTI1
# NlRpbWVTdGFtcGluZ0NBLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAgRrW3qCptZgX
# vHCNT4o8aJzYJf/LLOTN6l0ikuyMIgKpuM+AqNnn48XtJoKKcS8Y3U623mzX4WCc
# K+3tPUiOuGu6fF29wmE3aEl3o+uQqhLXJ4Xzjh6S2sJAOJ9dyKAuJXglnSoFeoQp
# mLZXeY/bJlYrsPOnvTcM2Jh2T1a5UsK2nTipgedtQVyMadG5K8TGe8+c+njikxp2
# oml101DkRBK+IA2eqUTQ+OVJdwhaIcW0z5iVGlS6ubzBaRm6zxbygzc0brBBJt3e
# WpdPM43UjXd9dUWhpVgmagNF3tlQtVCMr1a9TMXhRsUo063nQwBw3syYnhmJA+rU
# kTfvTVLzyWAhxFZH7doRS4wyw4jmWOK22z75X7BC1o/jF5HRqsBV44a/rCcsQdCa
# M0qoNtS5cpZ+l3k4SF/Kwtw9Mt911jZnWon49qfH5U81PAC9vpwqbHkB3NpE5jre
# ODsHXjlY9HxzMVWggBHLFAx+rrz+pOt5Zapo1iLKO+uagjVXKBbLafIymrLS2Dq4
# sUaGa7oX/cR3bBVsrquvczroSUa31X/MtjjA2Owc9bahuEMs305MfR5ocMB3CtQC
# 4Fxguyj/OOVSWtasFyIjTvTs0xf7UGv/B3cfcZdEQcm4RtNsMnxYL2dHZeUbc7aZ
# +WssBkbvQR7w8F/g29mtkIBEr4AQQYowggauMIIElqADAgECAhAHNje3JFR82Ees
# /ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxE
# aWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMT
# GERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAz
# MjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5j
# LjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBU
# aW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG
# hjUGSbPBPXJJUVXHJQPE8pE3qZdRodbSg9GeTKJtoLDMg/la9hGhRBVCX6SI82j6
# ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9/UO0hNoR8XOxs+4rgISKIhjf69o9xBd/
# qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXnHwZljZQp09nsad/ZkIdGAHvbREGJ3Hxq
# V3rwN3mfXazL6IRktFLydkf3YYMZ3V+0VAshaG43IbtArF+y3kp9zvU5EmfvDqVj
# bOSmxR3NNg1c1eYbqMFkdECnwHLFuk4fsbVYTXn+149zk6wsOeKlSNbwsDETqVcp
# licu9Yemj052FVUmcJgmf6AaRyBD40NjgHt1biclkJg6OBGz9vae5jtb7IHeIhTZ
# girHkr+g3uM+onP65x9abJTyUpURK1h0QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZG
# s506o9UD4L/wojzKQtwYSH8UNM/STKvvmz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHz
# NklNiyDSLFc1eSuo80VgvCONWPfcYd6T/jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2
# ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJ
# ASgADoRU7s7pXcheMBK9Rp6103a50g5rmQzSM7TNsQIDAQABo4IBXTCCAVkwEgYD
# VR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUuhbZbU2FL3MpdpovdYxqII+eyG8w
# HwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGG
# MBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcw
# AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8v
# Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBD
# BgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNl
# cnRUcnVzdGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgB
# hv1sBwEwDQYJKoZIhvcNAQELBQADggIBAH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4Q
# TRPPMFPOvxj7x1Bd4ksp+3CKDaopafxpwc8dB+k+YMjYC+VcW9dth/qEICU0MWfN
# thKWb8RQTGIdDAiCqBa9qVbPFXONASIlzpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1g
# tqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQcAp876i8dU+6WvepELJd6f8oVInw1Ypx
# dmXazPByoyP6wCeCRK6ZJxurJB4mwbfeKuv2nrF5mYGjVoarCkXJ38SNoOeY+/um
# nXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+jSbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+U
# zTl63f8lY5knLD0/a6fxZsNBzU+2QJshIUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhz
# q6YBT70/O3itTK37xJV77QpfMzmHQXh6OOmc4d0j/R0o08f56PGYX/sr2H7yRp11
# LB4nLCbbbxV7HhmLNriT1ObyF5lZynDwN7+YAN8gFk8n+2BnFqFmut1VwDophrCY
# oCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvk
# dgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3
# OBqhK/bt1nz8MIIFjTCCBHWgAwIBAgIQDpsYjvnQLefv21DiCEAYWjANBgkqhkiG
# 9w0BAQwFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkw
# FwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1
# cmVkIElEIFJvb3QgQ0EwHhcNMjIwODAxMDAwMDAwWhcNMzExMTA5MjM1OTU5WjBi
# MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
# d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg
# RzQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAi
# MGkz7MKnJS7JIT3yithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnny
# yhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE
# 5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm
# 7nfISKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5
# w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsD
# dV14Ztk6MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1Z
# XUJ2h4mXaXpI8OCiEhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS0
# 0mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hk
# pjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m8
# 00ERElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+i
# sX4KJpn15GkvmB0t9dmpsh3lGwIDAQABo4IBOjCCATYwDwYDVR0TAQH/BAUwAwEB
# /zAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wHwYDVR0jBBgwFoAUReui
# r/SSy4IxLVGLp6chnfNtyA8wDgYDVR0PAQH/BAQDAgGGMHkGCCsGAQUFBwEBBG0w
# azAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUF
# BzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVk
# SURSb290Q0EuY3J0MEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2lj
# ZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwEQYDVR0gBAowCDAG
# BgRVHSAAMA0GCSqGSIb3DQEBDAUAA4IBAQBwoL9DXFXnOF+go3QbPbYW1/e/Vwe9
# mqyhhyzshV6pGrsi+IcaaVQi7aSId229GhT0E0p6Ly23OO/0/4C5+KH38nLeJLxS
# A8hO0Cre+i1Wz/n096wwepqLsl7Uz9FDRJtDIeuWcqFItJnLnU+nBgMTdydE1Od/
# 6Fmo8L8vC6bp8jQ87PcDx4eo0kxAGTVGamlUsLihVo7spNU96LHc/RzY9HdaXFSM
# b++hUD38dglohJ9vytsgjTVgHAIDyyCwrFigDkBjxZgiwbJZ9VVrzyerbHbObyMt
# 9H5xaiNrIv8SuFQtJ37YOtnwtoeW/VvRXKwYw02fc7cBqZ9Xql4o4rmUMYIDdjCC
# A3ICAQEwdzBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4x
# OzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGlt
# ZVN0YW1waW5nIENBAhAFRK/zlJ0IOaa/2z9f5WEWMA0GCWCGSAFlAwQCAQUAoIHR
# MBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjQw
# NjEyMTMxNDQxWjArBgsqhkiG9w0BCRACDDEcMBowGDAWBBRm8CsywsLJD4JdzqqK
# ycZPGZzPQDAvBgkqhkiG9w0BCQQxIgQgTmCV8oWJFcs+BFN8+4yDQ1YRwJG8jxEA
# MurxMufsDGswNwYLKoZIhvcNAQkQAi8xKDAmMCQwIgQg0vbkbe10IszR1EBXaEE2
# b4KK2lWarjMWr00amtQMeCgwDQYJKoZIhvcNAQEBBQAEggIAAhC/igGTGY+VGIVJ
# VIxFol8Np8uVBLzqRyya0i34e6eBnobateSwxzLp2Ukd8yeIrpX7HSyu1AAU/c0s
# 2AMMKzdwaWSabfSzt1cjwDVNak9/0Sb63itdhYqCAbMKOd4RkKfL/0Yuqz9vsOvs
# 9svA13RJ5YYzVB1Dk7NXLJ37cUQ5E5ibAMfDS232t6X1JGz7+dxiHnU6dGD3nQua
# oMlC6bheBzdDv6OnJp5bQ9q3g65vSeMUB9mS/CGMoTvKo06ywjkxwDkP/LFtGRJg
# oxA60/Mh0T9N2vvzaupxYG4Xhn8i6V88AA5wgaUJHYUuckY82EJdWDwEXr3matLn
# 7p9H0aAfCMG7W7x+yhFLnl4/uiEjfcFdT/Ml36E6KgzJANDCo56UCF8scrL2Hk2n
# gmVUTT+VkjnEACWgbD/EIOk8WTdzD/wrhjmKVK+r95XKGV3RRVp42PKDVEIXn+zA
# AawFS4MKyDU8uqLYIJTalF4iC8eEA5FmimfJpsJf7+Ihmdp2QfYBxDAGvYp3YkED
# LilL31yvfdWQJ3AAC02JDzr8PB/q47dqb3DcJrLp/R+H+lqJmmZ+MSfvrFmSmFdz
# HEf4pNQm0JNfRpVx7VcGmrDQ23Z0GYTQx5+LqDbw8p4y9mdNdmKEsJS0kd+pVHUG
# VjbdboK0vnyGKCcHrR+PH2we1O0=
# SIG # End signature block