Framework/Helpers/ContextHelper.ps1

<#
.Description
# Context class for indenity details.
# Provides functionality to login, create context, get token for api calls
#>

using namespace Microsoft.IdentityModel.Clients.ActiveDirectory

class ContextHelper {
    
    static hidden [Context] $currentContext;
    static hidden [bool] $IsOAuthScan;
    static hidden [bool] $PromptForLogin;
    #This will be used to carry current org under current context.
    static hidden [string] $orgName;
    static hidden [bool] $IsBatchScan;
    static hidden [int] $PSVersion = $null;
    static hidden $appObj = $null;
    static hidden $Account = $null;
    static hidden $IsPATUsed = $false;

    ContextHelper()
    {
        if(-not [string]::IsNullOrWhiteSpace($env:RefreshToken) -and -not [string]::IsNullOrWhiteSpace($env:ClientSecret))  # this if block will be executed for OAuth based scan
        {
            [ContextHelper]::IsOAuthScan = $true
        }
        if (![ContextHelper]::PSVersion) {
            [ContextHelper]::PSVersion = ($global:PSVersionTable).PSVersion.major 
        }
    }

    ContextHelper([bool] $IsBatchScan)
    {
        if(-not [string]::IsNullOrWhiteSpace($env:RefreshToken) -and -not [string]::IsNullOrWhiteSpace($env:ClientSecret))  # this if block will be executed for OAuth based scan
        {
            [ContextHelper]::IsOAuthScan = $true
        }
        [ContextHelper]::IsBatchScan=$true;
        if (![ContextHelper]::PSVersion) {
            [ContextHelper]::PSVersion = ($global:PSVersionTable).PSVersion.major 
        }
    }

    hidden static [PSObject] GetCurrentContext()
    {
        return [ContextHelper]::GetCurrentContext($false);
    }

    hidden static [PSObject] GetCurrentContext([bool]$authNRefresh)
    {
        if( (-not [ContextHelper]::currentContext) -or $authNRefresh -or [ContextHelper]::PromptForLogin)
        {
            [ContextHelper]::IsPATUsed = $false
            $clientId = [Constants]::DefaultClientId ;          
            $replyUri = [Constants]::DefaultReplyUri; 
            $adoResourceId = [Constants]::DefaultADOResourceId;
            [AuthenticationContext] $ctx = $null;

            $ctx = [AuthenticationContext]::new("https://login.windows.net/common");

            $result = $null;

            if([ContextHelper]::IsOAuthScan) { # this if block will be executed for OAuth based scan
                $tokenInfo = [ContextHelper]::GetOAuthAccessToken()
                [ContextHelper]::ConvertToContextObject($tokenInfo)
            }
            else {
                if ([ContextHelper]::PSVersion -gt 5) {
                    [string[]] $Scopes = "$adoResourceId/.default";
                    [Microsoft.Identity.Client.IPublicClientApplication] $app = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($ClientId).Build();
                    if(![ContextHelper]::appObj) {
                        [ContextHelper]::appObj = $app
                    }

                    if (![ContextHelper]::Account) {
                        [ContextHelper]::Account = $app.GetAccountsAsync().GetAwaiter().GetResult() | Select-Object -First 1
                    }
                    $tokenSource = New-Object System.Threading.CancellationTokenSource
                    $taskAuthenticationResult=$null
                    try {
                        if ( !$authNRefresh -and [ContextHelper]::PromptForLogin)
                        {
                            if ([ContextHelper]::PromptForLogin)
                            {
                                $AquireTokenParameters = $app.AcquireTokenInteractive($Scopes)
                                $taskAuthenticationResult = $AquireTokenParameters.ExecuteAsync($tokenSource.Token)
                            }
                            else {
                                $AquireTokenParameters = $app.AcquireTokenSilent($Scopes, [ContextHelper]::Account)
                                $taskAuthenticationResult = $AquireTokenParameters.ExecuteAsync($tokenSource.Token)
                                if ($taskAuthenticationResult.exception.message -like "*errors occurred*") {
                                    $AquireTokenParameters = $app.AcquireTokenInteractive($Scopes)
                                    $taskAuthenticationResult = $AquireTokenParameters.ExecuteAsync($tokenSource.Token)
                                }
                            }
                        }
                        else {
                            if ([ContextHelper]::appObj) {
                                $AquireTokenParameters = [ContextHelper]::appObj.AcquireTokenSilent($Scopes, [ContextHelper]::Account)
                            }
                            else {
                                $AquireTokenParameters = $app.AcquireTokenSilent($Scopes, [ContextHelper]::Account) 
                            }
                            $taskAuthenticationResult = $AquireTokenParameters.ExecuteAsync($tokenSource.Token)
                            if ($taskAuthenticationResult.exception.message -like "*errors occurred*") {
                                $AquireTokenParameters = $app.AcquireTokenInteractive($Scopes)
                                $taskAuthenticationResult = $AquireTokenParameters.ExecuteAsync($tokenSource.Token)
                            }
                        }
                    }
                    catch {
                        $AquireTokenParameters = $app.AcquireTokenInteractive($Scopes)
                        $taskAuthenticationResult = $AquireTokenParameters.ExecuteAsync($tokenSource.Token)
                    }
                    if ($taskAuthenticationResult.Result) {
                        $result = $taskAuthenticationResult.Result;
                    }
                    
                    if (![ContextHelper]::Account) {
                        [ContextHelper]::Account = $app.GetAccountsAsync().GetAwaiter().GetResult() | Select-Object -First 1
                    }
                    [ContextHelper]::appObj = $app;
                }
                else {
                    if ( !$authNRefresh -and [ContextHelper]::PromptForLogin) {
                        if ([ContextHelper]::PromptForLogin) {
                        $PromptBehavior = [Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior]::Always
                        $PlatformParameters = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters -ArgumentList $PromptBehavior
                        $result = $ctx.AcquireTokenAsync($adoResourceId, $clientId, [Uri]::new($replyUri),$PlatformParameters).Result;
                        [ContextHelper]::PromptForLogin = $false
                        }
                        else {
                        $PromptBehavior = [Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior]::Auto
                        $PlatformParameters = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters -ArgumentList $PromptBehavior
                        $result = $ctx.AcquireTokenAsync($adoResourceId, $clientId, [Uri]::new($replyUri),$PlatformParameters).Result;
                        }
                    }
                    else {
                        $PromptBehavior = [Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior]::Auto
                        $PlatformParameters = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters -ArgumentList $PromptBehavior
                        $result = $ctx.AcquireTokenAsync($adoResourceId, $clientId, [Uri]::new($replyUri),$PlatformParameters).Result;
                    }
                }
                [ContextHelper]::ConvertToContextObject($result)
            }
        }
        return [ContextHelper]::currentContext
    }
    
