Framework/Core/SVT/ADO/ADO.ServiceConnection.ps1

Set-StrictMode -Version Latest
class ServiceConnection: ADOSVTBase
{
    hidden [PSObject] $ServiceEndpointsObj = $null;
    hidden static [string] $SecurityNamespaceId = $null;
    hidden [PSObject] $ProjectId;
    hidden [PSObject] $ServiceConnEndPointDetail = $null;
    hidden [PSObject] $pipelinePermission = $null;
    hidden [PSObject] $serviceEndPointIdentity = $null;
    hidden [PSObject] $SvcConnActivityDetail = @{isSvcConnActive = $true; svcConnLastRunDate = $null; message = $null; isComputed = $false; errorObject = $null};
    hidden static $IsOAuthScan = $false;
    hidden [string] $checkInheritedPermissionsPerSvcConn = $false
    ServiceConnection([string] $organizationName, [SVTResource] $svtResource): Base($organizationName,$svtResource)
    {
        if(-not [string]::IsNullOrWhiteSpace($env:RefreshToken) -and -not [string]::IsNullOrWhiteSpace($env:ClientSecret))  # this if block will be executed for OAuth based scan
        {
            [ServiceConnection]::IsOAuthScan = $true
        }

        # Get project id
        $this.ProjectId = ($this.ResourceContext.ResourceId -split "project/")[-1].Split('/')[0]
        # Get security namespace identifier of service endpoints.
        if([string]::IsNullOrEmpty([ServiceConnection]::SecurityNamespaceId))
        {
            $apiURL = "https://dev.azure.com/{0}/_apis/securitynamespaces?api-version=6.0" -f $($this.OrganizationContext.OrganizationName)
            $securityNamespacesObj = [WebRequestHelper]::InvokeGetWebRequest($apiURL);
            [ServiceConnection]::SecurityNamespaceId = ($securityNamespacesObj | Where-Object { ($_.Name -eq "ServiceEndpoints")}).namespaceId

            $securityNamespacesObj = $null;
        }

        # Get service connection details https://dev.azure.com/{organization}/{project}/_admin/_services
        $this.ServiceEndpointsObj = $this.ResourceContext.ResourceDetails

        if(($this.ServiceEndpointsObj | Measure-Object).Count -eq 0)
        {
            throw [SuppressedException] "Unable to find active service connection(s) under [$($this.ResourceContext.ResourceGroupName)] project."
        }

        try {
            $apiURL = "https://dev.azure.com/{0}/_apis/Contribution/HierarchyQuery?api-version=5.0-preview.1" -f $($this.OrganizationContext.OrganizationName)
            $sourcePageUrl = "https://dev.azure.com/{0}/{1}/_settings/adminservices" -f $($this.OrganizationContext.OrganizationName), $this.ResourceContext.ResourceGroupName;
            $inputbody = "{'contributionIds':['ms.vss-serviceEndpoints-web.service-endpoints-details-data-provider'],'dataProviderContext':{'properties':{'serviceEndpointId':'$($this.ServiceEndpointsObj.id)','projectId':'$($this.projectId)','sourcePage':{'url':'$($sourcePageUrl)','routeId':'ms.vss-admin-web.project-admin-hub-route','routeValues':{'project':'$($this.ResourceContext.ResourceGroupName)','adminPivot':'adminservices','controller':'ContributedPage','action':'Execute'}}}}}" | ConvertFrom-Json

            $responseObj = [WebRequestHelper]::InvokePostWebRequest($apiURL,$inputbody);
            if([Helpers]::CheckMember($responseObj, "dataProviders") -and $responseObj.dataProviders."ms.vss-serviceEndpoints-web.service-endpoints-details-data-provider")
            {
                $this.ServiceConnEndPointDetail = $responseObj.dataProviders."ms.vss-serviceEndpoints-web.service-endpoints-details-data-provider"
            }
        }
        catch {

        }


        # if service connection activity check function is not computed, then first compute the function to get the correct status of service connection.
        if($this.SvcConnActivityDetail.isComputed -eq $false)
        {
            $this.CheckActiveConnection()
        }

        # overiding the '$this.isResourceActive' global variable based on the current status of service connection .
        if ($this.SvcConnActivityDetail.isSvcConnActive)
        {
            $this.isResourceActive = $true
        }
        else
        {
            $this.isResourceActive = $false
        }

        # calculating the inactivity period in days for the service connection. If there is no usage history, then setting it with negative value.
        # This will ensure inactive period is always computed irrespective of whether inactive control is scanned or not.
        if ($null -ne $this.SvcConnActivityDetail.svcConnLastRunDate)
        {
            $this.InactiveFromDays = ((Get-Date) - $this.SvcConnActivityDetail.svcConnLastRunDate).Days
        }

        if ([Helpers]::CheckMember($this.ControlSettings, "ServiceConnection.CheckForInheritedPermissions") -and $this.ControlSettings.ServiceConnection.CheckForInheritedPermissions) {
            $this.checkInheritedPermissionsPerSvcConn = $true
        }

    }

    [ControlItem[]] ApplyServiceFilters([ControlItem[]] $controls)
    {
        $result = $controls;
        # Applying filter to exclude certain controls based on Tag
        #For non azurerm svc conn - filter out all controls that are specific to azurerm
        if($this.ServiceEndpointsObj.type -ne "azurerm")
        {
            $result = $result | Where-Object { $_.Tags -notcontains "AzureRM" };
        }

        #For non azure svc conn - filter out all controls that are specific to azure
        if($this.ServiceEndpointsObj.type -ne "azure")
        {
            $result = $result | Where-Object { $_.Tags -notcontains "Azure" };
        }

        #if svc conn is either azure/azurerm - some controls that are specific and common to both azure/azurerm should be readded as they might have been filtered out in one of the previous two if conditions.
        if(($this.ServiceEndpointsObj.type -eq "azurerm") -or ($this.ServiceEndpointsObj.type -eq "azure"))
        {
            $result += $controls | Where-Object { ($_.Tags -contains "AzureRM") -and ($_.Tags -contains "Azure") };
        }

        return $result;
    }