    hidden static [PSObject] GetCurrentContext([System.Security.SecureString] $PATToken)
    {
        if(-not [ContextHelper]::currentContext)
        {
            [ContextHelper]::IsPATUsed = $true;
            [ContextHelper]::ConvertToContextObject($PATToken)
        }
        return [ContextHelper]::currentContext
    }

    hidden static [PSObject] GetOAuthAccessToken() {
        $tokenInfo = @{};
        try{
            $url = "https://app.vssps.visualstudio.com/oauth2/token"
            # exchange refresh token with new access token
            $body = "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=$($env:ClientSecret)&grant_type=refresh_token&assertion=$($env:RefreshToken)&redirect_uri=https://localhost/"
        
            $res = Invoke-WebRequest -Uri $url -ContentType "application/x-www-form-urlencoded" -Method POST -Body $body
            $response = $res.Content | ConvertFrom-Json

            $tokenInfo['AccessToken'] = $response.access_token
            $expiry = $response.expires_in
            $request_time = get-date
            $tokenInfo['ExpiresOn'] = $request_time.AddSeconds($expiry)
            $refreshToken = ConvertTo-SecureString  $response.refresh_token -AsPlainText -Force

            #Update refresh token if it is expiring in next 1 day
            $updateTokenInKV = $false
            $secretName = "RefreshTokenForADOScan"
            $tokenSecret = Get-AzKeyVaultSecret -VaultName $env:KeyVaultName -Name $secretName
            if (-not [string]::IsNullOrEmpty($tokenSecret) -and [Helpers]::CheckMember($tokenSecret,"Expires")) {
                if ($tokenSecret.Expires -le [DateTime]::Now.AddDays(1))
                {
                    $updateTokenInKV = $true
                }
            }
            else {
                $updateTokenInKV = $true
            }
            if ($updateTokenInKV -eq $true)
            {
                $RefreshTokenExpiresInDays = [Constants]::RefreshTokenExpiresInDays;
                $ExpiryDate = [DateTime]::Now.AddDays($RefreshTokenExpiresInDays)
                Set-AzKeyVaultSecret -VaultName $env:KeyVaultName -Name $secretName -SecretValue $refreshToken -Expires $ExpiryDate | out-null
            }
        }
        catch{
            write-Host "Error fetching OAuth access token"
            Write-Host $_
            return $null
        }
        return $tokenInfo
    }

    static [string] GetAccessToken([string] $resourceAppIdUri) {
            return [ContextHelper]::GetAccessToken()   
    }

    static [string] GetAccessToken()
    {
        if([ContextHelper]::currentContext)
        {
            # Validate if token is PAT using lenght (PAT has lengh of 52), if PAT dont go to refresh login session.
            #TODO: Change code to find token type supplied PAT or login session token
            #if token expiry is within 2 min, refresh. ([ContextHelper]::currentContext.AccessToken.length -ne 52)
            if ( [ContextHelper]::IsPATUsed -eq $false -and ([ContextHelper]::currentContext.TokenExpireTimeLocal -le [DateTime]::Now.AddMinutes(2)))
            {
                [ContextHelper]::GetCurrentContext($true);
            }
            return  [ContextHelper]::currentContext.AccessToken
        }
        else
        {
            return $null
        }
    }
    