    hidden [ControlResult] CheckServiceConnectionAccess([ControlResult] $controlResult)
    {
        $controlResult.VerificationResult = [VerificationResult]::Failed
        if ([ServiceConnection]::IsOAuthScan -eq $true)
        {
            if ($this.ServiceEndpointsObj.type -eq "azurerm")
            {
                try {
                    if([Helpers]::CheckMember($this.ServiceEndpointsObj, "data") )
                    {
                        $message = "Service connection has access at [{0}] {1} scope in the subscription [{2}] .";
                        $serviceEndPoint = $this.ServiceEndpointsObj
                        # 'scopeLevel' and 'creationMode' properties are required to determine whether a svc conn is automatic or manual.
                        # irrespective of creationMode - pass the control for conn authorized at MLWorkspace and PublishProfile (app service) scope as such conn are granted access at resource level.
                        if(([Helpers]::CheckMember($serviceEndPoint, "data.scopeLevel") -and ([Helpers]::CheckMember($serviceEndPoint.data, "creationMode")) ))
                        {
                            #If Service connection creation mode is 'automatic' and scopeLevel is subscription and no resource group is defined in its access definition -> conn has subscription level access -> fail the control,
                            #else pass the control if scopeLevel is 'Subscription' and 'scope' is RG (note scope property is visible, only if conn is authorized to an RG)
                            #Fail the control if it has access to management group (last condition)
                            if(($serviceEndPoint.data.scopeLevel -eq "Subscription" -and $serviceEndPoint.data.creationMode -eq "Automatic" -and !([Helpers]::CheckMember($serviceEndPoint.authorization,"parameters.scope") )) -or ($serviceEndPoint.data.scopeLevel -eq "ManagementGroup"))
                            {
                                $controlFailedMsg = '';
                                if ($serviceEndPoint.data.scopeLevel -eq "Subscription") {
                                    $controlFailedMsg = "Service connection has access at [$($serviceEndPoint.data.subscriptionName)] subscription scope."
                                }
                                elseif ($serviceEndPoint.data.scopeLevel -eq "ManagementGroup") {
                                    $controlFailedMsg = "Service connection has access at [$($serviceEndPoint.data.managementGroupName)] management group scope."
                                }
                                $controlResult.AddMessage([VerificationResult]::Failed, $controlFailedMsg);
                                $controlResult.AdditionalInfo += $controlFailedMsg;
                            }
                            else{ # else gets executed when svc is scoped at RG and not at sub or MG
                                if ([Helpers]::CheckMember($serviceEndPoint.authorization.parameters, "scope")) {
                                    $message =  $message -f $serviceEndPoint.authorization.parameters.scope.split('/')[-1], 'resource group', $serviceEndPoint.data.subscriptionName
                                }
                                else {
                                    $message = "Service connection is not configured at subscription scope."
                                }
                                $controlResult.AddMessage([VerificationResult]::Passed, $message);
                                $controlResult.AdditionalInfo += $message;
                            }
                        }
                        #elseif gets executed when scoped at AzureMLWorkspace
                        elseif(([Helpers]::CheckMember($serviceEndPoint, "data.scopeLevel") -and $serviceEndPoint.data.scopeLevel -eq "AzureMLWorkspace"))
                        {
                            $message =  $message -f $serviceEndPoint.data.mlWorkspaceName, 'ML workspace', $serviceEndPoint.data.subscriptionName
                            $controlResult.AddMessage([VerificationResult]::Passed, $message);
                            $controlResult.AdditionalInfo += $message;
                        }
                        #elseif gets executed when scoped at PublishProfile
                        elseif(([Helpers]::CheckMember($serviceEndPoint, "authorization.scheme") -and $serviceEndPoint.authorization.scheme -eq "PublishProfile"))
                        {
                            $message =  $message -f $serviceEndPoint.data.resourceId.split('/')[-1], 'app service', $serviceEndPoint.data.subscriptionName
                            $controlResult.AddMessage([VerificationResult]::Passed, $message);
                            $controlResult.AdditionalInfo += $message;
                        }
                        else  # if creation mode is manual and type is other (eg. managed identity) then verify the control
                        {
                            $controlResult.AddMessage([VerificationResult]::Verify, "Access scope of service connection can not be verified as it is not an 'automatic' service prinicipal.");
                        }
                    }
                    else
                    {
                        $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch the service connection details.");
                    }
                }
                catch {
                    $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch the service connection details.");
                    $controlResult.LogException($_)
                }
            }
            else {
                $controlResult.AddMessage([VerificationResult]::Manual,"Access scope of service connections of type other than 'Azure Resource Manager' can not be verified.");
            }
        }
        else {
            if ($this.ServiceEndpointsObj.type -eq "azurerm")
            {
                try {
                    if($this.ServiceConnEndPointDetail -and [Helpers]::CheckMember($this.ServiceConnEndPointDetail, "serviceEndpoint") )
                    {
                        $message = "Service connection has access at [{0}] {1} scope in the subscription [{2}] .";
                        $serviceEndPoint = $this.ServiceConnEndPointDetail.serviceEndpoint
                        # 'scopeLevel' and 'creationMode' properties are required to determine whether a svc conn is automatic or manual.
                        # irrespective of creationMode - pass the control for conn authorized at MLWorkspace and PublishProfile (app service) scope as such conn are granted access at resource level.
                        if(([Helpers]::CheckMember($serviceEndPoint, "data.scopeLevel") -and ([Helpers]::CheckMember($serviceEndPoint.data, "creationMode")) ))
                        {
                            #If Service connection creation mode is 'automatic' and scopeLevel is subscription and no resource group is defined in its access definition -> conn has subscription level access -> fail the control,
                            #else pass the control if scopeLevel is 'Subscription' and 'scope' is RG (note scope property is visible, only if conn is authorized to an RG)
                            #Fail the control if it has access to management group (last condition)
                            if(($serviceEndPoint.data.scopeLevel -eq "Subscription" -and $serviceEndPoint.data.creationMode -eq "Automatic" -and !([Helpers]::CheckMember($serviceEndPoint.authorization.parameters,"scope") )) -or ($serviceEndPoint.data.scopeLevel -eq "ManagementGroup"))
                            {
                                $controlFailedMsg = '';
                                if ($serviceEndPoint.data.scopeLevel -eq "Subscription") {
                                    $controlFailedMsg = "Service connection has access at [$($serviceEndPoint.data.subscriptionName)] subscription scope."
                                }
                                elseif ($serviceEndPoint.data.scopeLevel -eq "ManagementGroup") {
                                    $controlFailedMsg = "Service connection has access at [$($serviceEndPoint.data.managementGroupName)] management group scope."
                                }
                                $controlResult.AddMessage([VerificationResult]::Failed, $controlFailedMsg);
                                $controlResult.AdditionalInfo += $controlFailedMsg;
                            }
                            else{ # else gets executed when svc is scoped at RG and not at sub or MG
                                if ([Helpers]::CheckMember($serviceEndPoint.authorization.parameters, "scope")) {
                                    $message =  $message -f $serviceEndPoint.authorization.parameters.scope.split('/')[-1], 'resource group', $serviceEndPoint.data.subscriptionName
                                }
                                else {
                                    $message = "Service connection is not configured at subscription scope."
                                }
                                $controlResult.AddMessage([VerificationResult]::Passed, $message);
                                $controlResult.AdditionalInfo += $message;
                            }
                        }
                        #elseif gets executed when scoped at AzureMLWorkspace
                        elseif(([Helpers]::CheckMember($serviceEndPoint, "data.scopeLevel") -and $serviceEndPoint.data.scopeLevel -eq "AzureMLWorkspace"))
                        {
                            $message =  $message -f $serviceEndPoint.data.mlWorkspaceName, 'ML workspace', $serviceEndPoint.data.subscriptionName
                            $controlResult.AddMessage([VerificationResult]::Passed, $message);
                            $controlResult.AdditionalInfo += $message;
                        }
                        #elseif gets executed when scoped at PublishProfile
                        elseif(([Helpers]::CheckMember($serviceEndPoint, "authorization.scheme") -and $serviceEndPoint.authorization.scheme -eq "PublishProfile"))
                        {
                            $message =  $message -f $serviceEndPoint.data.resourceId.split('/')[-1], 'app service', $serviceEndPoint.data.subscriptionName
                            $controlResult.AddMessage([VerificationResult]::Passed, $message);
                            $controlResult.AdditionalInfo += $message;
                        }
                        else  # if creation mode is manual and type is other (eg. managed identity) then verify the control
                        {
                            $controlResult.AddMessage([VerificationResult]::Verify, "Access scope of service connection can not be verified as it is not an 'automatic' service prinicipal.");
                        }
                    }
                    else
                    {
                        $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch the service connection details.");
                    }
                }
                catch {
                    $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch the service connection details.");
                    $controlResult.LogException($_)
                }
            }
            else {
                $controlResult.AddMessage([VerificationResult]::Manual,"Access scope of service connections of type other than 'Azure Resource Manager' can not be verified.");
            }
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckClassicConnection([ControlResult] $controlResult)
    {
        $controlResult.VerificationResult = [VerificationResult]::Failed

        if([Helpers]::CheckMember($this.ServiceEndpointsObj,"type"))
        {
            if($this.ServiceEndpointsObj.type -eq "azure")
            {
                    $controlResult.AddMessage([VerificationResult]::Failed,
                                                "Classic service connection detected.");
            }
            else {
                $controlResult.AddMessage([VerificationResult]::Passed,
                                                "Classic service connection not detected.");
            }
        }
        else{
            $controlResult.AddMessage([VerificationResult]::Error,
                                                "Service connection type could not be detected.");
        }
        return $controlResult;
    }


    hidden [ControlResult] CheckSPNAuthenticationCertificate([ControlResult] $controlResult)
    {
        if([Helpers]::CheckMember($this.ServiceEndpointsObj, "authorization.parameters.authenticationType"))
        {
            if( $this.ServiceEndpointsObj.authorization.parameters.authenticationType -eq "spnKey")
            {
                $controlResult.AddMessage([VerificationResult]::Failed,
                                        "Service endpoint is authenticated using secret.");
            }
            else
            {
                $controlResult.AddMessage([VerificationResult]::Passed,
                                            "Service endpoint is authenticated using certificate.");
            }
        }
        return $controlResult;
    }

    hidden [ControlResult] CheckInheritedPermissions ([ControlResult] $controlResult)
    {
        $failMsg = $null
        try
        {
            $Endpoint = $this.ServiceEndpointsObj
            $apiURL = "https://dev.azure.com/{0}/_apis/accesscontrollists/{1}?token=endpoints/{2}/{3}&api-version=6.0" -f $($this.OrganizationContext.OrganizationName),$([ServiceConnection]::SecurityNamespaceId),$($this.ProjectId),$($Endpoint.id);
            $responseObj = [WebRequestHelper]::InvokeGetWebRequest($apiURL);
            if(($responseObj | Measure-Object).Count -eq 0)
            {
                $inheritPermissionsEnabled += @{EndPointName= $Endpoint.Name; Creator = $Endpoint.createdBy.displayName; inheritPermissions="Unable to fetch permissions inheritance details." }
            }
            elseif([Helpers]::CheckMember($responseObj,"inheritPermissions") -and $responseObj.inheritPermissions -eq $true)
            {
                $controlResult.AddMessage([VerificationResult]::Failed,"Inherited permissions are enabled on service connection.");
            }
            else {
                $controlResult.AddMessage([VerificationResult]::Passed,"Inherited permissions are disabled on service connection.");
            }

            $Endpoint = $null;
            $responseObj = $null;
        }
        catch {
            $failMsg = $_
            $controlResult.LogException($_)
        }

        if(![string]::IsNullOrEmpty($failMsg))
        {
            $controlResult.AddMessage([VerificationResult]::Manual,"Unable to fetch service connections details. $($failMsg)Please verify from portal that permission inheritance is turned OFF for all the service connections");
        }
        return $controlResult;
    }

    hidden [ControlResult] CheckGlobalGroupsAddedToServiceConnections ([ControlResult] $controlResult)
    {
        # Any identity other than teams identity needs to be verified manually as it's details cannot be retrived using API
        $controlResult.VerificationResult = [VerificationResult]::Failed        
        try
        {
            if ($null -eq $this.serviceEndPointIdentity) {
                $apiURL = "https://dev.azure.com/{0}/_apis/securityroles/scopes/distributedtask.serviceendpointrole/roleassignments/resources/{1}_{2}" -f $($this.OrganizationContext.OrganizationName), $($this.ProjectId),$($this.ServiceEndpointsObj.id);
                $this.serviceEndPointIdentity = @([WebRequestHelper]::InvokeGetWebRequest($apiURL));                
            }
            $restrictedGroups = @();
           
            $restrictedGlobalGroupsForSerConn = $this.ControlSettings.ServiceConnection.RestrictedGlobalGroupsForSerConn;
            if([Helpers]::CheckMember($this.serviceEndPointIdentity,"identity"))
            {
                # match all the identities added on service connection with defined restricted list
                $restrictedGroups = $this.serviceEndPointIdentity.identity | Where-Object { $restrictedGlobalGroupsForSerConn -contains $_.displayName.split('\')[-1] } | select displayName

                # fail the control if restricted group found on service connection
                if($restrictedGroups)
                {
                    $controlResult.AddMessage("Count of global groups that have access to service connection: ", @($restrictedGroups).Count)
                    $controlResult.AddMessage([VerificationResult]::Failed,"Do not grant global groups access to service connections. Granting elevated permissions to these groups can risk exposure of service connections to unwarranted individuals.");
                    $controlResult.AddMessage("Global groups that have access to service connection.",$restrictedGroups)
                    $controlResult.SetStateData("Global groups that have access to service connection",$restrictedGroups)
                    $controlResult.AdditionalInfo += "Count of global groups that have access to service connection: " + @($restrictedGroups).Count;
                }
                else{
                    $controlResult.AddMessage([VerificationResult]::Passed,"No global groups have access to service connection.");
                }
            }
            else {
                $controlResult.AddMessage([VerificationResult]::Passed,"No global groups have access to service connection.");
            }                   

            $restrictedGroups = $null;
            $restrictedGlobalGroupsForSerConn = $null;
        }
        catch {        
            $controlResult.AddMessage([VerificationResult]::Error,"Unable to fetch service connections details.")    
            $controlResult.LogException($_)
        }       
        return $controlResult;
    }

    hidden [ControlResult] CheckBuildServiceAccountAccess([ControlResult] $controlResult)
    {
        $controlResult.VerificationResult = [VerificationResult]::Failed
        $failMsg = $null

        try
        {
            #$isBuildSvcAccGrpFound = $false
            $buildServieAccountOnSvc = @();
            if ($null -eq $this.serviceEndPointIdentity) {
                $apiURL = "https://dev.azure.com/{0}/_apis/securityroles/scopes/distributedtask.serviceendpointrole/roleassignments/resources/{1}_{2}" -f $($this.OrganizationContext.OrganizationName), $($this.ProjectId),$($this.ServiceEndpointsObj.id);
                $this.serviceEndPointIdentity = @([WebRequestHelper]::InvokeGetWebRequest($apiURL));
            }
            if(($this.serviceEndPointIdentity.Count -gt 0) -and [Helpers]::CheckMember($this.serviceEndPointIdentity[0],"identity"))
            {
                foreach ($endPointidentity in $this.serviceEndPointIdentity)
                {
                    if ($endPointidentity.identity.displayName -like '*Project Collection Build Service Accounts' -or $endPointidentity.identity.displayName -like "*Build Service ($($this.OrganizationContext.OrganizationName))")
                    {
                        $buildServieAccountOnSvc += $endPointidentity;
                        #$isBuildSvcAccGrpFound = $true;
                        #break;
                    }
                }
                #Faile the control if prj coll Buil Ser Acc Group Found added on serv conn
                $restrictedBuildSVCAcctCount = $buildServieAccountOnSvc.Count;
                if($restrictedBuildSVCAcctCount -gt 0)
                {
                    $controlResult.AddMessage([VerificationResult]::Failed, "Count of restricted Build Service groups that have access to service connection: $($restrictedBuildSVCAcctCount)")
                    $formattedBSAData = $($buildServieAccountOnSvc.identity.displayName | FT | out-string )
                    #$formattedGroupsTable = ($formattedGroupsData | Out-String)
                    $controlResult.AddMessage("`nList of 'Build Service' Accounts: ", $formattedBSAData)
                    $controlResult.SetStateData("List of 'Build Service' Accounts: ", $formattedBSAData)
                    $controlResult.AdditionalInfo += "Count of restricted Build Service groups that have access to service connection: $($restrictedBuildSVCAcctCount)";
                    $formatedMembers = $buildServieAccountOnSvc | ForEach-Object { $_.identity.displayName + ': ' + $_.role.displayName }
                    $controlResult.AdditionalInfoInCSV = $(($formatedMembers) -join '; ')
                }
                else{
                    $controlResult.AddMessage([VerificationResult]::Passed,"Build Service accounts are not granted access to the service connection.");
                    $controlResult.AdditionalInfoInCSV = "NA";
                }

                $controlResult.AddMessage("`nNote:`nThe following 'Build Service' accounts should not have access to service connection: `nProject Collection Build Service Account`n$($this.ResourceContext.ResourceGroupName) Build Service ($($this.OrganizationContext.OrganizationName))");
            }
            else{
                $controlResult.AddMessage([VerificationResult]::Error,"Unable to fetch service endpoint group identity.");
            }
        }
        catch {
            $failMsg = $_
            $controlResult.LogException($_)
        }

        if(![string]::IsNullOrEmpty($failMsg))
        {
            $controlResult.AddMessage([VerificationResult]::Error,"Unable to fetch service connections details. $($failMsg)Please verify from portal that you are not granting global security groups access to service connections");
        }
        return $controlResult;
    }

    hidden [ControlResult] CheckServiceConnectionBuildAccess([ControlResult] $controlResult)
    {
        $controlResult.VerificationResult = [VerificationResult]::Failed
        try
        {
            if ($null -eq $this.pipelinePermission) {
                $apiURL = "https://dev.azure.com/{0}/{1}/_apis/pipelines/pipelinePermissions/endpoint/{2}?api-version=6.1-preview.1" -f $($this.OrganizationContext.OrganizationName),$($this.ProjectId),$($this.ServiceEndpointsObj.id) ;
                $this.pipelinePermission = [WebRequestHelper]::InvokeGetWebRequest($apiURL);
            }
            if([Helpers]::CheckMember($this.pipelinePermission,"allPipelines")) {
                if($this.pipelinePermission.allPipelines.authorized){
                    $controlResult.AddMessage([VerificationResult]::Failed,"Service connection is accessible to all pipelines.");
                }
                else {
                    $controlResult.AddMessage([VerificationResult]::Passed,"Service connection is not accessible to all pipelines.");
                }
            }
            else {
                $controlResult.AddMessage([VerificationResult]::Passed, "Service connection is not accessible to all pipelines.");
            }
            $controlResult.AdditionalInfoInCSV = "NA";
        }
        catch {
            $controlResult.AddMessage([VerificationResult]::Error,"Unable to fetch service connection details. $($_) Please verify from portal that you are not granting all pipeline access to service connections");
            $controlResult.LogException($_)
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckSecureAuthN([ControlResult] $controlResult)
    {
        if([Helpers]::CheckMember($this.ServiceEndpointsObj, "authorization.scheme"))
        {
            if($this.ServiceEndpointsObj.type -eq "github")
            {
                #Nov 2020 - Currently, authorizing using OAuth, permissions are fixed (high privileges by default) and can not be modified. If authorized using PAT, we can not determine whether it is a full scope or custom access scope token.
                if( $this.ServiceEndpointsObj.authorization.scheme -eq "OAuth")
                {
                    $controlResult.AddMessage([VerificationResult]::Verify, "Service connection [$($this.ServiceEndpointsObj.name)] is authenticated via $($this.ServiceEndpointsObj.authorization.scheme).");
                }
                else
                {
                    $controlResult.AddMessage([VerificationResult]::Verify, "Service connection [$($this.ServiceEndpointsObj.name)] is authenticated via $($this.ServiceEndpointsObj.authorization.scheme).");
                }
            }
            elseif($this.ServiceEndpointsObj.type -eq "azure")
            {
                if( $this.ServiceEndpointsObj.authorization.scheme -eq "Certificate")
                {
                    $controlResult.AddMessage([VerificationResult]::Passed, "Service connection [$($this.ServiceEndpointsObj.name)] is authenticated via $($this.ServiceEndpointsObj.authorization.scheme).");
                }
                else
                {
                    $controlResult.AddMessage([VerificationResult]::Failed, "Service connection [$($this.ServiceEndpointsObj.name)] is authenticated via $($this.ServiceEndpointsObj.authorization.scheme).");
                    $controlResult.AddMessage("Certificate based authentication should be used for Azure Classic service connection.")
                }
            }
            elseif($this.ServiceEndpointsObj.type -eq "azurerm")
            {
                $controlResult.AddMessage([VerificationResult]::Verify, "Service connection [$($this.ServiceEndpointsObj.name)] is authenticated via $($this.ServiceEndpointsObj.authorization.scheme).");
            }
            elseif($this.ServiceEndpointsObj.type -eq "externalnpmregistry")
            {
                if( $this.ServiceEndpointsObj.authorization.scheme -eq "Token")
                {
                    $controlResult.AddMessage([VerificationResult]::Passed, "Service connection [$($this.ServiceEndpointsObj.name)] is authenticated via $($this.ServiceEndpointsObj.authorization.scheme).");
                }
                else
                {
                    $controlResult.AddMessage([VerificationResult]::Failed, "Service connection [$($this.ServiceEndpointsObj.name)] is authenticated via $($this.ServiceEndpointsObj.authorization.scheme).");
                    $controlResult.AddMessage("Token based authentication should be used for NPM service connection.")
                }
            }
            elseif($this.ServiceEndpointsObj.type -eq "externalnugetfeed")
            {
                if( $this.ServiceEndpointsObj.authorization.scheme -eq "None") #APIKey
                {
                    $controlResult.AddMessage([VerificationResult]::Passed, "Service connection [$($this.ServiceEndpointsObj.name)] is authenticated via $($this.ServiceEndpointsObj.authorization.scheme).");
                }
                else
                {
                    $controlResult.AddMessage([VerificationResult]::Failed, "Service connection [$($this.ServiceEndpointsObj.name)] is authenticated via $($this.ServiceEndpointsObj.authorization.scheme).");
                    $controlResult.AddMessage("ApiKey based authentication should be used for NuGet service connection.")
                }
            }
            elseif($this.ServiceEndpointsObj.type -eq "externaltfs")
            {
                if( $this.ServiceEndpointsObj.authorization.scheme -eq "Token")
                {
                    $controlResult.AddMessage([VerificationResult]::Passed, "Service connection [$($this.ServiceEndpointsObj.name)] is authenticated via $($this.ServiceEndpointsObj.authorization.scheme).");
                }
                else
                {
                    $controlResult.AddMessage([VerificationResult]::Failed, "Service connection [$($this.ServiceEndpointsObj.name)] is authenticated via $($this.ServiceEndpointsObj.authorization.scheme).");
                    $controlResult.AddMessage("Token based authentication should be used for Azure Repos/Team Foundation Server service connection.")
                }
            }
            elseif($this.ServiceEndpointsObj.type -eq "MicrosoftSwagger")
            {
                if( $this.ServiceEndpointsObj.authorization.scheme -eq "Token")
                {
                    $controlResult.AddMessage([VerificationResult]::Passed, "Service connection [$($this.ServiceEndpointsObj.name)] is authenticated via $($this.ServiceEndpointsObj.authorization.scheme).");
                }
                else
                {
                    $controlResult.AddMessage([VerificationResult]::Failed, "Service connection [$($this.ServiceEndpointsObj.name)] is authenticated via $($this.ServiceEndpointsObj.authorization.scheme).");
                    $controlResult.AddMessage("Token based authentication should be used for Microsoft Swagger service connection.")
                }
            }
            else
            {
                $controlResult.AddMessage([VerificationResult]::NotScanned,"Control is not applicable to [$($this.ServiceEndpointsObj.name)] service connection.");
            }
        }
        return $controlResult;
    }

    hidden [ControlResult] CheckInactiveConnection([ControlResult] $controlResult)
    {
        $controlResult.VerificationResult = [VerificationResult]::Failed
        try
        {
            if ($this.SvcConnActivityDetail.message -eq 'Could not fetch the service connection details.') {
                $controlResult.AddMessage([VerificationResult]::Error, $this.SvcConnActivityDetail.message);
                if ($null -ne $this.SvcConnActivityDetail.errorObject)
                {
                    $controlResult.LogException($this.SvcConnActivityDetail.errorObject)
                }
            }
            elseif ($null -ne $this.SvcConnActivityDetail.svcConnLastRunDate)
            {
                if ($this.SvcConnActivityDetail.isSvcConnActive) {
                    $controlResult.AddMessage([VerificationResult]::Passed, $this.SvcConnActivityDetail.message);
                }
                else {
                    $controlResult.AddMessage([VerificationResult]::Failed, $this.SvcConnActivityDetail.message);
                }
                $formattedDate = $this.SvcConnActivityDetail.svcConnLastRunDate.ToString("d MMM yyyy")
                $controlResult.AddMessage("Last usage date of service connection: $($formattedDate )");
                $controlResult.AdditionalInfo += "Last usage date of service connection: " + $formattedDate ;
                $SvcConnInactivePeriod = ((Get-Date) - $this.SvcConnActivityDetail.svcConnLastRunDate).Days
                $controlResult.AdditionalInfoInCSV += "InactiveDays: $($SvcConnInactivePeriod)";                               
                $controlResult.AddMessage("The service connection was inactive from last $($SvcConnInactivePeriod) days.");
            }
            elseif ($this.SvcConnActivityDetail.isSvcConnActive)
            {
                $controlResult.AddMessage([VerificationResult]::Passed, $this.SvcConnActivityDetail.message);
                $controlResult.AdditionalInfoInCSV = "NA";
            }
            else
            {
                $controlResult.AddMessage([VerificationResult]::Failed, $this.SvcConnActivityDetail.message);
                $controlResult.AdditionalInfoInCSV += "Serivce connection last run date not found.";
            }
        }
        catch
        {
            $controlResult.AddMessage([VerificationResult]::Error,"Could not fetch the service connection details.");
            $controlResult.LogException($_)
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckCrossProjectSharing([ControlResult] $controlResult)
    {
        $controlResult.VerificationResult = [VerificationResult]::Failed
        if ([ServiceConnection]::IsOAuthScan -eq $true)
        {
            if($this.serviceendpointsobj -and [Helpers]::CheckMember($this.serviceendpointsobj, "serviceEndpointProjectReferences") )
            {
                #Get the project list which are accessible to the service connection.
                $svcProjectReferences = $this.serviceendpointsobj.serviceEndpointProjectReferences
                if (($svcProjectReferences | Measure-Object).Count -gt 1)
                {
                    $stateData = @();
                    $stateData += $svcProjectReferences | Select-Object name, projectReference

                    $controlResult.AddMessage("`nCount of projects that have access to the service connection: $($stateData.Count)") ;
                    $display = $stateData.projectReference | FT @{l='ProjectId';e={$_.id}},@{l='ProjectName';e={$_.name}}  -AutoSize | Out-String -Width 512
                    $controlResult.AddMessage([VerificationResult]::Failed, "Review the list of projects that have access to the service connection: ", $display);
                    $controlResult.SetStateData("List of projects that have access to the service connection: ", $stateData);
                    $controlResult.AdditionalInfo += "Count of projects that have access to the service connection: $($stateData.Count)";
                    $controlResult.AdditionalInfo += "List of projects that have access to the service connection: " + [JsonHelper]::ConvertToJsonCustomCompressed($stateData);
                }
                else
                {
                    $controlResult.AddMessage([VerificationResult]::Passed, "Service connection is not shared with multiple projects.");
                }
            }
            else
            {
                $controlResult.AddMessage([VerificationResult]::Error, "Service connection details could not be fetched.");
            }
        }
        else
        {
            if($this.ServiceConnEndPointDetail -and [Helpers]::CheckMember($this.ServiceConnEndPointDetail, "serviceEndpoint") )
            {
                #Get the project list which are accessible to the service connection.
                $svcProjectReferences = $this.ServiceConnEndPointDetail.serviceEndpoint.serviceEndpointProjectReferences
                if (($svcProjectReferences.Count -gt 1))
                {
                    $stateData = @();
                    $stateData += $svcProjectReferences | Select-Object name, projectReference

                    $controlResult.AddMessage("`nCount of projects that have access to the service connection: $($stateData.Count)") ;
                    $display = $stateData.projectReference | FT @{l='ProjectId';e={$_.id}},@{l='ProjectName';e={$_.name}}  -AutoSize | Out-String -Width 512
                    $controlResult.AddMessage([VerificationResult]::Failed, "Review the list of projects that have access to the service connection:`n ", $display);
                    $controlResult.SetStateData("List of projects that have access to the service connection: ", $stateData);
                    $controlResult.AdditionalInfo += "Count of projects that have access to the service connection: $($stateData.Count)";
                    $controlResult.AdditionalInfo += "List of projects that have access to the service connection: " + [JsonHelper]::ConvertToJsonCustomCompressed($stateData);
                }
                else
                {
                    $controlResult.AddMessage([VerificationResult]::Passed, "Service connection is not shared with multiple projects.");
                }
            }
            else
            {
                $controlResult.AddMessage([VerificationResult]::Error, "Service connection details could not be fetched.");
            }
        }
        return $controlResult;
    }

    hidden [ControlResult] CheckCrossPipelineSharing([ControlResult] $controlResult)
    {
        try
        {
            if ($null -eq $this.pipelinePermission) {
                #Get pipeline access on svc conn
                $apiURL = "https://dev.azure.com/{0}/{1}/_apis/pipelines/pipelinePermissions/endpoint/{2}?api-version=6.1-preview.1" -f $($this.OrganizationContext.OrganizationName), $($this.ProjectId), $($this.ServiceEndpointsObj.id) ;
                $this.pipelinePermission = [WebRequestHelper]::InvokeGetWebRequest($apiURL);
            }

            #check if svc conn is set to "Grant access permission to all pipelines"
            if ([Helpers]::CheckMember($this.pipelinePermission[0], "allPipelines.authorized") -and $this.pipelinePermission[0].allPipelines.authorized -eq $true)
            {
                $controlResult.AddMessage([VerificationResult]::Failed, "Service connection is accessible to all pipelines in the project.");
            }
            elseif ([Helpers]::CheckMember($this.pipelinePermission[0], "pipelines") -and ($this.pipelinePermission[0].pipelines | Measure-Object).Count -gt 1) #Atleast one pipeline has access to svvc conn
            {
                #get the pipelines ids in comma separated string to pass in api to get the pipeline name
                $pipelinesIds = $this.pipelinePermission[0].pipelines.id -join ","
                #api call to get the pipeline name
                $apiURL = "https://dev.azure.com/{0}/{1}/_apis/build/definitions?definitionIds={2}&api-version=6.0" -f $($this.OrganizationContext.OrganizationName), $($this.ProjectId), $pipelinesIds;
                $pipelineObj = [WebRequestHelper]::InvokeGetWebRequest($apiURL);

                # We are fixing the control status here and the state data info will be done as shown below. This is done in case we are not able to fetch the pipeline names. Although, we have the pipeline ids as shown above.
                $controlResult.AddMessage([VerificationResult]::Verify, "");
                $pipelines = @();

                if ($pipelineObj -and ($pipelineObj | Measure-Object).Count -gt 0)
                {
                    $pipelines += $pipelineObj.name
                    $controlResult.AddMessage("Total number of pipelines that have access to the service connection: ", ($pipelines | Measure-Object).Count);
                    $controlResult.AddMessage("Review the list of pipelines that have access to the service connection: ", $pipelines);
                    $controlResult.SetStateData("List of pipelines that have access to the service connection: ", $pipelines);
                    $controlResult.AdditionalInfo += "Total number of pipelines that have access to the service connection: " + ($pipelines | Measure-Object).Count;
                }
            }
            else
            {
                $controlResult.AddMessage([VerificationResult]::Passed, "Service connection is not shared with multiple pipelines.");
            }
        }
        catch
        {
            $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch pipeline permission details for the service connection.");
            $controlResult.LogException($_)
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckRBACAccess([ControlResult] $controlResult)
    {
        <#
        {
            "ControlID": "ADO_ServiceConnection_AuthZ_Grant_Min_RBAC_Access",
            "Description": "Justify all users/groups that have access to the service connection.",
            "Id": "ServiceConnection130",
            "ControlSeverity": "High",
            "Automated": "Yes",
            "MethodName": "CheckRBACAccess",
            "Rationale": "Granting minimum access by leveraging RBAC feature ensures that users/groups are granted just enough permissions on service connection to perform their tasks. This minimizes exposure of the resources in case of user/service account compromise.",
            "Recommendation": "Go to Project Settings --> Pipelines --> Service Connections --> Select Service Connection --> Select three dots on top right --> Select Security --> Under user permissions verify role assignments",
            "Tags": [
              "SDL",
              "TCP",
              "Manual",
              "AuthZ"
            ],
            "Enabled": true
          }
        #>


        try
        {
            if ($null -eq $this.serviceEndPointIdentity) {
                $apiURL = "https://dev.azure.com/{0}/_apis/securityroles/scopes/distributedtask.serviceendpointrole/roleassignments/resources/{1}_{2}" -f $($this.OrganizationContext.OrganizationName), $($this.ProjectId),$($this.ServiceEndpointsObj.id);
                $this.serviceEndPointIdentity = [WebRequestHelper]::InvokeGetWebRequest($apiURL);
            }
            if((($this.serviceEndPointIdentity | Measure-Object).Count -gt 0) -and [Helpers]::CheckMember($this.serviceEndPointIdentity[0],"identity"))
            {
                $roles = @();
                $roles +=   ($this.serviceEndPointIdentity | Select-Object -Property @{Name="Name"; Expression = {$_.identity.displayName}},@{Name="Role"; Expression = {$_.role.displayName}});
                $rolesCount = ($roles | Measure-Object).Count;
                $controlResult.AddMessage("Total number of identities that have access to service connection: $($rolesCount)");
                $controlResult.AddMessage([VerificationResult]::Verify,"Verify whether following identities have been provided with minimum RBAC access to service connection: ", $roles);
                $controlResult.SetStateData("List of identities having access to service connection: ", $roles);
                $controlResult.AdditionalInfo += "Total number of identities that have access to service connection: " + $rolesCount;
            }
            elseif(($this.ServiceEndpointsObj | Measure-Object).Count -eq 0)
            {
                $controlResult.AddMessage([VerificationResult]::Passed,"No role assignments found on service connection.")
            }
        }
        catch
        {
            $controlResult.AddMessage([VerificationResult]::Error,"Unable to fetch role assignments.")
            $controlResult.LogException($_)
        }

        return $controlResult
    }

    hidden CheckActiveConnection()
    {
        try
        {
            if ([ServiceConnection]::IsOAuthScan -eq $true)
            {
                $apiURL = "https://dev.azure.com/{0}/{1}/_apis/serviceendpoint/{2}/executionhistory?top=1&api-version=6.0-preview.1" -f $($this.OrganizationContext.OrganizationName), $($this.ResourceContext.ResourceGroupName), $($this.serviceendpointsobj.id);
                $serviceEndpointExecutionHistory = [WebRequestHelper]::InvokeGetWebRequest($apiURL);

                if (($serviceEndpointExecutionHistory | Measure-Object).Count -gt 0 -and ([Helpers]::CheckMember($serviceEndpointExecutionHistory[0],"data")))
                {
                    #if this job is still running then finishTime is not available. pass the control
                    if ([Helpers]::CheckMember($serviceEndpointExecutionHistory[0].data, "finishTime"))
                    {
                        #Get the last known usage (job) timestamp of the service connection
                        $svcLastRunDate = $serviceEndpointExecutionHistory[0].data.finishTime;

                        #format date
                        $formatLastRunTimeSpan = New-TimeSpan -Start (Get-Date $svcLastRunDate)

                        # $inactiveLimit denotes the upper limit on number of days of inactivity before the svc conn is deemed inactive.
                        if ($this.ControlSettings -and [Helpers]::CheckMember($this.ControlSettings, "ServiceConnection.ServiceConnectionHistoryPeriodInDays") )
                        {
                            $inactiveLimit = $this.ControlSettings.ServiceConnection.ServiceConnectionHistoryPeriodInDays
                            if ($formatLastRunTimeSpan.Days -gt $inactiveLimit)
                            {
                                $this.SvcConnActivityDetail.isSvcConnActive = $false;
                                $this.SvcConnActivityDetail.message = "Service connection has not been used in the last $inactiveLimit days.";
                            }
                            else
                            {
                                $this.SvcConnActivityDetail.isSvcConnActive = $true;
                                $this.SvcConnActivityDetail.message =  "Service connection has been used in the last $inactiveLimit days.";
                            }
                        }
                        else {
                            $this.SvcConnActivityDetail.isSvcConnActive = $false;
                            $this.SvcConnActivityDetail.message = "Could not fetch the inactive days limit for service connection.";
                        }
                        $this.SvcConnActivityDetail.svcConnLastRunDate = [datetime]::Parse($svcLastRunDate);
                    }
                    else
                    {
                        $this.SvcConnActivityDetail.isSvcConnActive = $true;
                        $this.SvcConnActivityDetail.message = "Service connection was under use during the control scan.";
                    }
                }
                else #service connection was created but never used. (Fail for now)
                {
                    $this.SvcConnActivityDetail.isSvcConnActive = $false;
                    $this.SvcConnActivityDetail.message = "Service connection has never been used.";
                }
            }
            else {
                if ($this.ServiceConnEndPointDetail -and [Helpers]::CheckMember($this.ServiceConnEndPointDetail, "serviceEndpointExecutionHistory") )
                {
                    #if this job is still running then finishTime is not available. pass the control
                    if ([Helpers]::CheckMember($this.ServiceConnEndPointDetail.serviceEndpointExecutionHistory[0].data, "finishTime"))
                    {
                        #Get the last known usage (job) timestamp of the service connection
                        $svcLastRunDate = $this.ServiceConnEndPointDetail.serviceEndpointExecutionHistory[0].data.finishTime;

                        #format date
                        $formatLastRunTimeSpan = New-TimeSpan -Start (Get-Date $svcLastRunDate)

                        # $inactiveLimit denotes the upper limit on number of days of inactivity before the svc conn is deemed inactive.
                        if ($this.ControlSettings -and [Helpers]::CheckMember($this.ControlSettings, "ServiceConnection.ServiceConnectionHistoryPeriodInDays") )
                        {
                            $inactiveLimit = $this.ControlSettings.ServiceConnection.ServiceConnectionHistoryPeriodInDays
                            if ($formatLastRunTimeSpan.Days -gt $inactiveLimit)
                            {
                                $this.SvcConnActivityDetail.isSvcConnActive = $false;
                                $this.SvcConnActivityDetail.message = "Service connection has not been used in the last $inactiveLimit days.";
                            }
                            else
                            {
                                $this.SvcConnActivityDetail.isSvcConnActive = $true;
                                $this.SvcConnActivityDetail.message =  "Service connection has been used in the last $inactiveLimit days.";
                            }
                        }
                        else {
                            $this.SvcConnActivityDetail.isSvcConnActive = $false;
                            $this.SvcConnActivityDetail.message = "Could not fetch the inactive days limit for service connection.";
                        }
                        $this.SvcConnActivityDetail.svcConnLastRunDate = [datetime]::Parse($svcLastRunDate);
                    }
                    else
                    {
                        $this.SvcConnActivityDetail.isSvcConnActive = $true;
                        $this.SvcConnActivityDetail.message = "Service connection was under use during the control scan.";
                    }
                }
                else #service connection was created but never used. (Fail for now)
                {
                    $this.SvcConnActivityDetail.isSvcConnActive = $false;
                    $this.SvcConnActivityDetail.message = "Service connection has never been used.";
                }
            }
        }
        catch
        {
            $this.SvcConnActivityDetail.message = "Could not fetch the service connection details.";
            $this.SvcConnActivityDetail.errorObject = $_
        }
        $this.SvcConnActivityDetail.isComputed = $true
    }


    hidden [ControlResult] CheckBroaderGroupAccess ([ControlResult] $controlResult) {
        $controlResult.VerificationResult = [VerificationResult]::Failed

        try {
            if ($null -eq $this.serviceEndPointIdentity) {
                $apiURL = "https://dev.azure.com/{0}/_apis/securityroles/scopes/distributedtask.serviceendpointrole/roleassignments/resources/{1}_{2}" -f $($this.OrganizationContext.OrganizationName), $($this.ProjectId), $($this.ServiceEndpointsObj.id);
                $this.serviceEndPointIdentity = @([WebRequestHelper]::InvokeGetWebRequest($apiURL));
            }
            $restrictedGroups = @();
            $restrictedBroaderGroups = @{}
            $restrictedBroaderGroupsForSvcConn = $this.ControlSettings.ServiceConnection.RestrictedBroaderGroupsForSvcConn;
            #Converting controlsettings broader groups into a hashtable.
            $restrictedBroaderGroupsForSvcConn.psobject.properties | foreach { $restrictedBroaderGroups[$_.Name] = $_.Value }

            if (($this.serviceEndPointIdentity.Count -gt 0) -and [Helpers]::CheckMember($this.serviceEndPointIdentity, "identity")) {
                # match all the identities added on service connection with defined restricted list
                $roleAssignments = @();
                $roleAssignmentsToCheck = $this.serviceEndPointIdentity
                if ($this.checkInheritedPermissionsPerSvcConn -eq $false) {
                    $roleAssignmentsToCheck = $this.serviceEndPointIdentity | where-object { $_.access -ne "inherited" }
                }
                $roleAssignments = @($roleAssignmentsToCheck | Select-Object -Property @{Name="Name"; Expression = {$_.identity.displayName}},@{Name="Id"; Expression = {$_.identity.id}},@{Name="AccessDisplayName"; Expression = {$_.accessDisplayName}},@{Name="Role"; Expression = {$_.role.displayName}});
                #Checking where broader groups have excessive permission on service connection
                $restrictedGroups += @($roleAssignments | Where-Object { $restrictedBroaderGroups.keys -contains $_.Name.split('\')[-1] -and ($_.Role -in $restrictedBroaderGroups[$_.Name.split('\')[-1]])})
                
                if ($this.ControlSettings.CheckForBroadGroupMemberCount -and $restrictedGroups.Count -gt 0)
                {
                    $broaderGroupsWithExcessiveMembers = @([ControlHelper]::FilterBroadGroupMembers($restrictedGroups, $true))
                    $restrictedGroups = @($restrictedGroups | Where-Object {$broaderGroupsWithExcessiveMembers -contains $_.Name})
                }

                $restrictedGroupsCount = $restrictedGroups.Count

                # fail the control if restricted group found on service connection
                if ($restrictedGroupsCount -gt 0) {
                    $controlResult.AddMessage([VerificationResult]::Failed, "Count of broader groups that have excessive permissions on service connection: $($restrictedGroupsCount)")
                    $backupDataObject = $restrictedGroups | Select @{l = 'Group'; e = { $_.Name} },@{l = 'Id'; e = { $_.Id} }, @{l = 'Role'; e = { $_.Role } },@{l = 'AccessDisplayName'; e = { $_.AccessDisplayName } }
                    $formattedGroupsData = $restrictedGroups | Select @{l = 'Group'; e = { $_.Name} }, @{l = 'Role'; e = { $_.Role } },@{l = 'AccessDisplayName'; e = { $_.AccessDisplayName } }
                    $formattedGroupsTable = ($formattedGroupsData | FT -AutoSize | Out-String -width 512)
                    $controlResult.AddMessage("`nList of groups: ", $formattedGroupsTable)
                    $controlResult.SetStateData("List of groups: ", $formattedGroupsTable)
                    $controlResult.AdditionalInfo += "Count of broader groups that have excessive permissions on service connection: $($restrictedGroupsCount)";
                    if ($this.ControlFixBackupRequired) {
                        #Data object that will be required to fix the control
                        $controlResult.BackupControlState = $backupDataObject;
                    }
                    $restrictedGroupsAccess = $restrictedGroups | ForEach-Object { $_.Name + ': ' + $_.Role }
                    $controlResult.AdditionalInfoInCSV = $restrictedGroupsAccess -join '; '
                }
                else {
                    $controlResult.AddMessage([VerificationResult]::Passed, "No broader groups have excessive permissions on service connection.");
                    $controlResult.AdditionalInfoInCSV = "NA";
                }
            }
            else {
                $controlResult.AddMessage([VerificationResult]::Passed, "No broader groups have excessive permissions on service connection.");
                $controlResult.AdditionalInfoInCSV = "NA";
            }
            $displayObj = $restrictedBroaderGroups.Keys | Select-Object @{Name = "Broader Group"; Expression = {$_}}, @{Name = "Excessive Permissions"; Expression = {$restrictedBroaderGroups[$_] -join ', '}}
            $controlResult.AddMessage("`nNote:`nThe following groups are considered 'broad' which should not have excessive permissions: `n$($displayObj | FT | out-string -width 512)`n");
        }
        catch
        {
            $controlResult.AddMessage([VerificationResult]::Error, "Unable to fetch service connections details. Please verify from portal that you are not granting global security groups access to service connections");
            $controlResult.LogException($_)
        }
        return $controlResult;
    }

    hidden [ControlResult] CheckBroaderGroupAccessAutomatedFix ([ControlResult] $controlResult) 
    {        
        try {
            $RawDataObjForControlFix = @();
            $RawDataObjForControlFix = ([ControlHelper]::ControlFixBackup | where-object {$_.ResourceId -eq $this.ResourceId}).DataObject

            $body = "["

            if (-not $this.UndoFix)
            {
                foreach ($identity in $RawDataObjForControlFix) 
                {                    
                    if ($body.length -gt 1) {$body += ","}
                    $body += @"
                        {
                            "userId": "$($identity.id)",
                            "roleName": "Reader",
                            "uniqueName": "$($identity.accessDisplayName)"
                        }
"@
;
                }
                $RawDataObjForControlFix | Add-Member -NotePropertyName NewRole -NotePropertyValue "Reader"
                $RawDataObjForControlFix = @($RawDataObjForControlFix  | Select-Object @{Name="DisplayName"; Expression={$_.group}}, @{Name="OldRole"; Expression={$_.Role}},@{Name="NewRole"; Expression={$_.NewRole}})
            }
            else {
                foreach ($identity in $RawDataObjForControlFix) 
                {                    
                    if ($body.length -gt 1) {$body += ","}
                    $body += @"
                        {
                            "userId": "$($identity.id)",
                            "roleName": "$($identity.role)",
                            "uniqueName": "$($identity.accessDisplayName)"
                        }
"@
;
                }
                $RawDataObjForControlFix | Add-Member -NotePropertyName OldRole -NotePropertyValue "Reader"
                $RawDataObjForControlFix = @($RawDataObjForControlFix  | Select-Object @{Name="DisplayName"; Expression={$_.group}}, @{Name="OldRole"; Expression={$_.OldRole}},@{Name="NewRole"; Expression={$_.Role}})
            }
            $body += "]"

            #Put request
            $url = "https://dev.azure.com/$($this.OrganizationContext.OrganizationName)/_apis/securityroles/scopes/distributedtask.serviceendpointrole/roleassignments/resources/$($this.ProjectId)_$($this.ServiceEndpointsObj.id)?api-version=5.0-preview.1";  
            $rmContext = [ContextHelper]::GetCurrentContext();
            $user = "";
            $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user,$rmContext.AccessToken)))
            $webRequestResult = Invoke-RestMethod -Uri $url -Method Put -ContentType "application/json" -Headers @{Authorization = ("Basic {0}" -f $base64AuthInfo) } -Body $body                
            $controlResult.AddMessage([VerificationResult]::Fixed,  "Permission for broader groups have been changed as below: ");
            $display = ($RawDataObjForControlFix |  FT -AutoSize | Out-String -Width 512)

            $controlResult.AddMessage("`n$display");
        }
        catch{
            $controlResult.AddMessage([VerificationResult]::Error,  "Could not apply fix.");
            $controlResult.LogException($_)
        }
        return $controlResult        
    }

    hidden [ControlResult] CheckRestricedCloudEnvironment ([ControlResult] $controlResult) {
        $disallowedEnvironments = @()
        if ($this.ControlSettings -and [Helpers]::CheckMember($this.ControlSettings, "Organization.DisallowedEnvironments") ) {
            $disallowedEnvironments =  $this.ControlSettings.Organization.DisallowedEnvironments
        }
        if($disallowedEnvironments.Length -ne 0) {
            $controlResult.AddMessage( "List of disallowed cloud environments.", $disallowedEnvironments);
            if ((-not [Helpers]::CheckMember($this.ServiceEndpointsObj, "data")) -or [string]::IsNullOrEmpty($this.ServiceEndpointsObj.data) -or (-not[Helpers]::CheckMember($this.ServiceEndpointsObj.data, "environment"))) {
                $controlResult.AddMessage([VerificationResult]::Passed, "Unable to determine the cloud environment for the service connection.");
            }
            else {
            $serviceConnectionEnvironment = $this.ServiceEndpointsObj.data.environment
            #check if the current environment is in list of restricted environments
            if ($disallowedEnvironments -contains $serviceConnectionEnvironment) {
                    $controlResult.AddMessage([VerificationResult]::Failed, "Service connection is connected to restricted cloud environment: $serviceConnectionEnvironment");
                }
                else {
                    $controlResult.AddMessage([VerificationResult]::Passed, "Service connection is not connected to restricted cloud environments.");
                }
            }
        }
        else {
            $controlResult.AddMessage([VerificationResult]::Passed, "No restricted cloud environments were configured in control settings.");
        }
        return $controlResult;
    }
}
# SIG # Begin signature block
# MIIjnwYJKoZIhvcNAQcCoIIjkDCCI4wCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAJOQC5wCbMFq7V
# yd8zUYTyEXIdzLroeWD+4eNaob8lVKCCDYEwggX/MIID56ADAgECAhMzAAAB32vw
# LpKnSrTQAAAAAAHfMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjAxMjE1MjEzMTQ1WhcNMjExMjAyMjEzMTQ1WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQC2uxlZEACjqfHkuFyoCwfL25ofI9DZWKt4wEj3JBQ48GPt1UsDv834CcoUUPMn
# s/6CtPoaQ4Thy/kbOOg/zJAnrJeiMQqRe2Lsdb/NSI2gXXX9lad1/yPUDOXo4GNw
# PjXq1JZi+HZV91bUr6ZjzePj1g+bepsqd/HC1XScj0fT3aAxLRykJSzExEBmU9eS
# yuOwUuq+CriudQtWGMdJU650v/KmzfM46Y6lo/MCnnpvz3zEL7PMdUdwqj/nYhGG
# 3UVILxX7tAdMbz7LN+6WOIpT1A41rwaoOVnv+8Ua94HwhjZmu1S73yeV7RZZNxoh
# EegJi9YYssXa7UZUUkCCA+KnAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUOPbML8IdkNGtCfMmVPtvI6VZ8+Mw
# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1
# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDYzMDA5MB8GA1UdIwQYMBaAFEhu
# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu
# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w
# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx
# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAnnqH
# tDyYUFaVAkvAK0eqq6nhoL95SZQu3RnpZ7tdQ89QR3++7A+4hrr7V4xxmkB5BObS
# 0YK+MALE02atjwWgPdpYQ68WdLGroJZHkbZdgERG+7tETFl3aKF4KpoSaGOskZXp
# TPnCaMo2PXoAMVMGpsQEQswimZq3IQ3nRQfBlJ0PoMMcN/+Pks8ZTL1BoPYsJpok
# t6cql59q6CypZYIwgyJ892HpttybHKg1ZtQLUlSXccRMlugPgEcNZJagPEgPYni4
# b11snjRAgf0dyQ0zI9aLXqTxWUU5pCIFiPT0b2wsxzRqCtyGqpkGM8P9GazO8eao
# mVItCYBcJSByBx/pS0cSYwBBHAZxJODUqxSXoSGDvmTfqUJXntnWkL4okok1FiCD
# Z4jpyXOQunb6egIXvkgQ7jb2uO26Ow0m8RwleDvhOMrnHsupiOPbozKroSa6paFt
# VSh89abUSooR8QdZciemmoFhcWkEwFg4spzvYNP4nIs193261WyTaRMZoceGun7G
# CT2Rl653uUj+F+g94c63AhzSq4khdL4HlFIP2ePv29smfUnHtGq6yYFDLnT0q/Y+
# Di3jwloF8EWkkHRtSuXlFUbTmwr/lDDgbpZiKhLS7CBTDj32I0L5i532+uHczw82
# oZDmYmYmIUSMbZOgS65h797rj5JJ6OkeEUJoAVwwggd6MIIFYqADAgECAgphDpDS
# AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0
# ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla
# MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT
# H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG
# OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S
# 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz
# y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7
# 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u
# M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33
# X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl
# XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP
# 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB
# l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF
# RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM
# CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ
# BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud
# DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO
# 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0
# LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p
# Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB
# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw
# cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA
# XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY
# 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj
# 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd
# d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ
# Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf
# wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ
# aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j
# NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B
# xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96
# eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7
# r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I
# RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIVdDCCFXACAQEwgZUwfjELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z
# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAd9r8C6Sp0q00AAAAAAB3zAN
# BglghkgBZQMEAgEFAKCBsDAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor
# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgpptCMEWi
# 1eQ597IZkUJUBhyKqZz1cn9uhy5y2S8v5LAwRAYKKwYBBAGCNwIBDDE2MDSgFIAS
# AE0AaQBjAHIAbwBzAG8AZgB0oRyAGmh0dHBzOi8vd3d3Lm1pY3Jvc29mdC5jb20g
# MA0GCSqGSIb3DQEBAQUABIIBAEbcLbJIr8cW0KVCuSf350Bmy5yqywpW97rKmh16
# yRbIzyqsh3U8jwneqm8kSV2Bmnn5ILATGN3nL7e6bXAhujNb9Pt8wFc5PoE0kqIe
# +aWidqydoYvqsA+eIStgrlVIu2FJa6wPC68XzGbzbpSW/O4sbIiV9ZL1Mm7Fp+o5
# k7D8stpiUBaxUQj99GTqR6ApzRge00ioOrLT8OUuM8ax88rGXbWyYhlVpBOr0VNV
# BncSrFjAlXyOWiA5FxbGrFczogV4aV3kV/Cf0GZn4ZoVgZUPaKfkaO+10gVcZ/YQ
# MM1p1UNrtcp/B3YdVKM7CeguXZ488kqtMwVV7Gibfl4LhHahghL8MIIS+AYKKwYB
# BAGCNwMDATGCEugwghLkBgkqhkiG9w0BBwKgghLVMIIS0QIBAzEPMA0GCWCGSAFl
# AwQCAQUAMIIBVwYLKoZIhvcNAQkQAQSgggFGBIIBQjCCAT4CAQEGCisGAQQBhFkK
# AwEwMTANBglghkgBZQMEAgEFAAQg6lCUBu6p17M16xpXbs22ilyxTY0z/KX6w/0D
# aRBdNU0CBmExH3P4pxgRMjAyMTA5MTUxMTA5NDEuNVowBIACAfSggdikgdUwgdIx
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1p
# Y3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEmMCQGA1UECxMdVGhh
# bGVzIFRTUyBFU046RkM0MS00QkQ0LUQyMjAxJTAjBgNVBAMTHE1pY3Jvc29mdCBU
# aW1lLVN0YW1wIFNlcnZpY2Wggg5NMIIE+TCCA+GgAwIBAgITMwAAAUAjGdZe3pUk
# MQAAAAABQDANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0Eg
# MjAxMDAeFw0yMDEwMTUxNzI4MjZaFw0yMjAxMTIxNzI4MjZaMIHSMQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQg
# SXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1Mg
# RVNOOkZDNDEtNEJENC1EMjIwMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFt
# cCBTZXJ2aWNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArn1rM3Hq
# 1S9N0z8R+YKqZu25ykk5OlT8TsuwtdBWyDCRFoASk9fB6siColFhXBhyej4c3yIw
# N0UyJWBOTAjHteOIYjfCpx539rbgBI5/BTHtC+qcBT7ftPknTtQn89lNOcpP70fu
# YVZLoQsDnLjGxxtW/eVewR5Q0I1mWQfJy5xOfelk5OWjz3YV4HKtqyIRzJZd/Rzc
# Y8w6qmzoSNsYIdvliT2eeQZbyYTdJQsRozIKTMLCJUBfVjow2fJMDtzDB9XEOdfh
# PWzvUOadYgqqh0lslAR7NV90FFmZgZWARrG8j7ZnVnC5MOXOS/NI58S48ycsug0p
# N2NGLLk2YWjxCwIDAQABo4IBGzCCARcwHQYDVR0OBBYEFDVDHC4md0YgjozSqnVs
# +OeELQ5nMB8GA1UdIwQYMBaAFNVjOlyKMZDzQ3t8RhvFM2hahW1VMFYGA1UdHwRP
# ME0wS6BJoEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1
# Y3RzL01pY1RpbVN0YVBDQV8yMDEwLTA3LTAxLmNybDBaBggrBgEFBQcBAQROMEww
# SgYIKwYBBQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMv
# TWljVGltU3RhUENBXzIwMTAtMDctMDEuY3J0MAwGA1UdEwEB/wQCMAAwEwYDVR0l
# BAwwCgYIKwYBBQUHAwgwDQYJKoZIhvcNAQELBQADggEBAGMMUq2gQuCC9wr4YhIS
# fPyobaNYV3Ov4YwWsSfIz/j1xaN9TvLAB2BxPi2CtRbgbBUf48n07yReZInwu2r8
# vwLoNG2TtYzey01DRyjjsNoiHF9UuRLFyKZChkKC3o9r0Oy2x0YYjUpDxVChZ5q5
# cAfw884wP0iUcYnKKGn8eJ0nwpr7zr/Tlu+HOjXDT9C754aS4KUFNm8D7iwuvWWz
# SOVl7XMWdu82BnnTmB7s2Ocf3I4adGzdixQ5Zxxa3zOAvKzrV+0HcVQIY3SQJ9Pz
# jDRlzCviMThxA8FUIRL3FnYqvchWkEoZ4w8S7FsGWNlXLWQ7fHMb3l4gjueHyO4p
# 6tUwggZxMIIEWaADAgECAgphCYEqAAAAAAACMA0GCSqGSIb3DQEBCwUAMIGIMQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNy
# b3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0xMDA3MDEy
# MTM2NTVaFw0yNTA3MDEyMTQ2NTVaMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX
# YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAy
# MDEwMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqR0NvHcRijog7PwT
# l/X6f2mUa3RUENWlCgCChfvtfGhLLF/Fw+Vhwna3PmYrW/AVUycEMR9BGxqVHc4J
# E458YTBZsTBED/FgiIRUQwzXTbg4CLNC3ZOs1nMwVyaCo0UN0Or1R4HNvyRgMlhg
# RvJYR4YyhB50YWeRX4FUsc+TTJLBxKZd0WETbijGGvmGgLvfYfxGwScdJGcSchoh
# iq9LZIlQYrFd/XcfPfBXday9ikJNQFHRD5wGPmd/9WbAA5ZEfu/QS/1u5ZrKsajy
# eioKMfDaTgaRtogINeh4HLDpmc085y9Euqf03GS9pAHBIAmTeM38vMDJRF1eFpwB
# BU8iTQIDAQABo4IB5jCCAeIwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFNVj
# OlyKMZDzQ3t8RhvFM2hahW1VMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsG
# A1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2VsuP6KJc
# YmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwubWljcm9z
# b2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIz
# LmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93d3cubWlj
# cm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3J0
# MIGgBgNVHSABAf8EgZUwgZIwgY8GCSsGAQQBgjcuAzCBgTA9BggrBgEFBQcCARYx
# aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL1BLSS9kb2NzL0NQUy9kZWZhdWx0Lmh0
# bTBABggrBgEFBQcCAjA0HjIgHQBMAGUAZwBhAGwAXwBQAG8AbABpAGMAeQBfAFMA
# dABhAHQAZQBtAGUAbgB0AC4gHTANBgkqhkiG9w0BAQsFAAOCAgEAB+aIUQ3ixuCY
# P4FxAz2do6Ehb7Prpsz1Mb7PBeKp/vpXbRkws8LFZslq3/Xn8Hi9x6ieJeP5vO1r
# VFcIK1GCRBL7uVOMzPRgEop2zEBAQZvcXBf/XPleFzWYJFZLdO9CEMivv3/Gf/I3
# fVo/HPKZeUqRUgCvOA8X9S95gWXZqbVr5MfO9sp6AG9LMEQkIjzP7QOllo9ZKby2
# /QThcJ8ySif9Va8v/rbljjO7Yl+a21dA6fHOmWaQjP9qYn/dxUoLkSbiOewZSnFj
# nXshbcOco6I8+n99lmqQeKZt0uGc+R38ONiU9MalCpaGpL2eGq4EQoO4tYCbIjgg
# tSXlZOz39L9+Y1klD3ouOVd2onGqBooPiRa6YacRy5rYDkeagMXQzafQ732D8OE7
# cQnfXXSYIghh2rBQHm+98eEA3+cxB6STOvdlR3jo+KhIq/fecn5ha293qYHLpwms
# ObvsxsvYgrRyzR30uIUBHoD7G4kqVDmyW9rIDVWZeodzOwjmmC3qjeAzLhIp9cAv
# VCch98isTtoouLGp25ayp0Kiyc8ZQU3ghvkqmqMRZjDTu3QyS99je/WZii8bxyGv
# WbWu3EQ8l1Bx16HSxVXjad5XwdHeMMD9zOZN+w2/XU/pnR4ZOC+8z1gFLu8NoFA1
# 2u8JJxzVs341Hgi62jbb01+P3nSISRKhggLXMIICQAIBATCCAQChgdikgdUwgdIx
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1p
# Y3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEmMCQGA1UECxMdVGhh
# bGVzIFRTUyBFU046RkM0MS00QkQ0LUQyMjAxJTAjBgNVBAMTHE1pY3Jvc29mdCBU
# aW1lLVN0YW1wIFNlcnZpY2WiIwoBATAHBgUrDgMCGgMVAEKl5h7yE6Y7MpfmMpEb
# QzkJclFToIGDMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0
# b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh
# dGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwDQYJ
# KoZIhvcNAQEFBQACBQDk7BhIMCIYDzIwMjEwOTE1MTQ1OTIwWhgPMjAyMTA5MTYx
# NDU5MjBaMHcwPQYKKwYBBAGEWQoEATEvMC0wCgIFAOTsGEgCAQAwCgIBAAICIngC
# Af8wBwIBAAICERgwCgIFAOTtacgCAQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYB
# BAGEWQoDAqAKMAgCAQACAwehIKEKMAgCAQACAwGGoDANBgkqhkiG9w0BAQUFAAOB
# gQBLLRh4r1m065GNIr1b1u/ITyaoE3JwixQx44zDQZWwKIK+Jx38LN/eXbWuGJpK
# +X0rwDLBsuA07FAo28z6mixd5YmEw3NsYWpLRTJfYLkbqZeemEQ1TiN16LuGPew1
# SzLhwIN4TlVtZQSyFIfwEvWNd8/4i6W+/t4YYOEYJ4ppaDGCAw0wggMJAgEBMIGT
# MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMT
# HU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABQCMZ1l7elSQxAAAA
# AAFAMA0GCWCGSAFlAwQCAQUAoIIBSjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQ
# AQQwLwYJKoZIhvcNAQkEMSIEIBRuCvOUYLsyVBJuQMwDhvHRFup3E40IwO5JrtCO
# /QtBMIH6BgsqhkiG9w0BCRACLzGB6jCB5zCB5DCBvQQgLzawterM0qRcJO/zcvJT
# 7do/ycp8RZsRSTqqtgIIl4MwgZgwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UE
# CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z
# b2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQ
# Q0EgMjAxMAITMwAAAUAjGdZe3pUkMQAAAAABQDAiBCDBtRxhfQdiy4OuZD0CYM/N
# keP3UhG8l5mWI9/2bOKUAjANBgkqhkiG9w0BAQsFAASCAQAqqyeR2SfwdrzGHtOA
# Mta4ynTVb6tRacNw6rHc/lTehjl0aO4KBIvS4e00bEEOUtWQbKqfvDG/gCU5xyZj
# 6vqx1HWVlOx/SIOTe83oy7JjN7wUYOpT0+Hs5peFpn7sgqZ5x/3ffMnY8WIHNyJw
# b7oZMv8SS1CKR3Mkv58a18JZ9yBWfFix8Umy9K8Ks+ZX7mHMGE5dfjsZ05PJWkRG
# ud4MYm20fsQ4WDjoN+Gbyq/h4tGo9xvb4+OE3FHU8WeHbVeppCPSA6qtIhNi94yO
# hYRdPMTiLuBjFcJz8lwCknwssgnFuzE/KzllcOGHc9Qpo5N0lYKEVBp69/eMk+O0
# Xovl
# SIG # End signature block