    static [string] GetAccessToken([string] $Uri, [string] $tenantId) 
    {
        $rmContext = Get-AzContext
        if (-not $rmContext) {
            throw ([SuppressedException]::new(("No Azure login found"), [SuppressedExceptionType]::InvalidOperation))
        }
        
        if ([string]::IsNullOrEmpty($tenantId) -and [Helpers]::CheckMember($rmContext,"Tenant")) {
            $tenantId = $rmContext.Tenant.Id
        }
        
        $authResult = [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.AuthenticationFactory.Authenticate(
        $rmContext.Account,
        $rmContext.Environment,
        $tenantId,
        [System.Security.SecureString] $null,
        "Never",
        $null,
        $Uri);
        
        if (-not ($authResult -and (-not [string]::IsNullOrWhiteSpace($authResult.AccessToken)))) {
          throw ([SuppressedException]::new(("Unable to get access token. Authentication Failed."), [SuppressedExceptionType]::Generic))
        }
        return $authResult.AccessToken;
    }

    
    static [string] GetGraphAccessToken($useAzContext)
    {
        $accessToken = ''
        try
        {   
            Write-Host "Graph access is required to evaluate some controls. Attempting to acquire graph token." -ForegroundColor Cyan
            # In CA mode, we use azure context to fetch the graph access token.
            if ($useAzContext)
            {
                #getting azure context because graph access token requires azure environment details.
                $Context = @(Get-AzContext -ErrorAction SilentlyContinue )
                if ($Context.count -eq 0)  
                {
                    
                    Connect-AzAccount -ErrorAction Stop
                    $Context = @(Get-AzContext -ErrorAction SilentlyContinue)
                }

                if ($null -eq $Context)  
                {
                    throw "Unable to acquire Graph token. The signed-in account may not have Graph permission. Control results for controls that depend on AAD group expansion may not be accurate."
                }
                else
                {
                    $graphUri = "https://graph.microsoft.com"
                    $authResult = [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.AuthenticationFactory.Authenticate(
                    $Context.Account,
                    $Context.Environment,
                    $Context.Tenant.Id,
                    [System.Security.SecureString] $null,
                    "Never",
                    $null,
                    $graphUri);

                    if (-not ($authResult -and (-not [string]::IsNullOrWhiteSpace($authResult.AccessToken))))
                    {
                        throw ([SuppressedException]::new(("Unable to acquire Graph token. The signed-in account may not have Graph permission. Control results for controls that depend on AAD group expansion may not be accurate."), [SuppressedExceptionType]::Generic))
                    }

                    $accessToken = $authResult.AccessToken;
                }
            }
            else 
            {
                # generating graph access token using default VSTS client.
                $clientId = [Constants]::DefaultClientId;          
                $replyUri = [Constants]::DefaultReplyUri; 
                $adoResourceId = "https://graph.microsoft.com/";
                                         
                if ([ContextHelper]::PSVersion -gt 5) {
                    $result = [ContextHelper]::GetGraphAccess()
                }
                else {
                    [AuthenticationContext] $ctx = [AuthenticationContext]::new("https://login.windows.net/common");
                    [AuthenticationResult] $result = $null;
                    $PromptBehavior = [Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior]::Auto
                    $PlatformParameters = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters -ArgumentList $PromptBehavior
                    $result = $ctx.AcquireTokenAsync($adoResourceId, $clientId, [Uri]::new($replyUri),$PlatformParameters).Result;
                }
                $accessToken = $result.AccessToken
            }
            Write-Host "Successfully acquired graph access token." -ForegroundColor Cyan
        }
        catch
        {
            Write-Host "Unable to acquire Graph token. The signed-in account may not have Graph permission. Control results for controls that depend on AAD group expansion may not be accurate." -ForegroundColor Red
            Write-Host "Continuing without graph access." -ForegroundColor Yellow
            return $null
        }

        return $accessToken;
    }

    static [string] GetDataExplorerAccessToken($useAzContext)
    {
        $accessToken = ''        
        try
        {   
            Write-Host "Graph access is required to evaluate some controls. Attempting to acquire graph token." -ForegroundColor Cyan
            # generating graph access token using default VSTS client.
            if ($useAzContext)
            {
                #getting azure context because graph access token requires azure environment details.
                $Context = @(Get-AzContext -ErrorAction SilentlyContinue )
                if ($Context.count -eq 0)  
                {                    
                    Connect-AzAccount -ErrorAction Stop
                    $Context = @(Get-AzContext -ErrorAction SilentlyContinue)
                }

                if ($null -eq $Context)  
                {
                    throw "Unable to acquire Graph token. The signed-in account may not have Graph permission. Control results for controls that depend on AAD group expansion may not be accurate."
                }
                else
                {
                    $graphUri = "https://help.kusto.windows.net"
                    $authResult = [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.AuthenticationFactory.Authenticate(
                    $Context.Account,
                    $Context.Environment,
                    $Context.Tenant.Id,
                    [System.Security.SecureString] $null,
                    "Never",
                    $null,
                    $graphUri);

                    if (-not ($authResult -and (-not [string]::IsNullOrWhiteSpace($authResult.AccessToken))))
                    {
                        throw ([SuppressedException]::new(("Unable to acquire Graph token. The signed-in account may not have Graph permission. Control results for controls that depend on AAD group expansion may not be accurate."), [SuppressedExceptionType]::Generic))
                    }

                    $accessToken = $authResult.AccessToken;
                }
            }
            else{
                $clientId = [Constants]::DefaultClientId;          
                $replyUri = [Constants]::DefaultReplyUri; 
                $adoResourceId = "https://help.kusto.windows.net";                                         
                if ([ContextHelper]::PSVersion -gt 5) {
                    $result = [ContextHelper]::GetGraphAccessForDataExplorer()
                    $accessToken = $result.AccessToken
                }
                else 
                {
                    # generating data explorer token using default VSTS client.
                    # this will generate token for local user and generates popup for user login.
                    $clientId = [Constants]::DefaultClientId;          
                    $replyUri = [Constants]::DefaultReplyUri; 
                    $adoResourceId = "https://help.kusto.windows.net";                                         
                    if ([ContextHelper]::PSVersion -gt 5) {
                        $result = [ContextHelper]::GetGraphAccess()
                    }
                    else {
                        [AuthenticationContext] $ctx = [AuthenticationContext]::new("https://login.windows.net/common");
                        [AuthenticationResult] $result = $null;
                        $PromptBehavior = [Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior]::Always
                        $PlatformParameters = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters -ArgumentList $PromptBehavior
                        $result = $ctx.AcquireTokenAsync($adoResourceId, $clientId, [Uri]::new($replyUri),$PlatformParameters).Result;
                        [ContextHelper]::PromptForLogin = $false
                    }
                    $accessToken = $result.AccessToken
                }
            }
        }
        catch
        {
            return $null
        }

        return $accessToken;
    }

    static [string] GetLAWSAccessToken()
    {
        $accessToken = ''
        try
        {              
            #getting azure context because graph access token requires azure environment details.
            $Context = @(Get-AzContext -ErrorAction SilentlyContinue )
            if ($Context.count -eq 0)  
            {                    
                Connect-AzAccount -ErrorAction Stop
                $Context = @(Get-AzContext -ErrorAction SilentlyContinue)
            }

            if ($null -eq $Context)  
            {
                throw "Unable to acquire Graph token. The signed-in account may not have Graph permission. Control results for controls that depend on AAD group expansion may not be accurate."
            }
            else
            {                
                $authResult = [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.AuthenticationFactory.Authenticate(
                $Context.Account,
                $Context.Environment,
                $Context.Tenant.Id,
                [System.Security.SecureString] $null,
                "Never",
                $null,
                "https://api.loganalytics.io/");

                if (-not ($authResult -and (-not [string]::IsNullOrWhiteSpace($authResult.AccessToken))))
                {
                    throw ([SuppressedException]::new(("Unable to acquire Graph token. The signed-in account may not have Graph permission. Control results for controls that depend on AAD group expansion may not be accurate."), [SuppressedExceptionType]::Generic))
                }

                $accessToken = $authResult.AccessToken;
            }                                  
        }
        catch
        {
            Write-Host "Unable to acquire Graph token. The signed-in account may not have Graph permission. Control results for controls that depend on AAD group expansion may not be accurate." -ForegroundColor Red
            Write-Host "Continuing without graph access." -ForegroundColor Yellow
            return $null
        }

        return $accessToken;        
    }

    hidden static [PSobject] GetGraphAccess()
    {
        $rootConfigPath = [Constants]::AzSKAppFolderPath;
        $azskSettings = (Get-Content -Raw -Path (Join-Path $rootConfigPath "AzSKSettings.json")) | ConvertFrom-Json
        if ([ContextHelper]::IsPATUsed -and $azskSettings -and $azskSettings.LASource -ne "CICD") {
            $Context = @(Get-AzContext -ErrorAction SilentlyContinue)
            if ($null -eq $Context -or $Context.count -eq 0) {
                Connect-AzAccount -ErrorAction Stop
                $Context = @(Get-AzContext -ErrorAction SilentlyContinue)
            }
            if ($null -eq $Context) {
                throw 
            }
            else {
                $graphUri = "https://graph.microsoft.com"
                $authResult = [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.AuthenticationFactory.Authenticate(
                $Context.Account,
                $Context.Environment,
                $Context.Tenant.Id,
                [System.Security.SecureString] $null,
                "Never",
                $null,
                $graphUri);

                return $authResult;
            }
        }
        else {
            $ClientId = [Constants]::DefaultClientId
            [Microsoft.Identity.Client.IPublicClientApplication] $appGrapth = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($ClientId).Build();
            if (![ContextHelper]::Account) {
                [ContextHelper]::Account = $appGrapth.GetAccountsAsync().GetAwaiter().GetResult() | Select-Object -First 1
            }
            $tokenSource = New-Object System.Threading.CancellationTokenSource
            $taskAuthenticationResult=$null
            $AquireTokenParameters = $null;
            [string[]] $Scopes = "https://graph.microsoft.com/.default";

            $AquireTokenParameters = [ContextHelper]::appObj.AcquireTokenSilent($Scopes, [ContextHelper]::Account)
            try {
                $taskAuthenticationResult = $AquireTokenParameters.ExecuteAsync($tokenSource.Token)
                if ( [Helpers]::CheckMember($taskAuthenticationResult, "exception.message") -and ($taskAuthenticationResult.exception.message -like "*errors occurred*")) {
                    $AquireTokenParameters = $appGrapth.AcquireTokenInteractive($Scopes)
                    $taskAuthenticationResult = $AquireTokenParameters.ExecuteAsync($tokenSource.Token)
                }
            }
            catch {
                $AquireTokenParameters = $appGrapth.AcquireTokenInteractive($Scopes)
                $taskAuthenticationResult = $AquireTokenParameters.ExecuteAsync($tokenSource.Token)
            }
        }
        
        return $taskAuthenticationResult.result;
    }

    hidden static [PSobject] GetGraphAccessForDataExplorer()
    {
        $rootConfigPath = [Constants]::AzSKAppFolderPath;
        $azskSettings = (Get-Content -Raw -Path (Join-Path $rootConfigPath "AzSKSettings.json")) | ConvertFrom-Json
        if ([ContextHelper]::IsPATUsed -and $azskSettings -and $azskSettings.LASource -ne "CICD") {
            $Context = @(Get-AzContext -ErrorAction SilentlyContinue)
            if ($null -eq $Context -or $Context.count -eq 0) {
                Connect-AzAccount -ErrorAction Stop
                $Context = @(Get-AzContext -ErrorAction SilentlyContinue)
            }
            if ($null -eq $Context) {
                throw 
            }
            else {
                $graphUri = "https://help.kusto.windows.net"
                $authResult = [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.AuthenticationFactory.Authenticate(
                $Context.Account,
                $Context.Environment,
                $Context.Tenant.Id,
                [System.Security.SecureString] $null,
                "Never",
                $null,
                $graphUri);

                return $authResult;
            }
        }
        else {
            $ClientId = [Constants]::DefaultClientId
            [Microsoft.Identity.Client.IPublicClientApplication] $appGrapth = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($ClientId).Build();
            if (![ContextHelper]::Account) {
                [ContextHelper]::Account = $appGrapth.GetAccountsAsync().GetAwaiter().GetResult() | Select-Object -First 1
            }
            $tokenSource = New-Object System.Threading.CancellationTokenSource
            $taskAuthenticationResult=$null
            $AquireTokenParameters = $null;
            [string[]] $Scopes = "https://help.kusto.windows.net";

            $AquireTokenParameters = [ContextHelper]::appObj.AcquireTokenSilent($Scopes, [ContextHelper]::Account)
            try {
                $taskAuthenticationResult = $AquireTokenParameters.ExecuteAsync($tokenSource.Token)
                if ( [Helpers]::CheckMember($taskAuthenticationResult, "exception.message") -and ($taskAuthenticationResult.exception.message -like "*errors occurred*")) {
                    $AquireTokenParameters = $appGrapth.AcquireTokenInteractive($Scopes)
                    $taskAuthenticationResult = $AquireTokenParameters.ExecuteAsync($tokenSource.Token)
                }
            }
            catch {
                $AquireTokenParameters = $appGrapth.AcquireTokenInteractive($Scopes)
                $taskAuthenticationResult = $AquireTokenParameters.ExecuteAsync($tokenSource.Token)
            }
        }
        
        return $taskAuthenticationResult.result;
    }

    hidden [OrganizationContext] SetContext([string] $organizationName)
    {
        if((-not [string]::IsNullOrEmpty($organizationName)))
              {
                     $OrganizationContext = [OrganizationContext]@{
                           OrganizationId = $organizationName;
                           Scope = "/Organization/$organizationName";
                           OrganizationName = $organizationName;
                     };
                     # $organizationId contains the organization name (due to framework).
                     [ContextHelper]::orgName = $organizationName;
                     [ContextHelper]::GetCurrentContext()                  
              }
              else
              {
                     throw [SuppressedException] ("OrganizationName name [$organizationName] is either malformed or incorrect.")
        }
        return $OrganizationContext;
    }

    hidden [OrganizationContext] SetContext([string] $organizationName, [System.Security.SecureString] $PATToken)
    {
        if((-not [string]::IsNullOrEmpty($organizationName)))
              {
                     $OrganizationContext = [OrganizationContext]@{
                           OrganizationId = $organizationName;
                           Scope = "/Organization/$organizationName";
                           OrganizationName = $organizationName;
                     };
                     # $organizationId contains the organization name (due to framework).
                     [ContextHelper]::orgName = $organizationName;
                     [ContextHelper]::GetCurrentContext($PATToken)         
              }
              else
              {
                     throw [SuppressedException] ("OrganizationName name [$organizationName] is either malformed or incorrect.")
        }
        return $OrganizationContext;
    }

    static [void] ResetCurrentContext()
    {
        
    }

    hidden static ConvertToContextObject([PSObject] $context)
    {
        $contextObj = [Context]::new()
        # We do not get ADO organization id as part of current context. Hence appending org name to both id and name param.
        $contextObj.Organization = [Organization]::new()
        $contextObj.Organization.Id = [ContextHelper]::orgName
        $contextObj.Organization.Name = [ContextHelper]::orgName

        if([ContextHelper]::IsOAuthScan) { # this if block will be executed for OAuth based scan
            $contextObj.Account.Id = [ContextHelper]::GetOAuthUserIdentity($context.AccessToken, $contextObj.Organization.Name)
            $contextObj.AccessToken = $context.AccessToken
            $contextObj.TokenExpireTimeLocal = $context.ExpiresOn
        }
        else {
            if ([ContextHelper]::PSVersion -gt 5) {
                $contextObj.Account.Id = $context.Account.username
            }
            else {
                $contextObj.Account.Id = $context.UserInfo.DisplayableId
            }
            $contextObj.Tenant.Id = $context.TenantId
            $contextObj.AccessToken = $context.AccessToken

            $contextObj.TokenExpireTimeLocal = $context.ExpiresOn.LocalDateTime
            #$contextObj.AccessToken = ConvertTo-SecureString -String $context.AccessToken -asplaintext -Force
        }
        [ContextHelper]::currentContext = $contextObj
    }
    
    hidden static [string] GetOAuthUserIdentity($accessToken, $orgName)
    {
        $apiURL = "https://dev.azure.com/{0}/_apis/connectionData" -f $orgName
        $headers =@{
            Authorization = "Bearer $accesstoken";
            "Content-Type"="application/json"
        };
        try{
            $responseObj = Invoke-RestMethod -Method Get -Uri $apiURL -Headers $headers -UseBasicParsing
            $descriptor = $responseObj.authenticatedUser.descriptor
            $userId = ($descriptor -split '\\')[-1]
            return $userId
        }
        catch{
            return ""
        }
    }

    hidden static ConvertToContextObject([System.Security.SecureString] $patToken)
    {
        $contextObj = [Context]::new()
        $contextObj.Account.Id = [string]::Empty
        $contextObj.Tenant.Id =  [string]::Empty
        $contextObj.AccessToken = [System.Net.NetworkCredential]::new("", $patToken).Password
        
        # We do not get ADO organization Id as part of current context. Hence appending org name to both Id and Name param.
        $contextObj.Organization = [Organization]::new()
        $contextObj.Organization.Id = [ContextHelper]::orgName
        $contextObj.Organization.Name = [ContextHelper]::orgName 

        #$contextObj.AccessToken = $patToken
        #$contextObj.AccessToken = ConvertTo-SecureString -String $context.AccessToken -asplaintext -Force
        [ContextHelper]::currentContext = $contextObj

        try {
            $apiURL = "https://dev.azure.com/{0}/_apis/connectionData" -f [ContextHelper]::orgName
            #Note: cannot use this WRH method below due to ordering constraints during load in Framework.ps1
            #$header = [WebRequestHelper]::GetAuthHeaderFromUri($apiURL);
            $user = ""
            $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user, $contextObj.AccessToken)))
            $headers = @{
                            "Authorization"= ("Basic " + $base64AuthInfo); 
                            "Content-Type"="application/json"
                        };
            $responseObj = Invoke-RestMethod -Method Get -Uri $apiURL -Headers $headers -UseBasicParsing

            #If the token is valid, we get: "descriptor"="Microsoft.IdentityModel.Claims.ClaimsIdentity;72f988bf-86f1-41af-91ab-2d7cd011db47\xyz@microsoft.com"
            #Note that even for guest users, we get the host tenant (and not their native tenantId). E.g., "descriptor...;72f...47\pqr@live.com"
            #If the token is invalid, we get a diff object: "descriptor":"System:PublicAccess;aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
            $authNUserInfo = @(($responseObj.authenticatedUser.descriptor -split ';') -split '\\')
                    
            #Check if the above split resulted in 3 elements (valid token case)
            if ($authNUserInfo.Count -eq 3)
            {
                $contextObj.Tenant.Id = $authNUserInfo[1]
                $contextObj.Account.Id = $authNUserInfo[2]
            }
            elseif ([Helpers]::CheckMember($responseObj.authenticatedUser,"customDisplayName")) {
                $contextObj.Account.Id = $responseObj.authenticatedUser.customDisplayName;
            }
        }
        catch {
            Write-Host "Organization not found: Incorrect organization name or account does not have necessary permission to access the organization. Use -ResetCredentials parameter in command to login with another account." -ForegroundColor Yellow
            throw [SuppressedException] "The remote server returned an error: (404) Not Found.";
        }
    }

    static [string] GetCurrentSessionUser() {
        $context = [ContextHelper]::GetCurrentContext()
        if ($null -ne $context) {
            return $context.Account.Id
        }
        else {
            return "NO_ACTIVE_SESSION"
        }
    }    

}
# SIG # Begin signature block
# MIInogYJKoZIhvcNAQcCoIInkzCCJ48CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCydutU5ezsBoIe
# MeCmOhe37H9nc7HK1lL/wQONbGGH9KCCDYUwggYDMIID66ADAgECAhMzAAACzfNk
# v/jUTF1RAAAAAALNMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjIwNTEyMjA0NjAyWhcNMjMwNTExMjA0NjAyWjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDrIzsY62MmKrzergm7Ucnu+DuSHdgzRZVCIGi9CalFrhwtiK+3FIDzlOYbs/zz
# HwuLC3hir55wVgHoaC4liQwQ60wVyR17EZPa4BQ28C5ARlxqftdp3H8RrXWbVyvQ
# aUnBQVZM73XDyGV1oUPZGHGWtgdqtBUd60VjnFPICSf8pnFiit6hvSxH5IVWI0iO
# nfqdXYoPWUtVUMmVqW1yBX0NtbQlSHIU6hlPvo9/uqKvkjFUFA2LbC9AWQbJmH+1
# uM0l4nDSKfCqccvdI5l3zjEk9yUSUmh1IQhDFn+5SL2JmnCF0jZEZ4f5HE7ykDP+
# oiA3Q+fhKCseg+0aEHi+DRPZAgMBAAGjggGCMIIBfjAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU0WymH4CP7s1+yQktEwbcLQuR9Zww
# VAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh
# dGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMwMDEyKzQ3MDUzMDAfBgNVHSMEGDAW
# gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw
# MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx
# XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB
# AE7LSuuNObCBWYuttxJAgilXJ92GpyV/fTiyXHZ/9LbzXs/MfKnPwRydlmA2ak0r
# GWLDFh89zAWHFI8t9JLwpd/VRoVE3+WyzTIskdbBnHbf1yjo/+0tpHlnroFJdcDS
# MIsH+T7z3ClY+6WnjSTetpg1Y/pLOLXZpZjYeXQiFwo9G5lzUcSd8YVQNPQAGICl
# 2JRSaCNlzAdIFCF5PNKoXbJtEqDcPZ8oDrM9KdO7TqUE5VqeBe6DggY1sZYnQD+/
# LWlz5D0wCriNgGQ/TWWexMwwnEqlIwfkIcNFxo0QND/6Ya9DTAUykk2SKGSPt0kL
# tHxNEn2GJvcNtfohVY/b0tuyF05eXE3cdtYZbeGoU1xQixPZAlTdtLmeFNly82uB
# VbybAZ4Ut18F//UrugVQ9UUdK1uYmc+2SdRQQCccKwXGOuYgZ1ULW2u5PyfWxzo4
# BR++53OB/tZXQpz4OkgBZeqs9YaYLFfKRlQHVtmQghFHzB5v/WFonxDVlvPxy2go
# a0u9Z+ZlIpvooZRvm6OtXxdAjMBcWBAsnBRr/Oj5s356EDdf2l/sLwLFYE61t+ME
# iNYdy0pXL6gN3DxTVf2qjJxXFkFfjjTisndudHsguEMk8mEtnvwo9fOSKT6oRHhM
# 9sZ4HTg/TTMjUljmN3mBYWAWI5ExdC1inuog0xrKmOWVMIIHejCCBWKgAwIBAgIK
# YQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm
# aWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEw
# OTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYD
# VQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG
# 9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+la
# UKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc
# 6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4D
# dato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+
# lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nk
# kDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6
# A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmd
# X4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL
# 5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zd
# sGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3
# T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS
# 4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRI
# bmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAL
# BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBD
# uRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jv
# c29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEF
# BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1h
# cnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkA
# YwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn
# 8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7
# v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0b
# pdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/
# KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvy
# CInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBp
# mLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJi
# hsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYb
# BL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbS
# oqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sL
# gOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtX
# cVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCGXMwghlvAgEBMIGVMH4x
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01p
# Y3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAALN82S/+NRMXVEAAAAA
# As0wDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw
# HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIC7n
# 8IWUnnc/rT75kENYxxNr/0YnIMyS57msEe1bACwxMEIGCisGAQQBgjcCAQwxNDAy
# oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20wDQYJKoZIhvcNAQEBBQAEggEAZhd1qYL/JJIGU5wYhbFXjw0ui6HyAX5UdGQl
# 0rKdUS1MzsUIids67YWIDW7l3N07q6mcP2Nkyv0KMZ5dsmvgdPXc+5BQOdwsRm80
# 37wYvU554L79ue75y1RzMcrDyWMpPBnQgRgQOLqac9uHmo+KWp7RaXwbQG50YSoi
# EIvQYsMfBkct282r8/CBUmrQ5j+Tl7yelApwjNc409D5b3NkUKdw5i+HT7XlRfHx
# m6B5eOs86h11RVShY930DJXH3yrP5PUutkxpssIp2xhfA4dGn/PtRodqUTUMTeUa
# uzSohJuLcQocoTmo7U2K/0f0Zb+N+jHseqXB6ALLzu6rdjHumKGCFv0wghb5Bgor
# BgEEAYI3AwMBMYIW6TCCFuUGCSqGSIb3DQEHAqCCFtYwghbSAgEDMQ8wDQYJYIZI
# AWUDBAIBBQAwggFRBgsqhkiG9w0BCRABBKCCAUAEggE8MIIBOAIBAQYKKwYBBAGE
# WQoDATAxMA0GCWCGSAFlAwQCAQUABCClBXzg9lOhKwpcMoZsWwk2un5loPMXlUcR
# PWrqoMegQwIGZDfpaRl/GBMyMDIzMDQxNzEwMzAwNi40NTVaMASAAgH0oIHQpIHN
# MIHKMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQL
# ExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMSYwJAYDVQQLEx1UaGFsZXMg
# VFNTIEVTTjozQkJELUUzMzgtRTlBMTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgU2VydmljZaCCEVQwggcMMIIE9KADAgECAhMzAAABxjDNLtbTocD0AAEA
# AAHGMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw
# MB4XDTIyMTEwNDE5MDEzNFoXDTI0MDIwMjE5MDEzNFowgcoxCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVy
# aWNhIE9wZXJhdGlvbnMxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOjNCQkQtRTMz
# OC1FOUExMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIIC
# IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA770iOr6v4Hk1m3SZj+1BR/iu
# ofv+f6eVb7Hc21YxzAzro4G6kKXF47YAsEgrWWT1ogvp0IroFm8CdRZTf/DlQ0fb
# NNO9pCA01KJ03zH82Clmk9ze9r1jPJ1ZJaMnsZmAy7VpY9mNqX9dhPvnW1/Zxbbi
# Hv7qwwgw9U2ST5mfcpPutsI/Qr/gLC6aTI3UCYziVPZ/Qfag8NQhKkpHZO3Kr5r8
# 3cy7jz4OWPy5M2WitWv5bJJ5rBTW518QPEzFwzq8e8P722CWKZJFjN8etBgsK05g
# HeHaN9kmlpYJJL84v9JiaX7NFJkORhApEFZiUIaZoLxJt4pcBDzf+WD9UAjRKCrA
# seJ/ckzQvOn95X4Ot4asnOuNhcCdcQWcrZoykFmEaoYkrsD7n/4nFFHwJDKUaBYZ
# ZLwPj7ux48S1Ye+cMccMxdRSjuoG4rqJqpEd6gzfz239v36L+LtOlQhfL5cnclhN
# SWdmKw1THyekH96RNtOpIE7c+9Tnsv1aE9hphejLOJxfsXTkyw0FIdilc0CP7zzU
# sqaCGF2mFXRwL4jfX1RyV7QGKEeOpvGZqQKLHVZbLD32ztW8Lfv99xQ24d/TIxO9
# LReuHsnwotn+8BsCrzu+/24vOcA9Xcpd7kIeW4zmUxhFsv4KCfiqMptZhhMAA0Sq
# Duj23cj10smXROLUnhUCAwEAAaOCATYwggEyMB0GA1UdDgQWBBRxX/lHiShECp1n
# 2lMa6G1uLvNglDAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNV
# HR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2Ny
# bC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYI
# KwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAy
# MDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMA0G
# CSqGSIb3DQEBCwUAA4ICAQALzF724jXugDU486PMBYR7Cc6aHr2nJDnGsnkqwZYm
# jRi28qv6S1Ly772zwm5DI189zgAQ99xhEjW6uYkrX5UrtVu7JUQP6bBBBJ98c28F
# AIPIK3fkQNCv2rxRjfQMJSdcwsJeTK7Ld09OuA5hY4PWCBgJpfY71LcaXz3FR8AN
# PFX6zcKYbgYOZregtpDub34N0QhR7wc/FcmV+g4I3IdTAoMD2/WI5ZsfKTzBUn/U
# 3ApUhNwbOl5YSC+f9S1LStbZLwPzMS+fLPXJUSe7SSvspfSsr/VEe0oQhmaR+5vc
# q+7MLw861WBVhYpJ7TB5YBS5ORO9XdIbcpbBFwcHPmb8iZqSIqW9JpgG76+5NQUL
# PVzZ75z5W2R5ZiyQktiHpMwjX2OO29Z8+nTw2tOsVCcwzH9LoELedv3PjcpbwOyL
# jtm1T4XHYd3qbd9DXoBjNYkSjdi37pNp58u+rITltLKOjjQCJwj1FpnuBY825B5C
# 0uC/NYESEKsTicEjhS/4ujBXLcNGDhVBl2vHE6qY/YW4ky1vcypvUrsG81gpv2+8
# /ihOwg4wTLO7XqikeIiU3ZWAUAoOpTl14tedQqxbHTDveJYR3OU0yKB2xwf87EWC
# Ab0CJimhDmyQaKEvSV0fLW9iVyI0wYcG4V2aVN6TrZ4mr+ffaqDQD9F+HpPhP0pl
# AzCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZIhvcNAQEL
# BQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH
# EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNV
# BAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDEwMB4X
# DTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMCVVMxEzAR
# BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p
# Y3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3Rh
# bXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDk4aZM
# 57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25PhdgM/9cT8dm
# 95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPFdvWGUNzB
# RMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6GnszrYBb
# fowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBpDco2LXCO
# Mcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50ZuyjLVwIYw
# XE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3EXzTdEonW
# /aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0lBw0gg/w
# EPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1qGFphAXPK
# Z6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ+QuJYfM2
# BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PAPBXbGjfH
# CBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkwEgYJKwYB
# BAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxGNSnPEP8v
# BO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARVMFMwUQYM
# KwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0
# LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAKBggrBgEF
# BQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD
# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoYxDBW
# BgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny
# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYBBQUH
# AQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtp
# L2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG9w0BAQsF
# AAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0xM7U518Jx
# Nj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmCVgADsAW+
# iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449xvNo32X2
# pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wMnosZiefw
# C2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDSPeZKPmY7
# T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2dY3RILLFO
# Ry3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxnGSgkujhL
# mm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+CrvsQWY9af3L
# wUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokLjzbaukz5
# m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL6Xu/OHBE
# 0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggLLMIICNAIB
# ATCB+KGB0KSBzTCByjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEmMCQGA1UE
# CxMdVGhhbGVzIFRTUyBFU046M0JCRC1FMzM4LUU5QTExJTAjBgNVBAMTHE1pY3Jv
# c29mdCBUaW1lLVN0YW1wIFNlcnZpY2WiIwoBATAHBgUrDgMCGgMVAC01yuYmIVvs
# okSacJmWe8Mu2QFtoIGDMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh
# c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD
# b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw
# MTAwDQYJKoZIhvcNAQEFBQACBQDn5wTNMCIYDzIwMjMwNDE3MDczNTQxWhgPMjAy
# MzA0MTgwNzM1NDFaMHQwOgYKKwYBBAGEWQoEATEsMCowCgIFAOfnBM0CAQAwBwIB
# AAICB4swBwIBAAICEdIwCgIFAOfoVk0CAQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYK
# KwYBBAGEWQoDAqAKMAgCAQACAwehIKEKMAgCAQACAwGGoDANBgkqhkiG9w0BAQUF
# AAOBgQADKhMRGOcYnVjciI+hXyA8NoKu5w/hrXjtdET80gUYJB8q7ReDcKp5b/iE
# V8sK2dteyoGQE55AAsGXZFUtQwMwt9AMCCuhwlbqmfp80Xbwc+RutTXDl++laVdT
# 4GA13A5JExhwFrTilInnR40CMxtdGAYnuzTKeV1HWonkTFnigjGCBA0wggQJAgEB
# MIGTMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH
# EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNV
# BAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABxjDNLtbTocD0
# AAEAAAHGMA0GCWCGSAFlAwQCAQUAoIIBSjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcN
# AQkQAQQwLwYJKoZIhvcNAQkEMSIEIMj4KRhikugwEQUt928n/kp5t9L4dZt/Rpa1
# QrWet9v3MIH6BgsqhkiG9w0BCRACLzGB6jCB5zCB5DCBvQQgVjETXFXJvZgpxiZq
# k/BwDWA5Pxw6hehULpkFr9rjry0wgZgwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEG
# A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj
# cm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFt
# cCBQQ0EgMjAxMAITMwAAAcYwzS7W06HA9AABAAABxjAiBCDqJ7opBkv+nXYyFbtv
# Gw6nchJaaHgtUOvvW3EtgfcRZjANBgkqhkiG9w0BAQsFAASCAgBHM3QB79DnE0YF
# 8Bp5frRnGUckuYkLk+2DndHu9TdQeL2T8hYhbHlXGG8LskNbVPt54Oi6APByxYhX
# zwjyhcNBvWmTIHfFRrJq///mvMvOaMh8LJyeB5MV3OVCc9z+WNQ/9/8sLUcg5LbA
# rtxYJSnmXFdphwpKoC0OPJpcqCmrhQCT2G4Wq0WIVVU5of4oNtBjPlYQEyOdpXTy
# gAE5oFo+MXEJU8YV+3o50WBP8VQQtLQvF5/h9Ca4SksJTdDI0e77IXZB6kNjvGzF
# 8eR8D5pj2x/K//QpCYpeU6wuzdm3p0FaBPCxSh5chImKANde4O2490eDSf79AS+N
# aOcEtQuifWPtU/Ej5YeHB9xAeay0whW+MKFAIl/1fk7GgZa5ApEXjW7u/7bKs/ub
# NQ/wlcPVJ/2MBxWMYtamZjlKAH7JHOvnP+P9JvuMRh7lWk864ATq9qCOPUIA9K/3
# ykxT1CODNIa8qkUe+ZhR6Mm+kZcqF41jUGc0reQAyvSmCbaniZdGjkySLBKaCWhK
# Gb8HsAmKPHBhFz3nOqusgVRg8wABhK1NNZWKEjVlbHimxfbD3UlKuI1+ihtsBkah
# MSYsIz/zt8UWyach1nMtfaoM9xmGXHqmwoNXKl0FollftB4m6ebBRZagYQutNxjF
# pF6Cv7ZOduRJH+fbHnfOo5M1OEypmA==
# SIG # End signature block