Framework/Managers/ControlStateExtension.ps1
using namespace System.Management.Automation Set-StrictMode -Version Latest class ControlStateExtension { #Static attestation index file object. #This gets cashed for every scan and reset for every fresh scan command in servicessecurity status [PSObject] $ControlStateIndexer = $null; #Property indicates if Attestation index file is present in blob [bool] $IsControlStateIndexerPresent = $true; hidden [int] $HasControlStateReadPermissions = 1; hidden [int] $HasControlStateWritePermissions = -1; hidden [string] $IndexerBlobName ="Resource.index.json" hidden [int] $retryCount = 3; hidden [string] $UniqueRunId; hidden [SubscriptionContext] $SubscriptionContext; hidden [InvocationInfo] $InvocationContext; hidden [PSObject] $ControlSettings; hidden [PSObject] $resourceType; hidden [PSObject] $resourceName; hidden [PSObject] $resourceGroupName; hidden [PSObject] $AttestationBody; [bool] $IsPersistedControlStates = $false; [bool] $FailedDownloadForControlStateIndexer = $false #hidden [bool] $PrintExtStgPolicyProjErr = $true; hidden [bool] $PrintParamPolicyProjErr = $true; hidden [bool] $PrintAttestationRepoErr = $true; hidden static [bool] $IsOrgAttestationProjectFound = $false; # Flag to represent if Host proj(attestation repo) is avilable for org controls. FALSE => Project or Repo not yet found. hidden [AzSKSettings] $AzSKSettings; ControlStateExtension([SubscriptionContext] $subscriptionContext, [InvocationInfo] $invocationContext) { $this.SubscriptionContext = $subscriptionContext; $this.InvocationContext = $invocationContext; $this.ControlSettings = [ConfigurationManager]::LoadServerConfigFile("ControlSettings.json"); $this.AttestationBody = [ConfigurationManager]::LoadServerConfigFile("ADOAttestation.json"); } static [string] ComputeHashX([string] $dataToHash) { return [Helpers]::ComputeHashShort($dataToHash, [Constants]::AttestationHashLen) } hidden [void] Initialize([bool] $CreateResourcesIfNotExists) { if([string]::IsNullOrWhiteSpace($this.UniqueRunId)) { $this.UniqueRunId = $(Get-Date -format "yyyyMMdd_HHmmss"); } # this function to check and set access permission $this.SetControlStatePermission(); #Reset attestation index file and set attestation index file present flag to get fresh index file from storage $this.ControlStateIndexer = $null; $this.IsControlStateIndexerPresent = $true } # fetch allowed group for attestation from setting file and check user is member of this group and set acccess permission hidden [void] SetControlStatePermission() { try { $this.HasControlStateWritePermissions = 1 } catch { $this.HasControlStateWritePermissions = 0 } } hidden [bool] ComputeControlStateIndexer() { try { $AzSKTemp = Join-Path $([Constants]::AzSKAppFolderPath) "Temp" | Join-Path -ChildPath $this.UniqueRunId | Join-Path -ChildPath "ServerControlState"; if(-not (Test-Path -Path $AzSKTemp)) { New-Item -ItemType Directory -Path $AzSKTemp -Force | Out-Null } $indexerObject = Get-ChildItem -Path (Join-Path $AzSKTemp $($this.IndexerBlobName)) -Force -ErrorAction Stop | Get-Content | ConvertFrom-Json } catch { #Write-Host $_ } #Cache code: Fetch index file only if index file is null and it is present on storage blob if(-not $this.ControlStateIndexer -and $this.IsControlStateIndexerPresent) { #Attestation index blob is not preset then return [ControlStateIndexer[]] $indexerObjects = @(); $this.ControlStateIndexer = $indexerObjects $AzSKTemp = Join-Path $([Constants]::AzSKAppFolderPath) "Temp" | Join-Path -ChildPath $this.UniqueRunId | Join-Path -ChildPath "ServerControlState"; if(-not (Test-Path -Path $AzSKTemp)) { New-Item -ItemType Directory -Path $AzSKTemp -Force | Out-Null } $indexerObject = @(); $loopValue = $this.retryCount; while($loopValue -gt 0) { $loopValue = $loopValue - 1; try { #FailedDownloadForControlStateIndexer is used if file present in repo then variable is false, if file not present then it goes to exception so variable value is true. #If file resent in repo with no content, there will be no exception in api call and respose body will be null $this.FailedDownloadForControlStateIndexer = $false $webRequestResult = $this.GetRepoFileContent( $this.IndexerBlobName ); if($webRequestResult){ $indexerObject = $webRequestResult } else { if ($this.FailedDownloadForControlStateIndexer -eq $false) { $this.IsControlStateIndexerPresent = $true } else { $this.IsControlStateIndexerPresent = $false } } $loopValue = 0; } catch{ #Attestation index blob is not preset then return $this.IsControlStateIndexerPresent = $false return $true; } } $this.ControlStateIndexer += $indexerObject; } return $true; } # set indexer for rescan post attestation hidden [PSObject] RescanComputeControlStateIndexer([string] $projectName, [string] $resourceType) { #$this.resourceType is used inside the GetProject method to get the project name for organization from extension storage, also return project for other resources $this.resourceType = $resourceType; if ($resourceType -eq "Organization" -or $resourceType -eq "Project") { $this.resourceName = $projectName } else { $this.resourceGroupName = $projectName } [PSObject] $ControlStateIndexerForRescan = $this.GetRepoFileContent($this.IndexerBlobName ); #setting below global variables null as needed for next resource. $this.resourceType = $null; $this.resourceName = ""; $this.resourceGroupName = ""; return $ControlStateIndexerForRescan; } #isRescan parameter is added to check if method is called from rescan. hidden [PSObject] GetControlState([string] $id, [string] $resourceType, [string] $resourceName, [string] $resourceGroupName, [bool] $isRescan = $false) { try { $this.resourceType = $resourceType; $this.resourceName = $resourceName $this.resourceGroupName = $resourceGroupName [ControlState[]] $controlStates = @(); if(!$this.GetProject()) { return $null; } # We reset ControlStateIndexer to null whenever we move to a new project (project context switch) if($this.resourceType -eq "Project" ){ $this.ControlStateIndexer = $null; $this.IsControlStateIndexerPresent = $true; } #getting resource.index for rescan [PSObject] $ControlStateIndexerForRescan = $null; [bool] $retVal = $true; if ($isRescan) { #this is to set project name from GetProject method $projectName = $resourceName; if ($resourceType -ne "Organization" -and $resourceType -ne "Project") { $projectName = $resourceGroupName } $ControlStateIndexerForRescan = $this.RescanComputeControlStateIndexer($projectName, $resourceType); #Above method setting below blobal variable null so settting them again. $this.resourceType = $resourceType; $this.resourceName = $resourceName $this.resourceGroupName = $resourceGroupName } else { $retVal = $this.ComputeControlStateIndexer(); } if(($null -ne $this.ControlStateIndexer -and $retVal) -or $isRescan) { $indexes = @(); if ($isRescan) { $indexes = $ControlStateIndexerForRescan; } else { $indexes += $this.ControlStateIndexer } $hashId = [ControlStateExtension]::ComputeHashX($id) $selectedIndex = $indexes | Where-Object { $_.HashId -eq $hashId} if(($selectedIndex | Measure-Object).Count -gt 0) { $hashId = $selectedIndex.HashId | Select-Object -Unique $controlStateBlobName = $hashId + ".json" $ControlStatesJson = $null; #Fetch attestation file content from repository $ControlStatesJson = $this.GetRepoFileContent($controlStateBlobName) if($ControlStatesJson ) { $retVal = $true; } else { $retVal = $false; } #$ControlStatesJson = Get-ChildItem -Path (Join-Path $AzSKTemp $controlStateBlobName) -Force | Get-Content | ConvertFrom-Json if($null -ne $ControlStatesJson) { $ControlStatesJson | ForEach-Object { try { $controlState = [ControlState] $_ $controlStates += $controlState; } catch { [EventBase]::PublishGenericException($_); } } } } } if($this.resourceType -eq "Organization" ){ $this.ControlStateIndexer = $null; $this.IsControlStateIndexerPresent = $true; } return $controlStates; } catch{ if($this.resourceType -eq "Organization"){ $this.ControlStateIndexer = $null; $this.IsControlStateIndexerPresent = $true; } [EventBase]::PublishGenericException($_); return $null; } } hidden [void] SetControlState([string] $id, [ControlState[]] $controlStates, [bool] $Override, [string] $resourceType, [string] $resourceName, [string] $resourceGroupName) { $this.resourceType = $resourceType; $this.resourceName = $resourceName; $this.resourceGroupName = $resourceGroupName if(!$this.GetProject()) { return } $AzSKTemp = Join-Path $([Constants]::AzSKAppFolderPath) "Temp" | Join-Path -ChildPath $this.UniqueRunId | Join-Path -ChildPath "ServerControlState"; if(-not (Test-Path $(Join-Path $AzSKTemp "ControlState"))) { New-Item -ItemType Directory -Path $(Join-Path $AzSKTemp "ControlState") -ErrorAction Stop | Out-Null } else { Remove-Item -Path $(Join-Path $AzSKTemp "ControlState" | Join-Path -ChildPath '*' ) -Force -Recurse } $hash = [ControlStateExtension]::ComputeHashX($id) $indexerPath = Join-Path $AzSKTemp "ControlState" | Join-Path -ChildPath $this.IndexerBlobName; if(-not (Test-Path -Path (Join-Path $AzSKTemp "ControlState"))) { New-Item -ItemType Directory -Path (Join-Path $AzSKTemp "ControlState") -Force } $fileName = Join-Path $AzSKTemp "ControlState" | Join-Path -ChildPath ($hash+".json"); #Filter out the "Passed" controls $finalControlStates = $controlStates | Where-Object { $_.ActualVerificationResult -ne [VerificationResult]::Passed}; if(($finalControlStates | Measure-Object).Count -gt 0) { $this.IsPersistedControlStates = $false; if($Override) { $this.IsPersistedControlStates = $true; # in the case of override, just persist what is evaluated in the current context. No merging with older data $this.UpdateControlIndexer($id, $finalControlStates, $false); $finalControlStates = $finalControlStates | Where-Object { $_.State}; } else { #merge with the exiting if found $persistedControlStates = $this.GetPersistedControlStates("$hash.json"); $finalControlStates = $this.MergeControlStates($persistedControlStates, $finalControlStates); # COmmenting this code out. We will be handling encoding-decoding to b64 at SetStateData and WriteDetailedLogs.ps1 #$finalControl = @(); ##convert state data object to encoded string #foreach ($controls in $finalControlStates) { # # checking If state.DataObject is not empty and dataobject is not encode string, if control is already attested it will have encoded string # if ($controls.state.DataObject -and !($controls.state.DataObject -is [string]) ) { # try { # #when dataobject is empty it comes like {} and null check does not work it alwasys count 1 # if ($controls.state.DataObject.count -gt 0) { # $stateData = $controls.state.DataObject | ConvertTo-Json -Depth 10 # $encodedStateData =[Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($stateData)) # $controls.state.DataObject = $encodedStateData; # } # } # catch { # #eat the exception # } # } # $finalControl += $controls; #} #$finalControlStates = $finalControl; $this.UpdateControlIndexer($id, $finalControlStates, $false); } } else { #purge would remove the entry from the control indexer and also purge the stale state json. $this.PurgeControlState($id); } if(($finalControlStates|Measure-Object).Count -gt 0) { [JsonHelper]::ConvertToJsonCustom($finalControlStates) | Out-File $fileName -Force } if($null -ne $this.ControlStateIndexer) { [JsonHelper]::ConvertToJsonCustom($this.ControlStateIndexer) | Out-File $indexerPath -Force $controlStateArray = Get-ChildItem -Path (Join-Path $AzSKTemp "ControlState") $controlStateArray | ForEach-Object { $state = $_; try { $this.UploadFileContent($state.FullName); } catch { $_ #eat this exception and retry } } } } [void] UploadFileContent( $FullName ) { $fileContent = Get-Content -Path $FullName -raw $fileName = $FullName.split('\')[-1]; $projectName = $this.GetProject(); $attestationRepo = [Constants]::AttestationRepo; #Get attesttion repo name from controlsetting file if AttestationRepo varibale value is not empty. if ([Helpers]::CheckMember($this.ControlSettings,"AttestationRepo")) { $attestationRepo = $this.ControlSettings.AttestationRepo; } $rmContext = [ContextHelper]::GetCurrentContext(); $user = ""; $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user,$rmContext.AccessToken))) $uri = "https://dev.azure.com/{0}/{1}/_apis/git/repositories/{2}/refs?api-version=5.0" -f $this.SubscriptionContext.subscriptionid, $projectName, $attestationRepo try { $webRequest = Invoke-RestMethod -Uri $uri -Method Get -ContentType "application/json" -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)} $branchName = [Constants]::AttestationDefaultBranch; #Get attesttion branch name from controlsetting file if AttestationBranch varibale value is not empty. if ([Helpers]::CheckMember($this.ControlSettings,"AttestationBranch")) { $branchName = $this.ControlSettings.AttestationBranch; } $branchId = ($webRequest.value | where {$_.name -eq "refs/heads/"+$branchName}).ObjectId $uri = [Constants]::AttRepoStorageUri -f $this.SubscriptionContext.subscriptionid, $projectName, $attestationRepo $body = $this.CreateBody($fileContent, $fileName, $branchId, $branchName); $webRequestResult = Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)} -Body $body if ($fileName -eq $this.IndexerBlobName) { $this.IsControlStateIndexerPresent = $true; } } catch { Write-Host "Error: Attestation denied.`nThis may be because: `n (a) $($attestationRepo) repository is not present in the project `n (b) you do not have write permission on the repository. `n" -ForegroundColor Red Write-Host "See more at https://aka.ms/adoscanner (search for 'ADOScannerAttestation' on the page). `n" -ForegroundColor Yellow } } [string] CreateBody([string] $fileContent, [string] $fileName, [string] $branchId, [string] $branchName){ $body = $this.AttestationBody.Post | ConvertTo-Json -Depth 10 $body = $body.Replace("{0}",$branchId) $body = $body.Replace("{2}", $this.CreatePath($fileName)) if ( $this.IsControlStateIndexerPresent -and $fileName -eq $this.IndexerBlobName ) { $body = $body.Replace("{1}","edit") } elseif ($this.IsPersistedControlStates -and $fileName -ne $this.IndexerBlobName ) { $body = $body.Replace("{1}","edit") } else { $body = $body.Replace("{1}","add") } $content = ($fileContent | ConvertTo-Json -Depth 10) -replace '^.|.$', '' $body = $body.Replace("{3}", $content) $body = $body.Replace("{4}", $branchName) return $body; } [string] CreatePath($fileName){ $path = $fileName if (!($this.resourceType -eq "Organization" -or $fileName -eq $this.IndexerBlobName) -and ($this.resourceType -ne "Project")) { $path = $this.resourceGroupName + "/" + $this.resourceType + "/" + $fileName; } elseif(!($this.resourceType -eq "Organization" -or $fileName -eq $this.IndexerBlobName)) { $path = $this.resourceName + "/" + $fileName; } return $path; } [string] GetProject(){ $projectName = ""; if ($this.resourceType -eq "Organization" -or $this.resourceType -eq $null) { if($this.InvocationContext) { #Get project name from ext storage to fetch org attestation $projectName = $this.GetProjectNameFromExtStorage(); #If not found then check if 'PolicyProject' parameter is provided in command if ([string]::IsNullOrEmpty($projectName)) { $projectName = $this.InvocationContext.BoundParameters["PolicyProject"] if ([string]::IsNullOrEmpty($projectName)) { #TODO: azsk setting fetching and add comment for EnableOrgControlAttestation if (!$this.AzSKSettings) { $this.AzSKSettings = [ConfigurationManager]::GetAzSKSettings(); } $projectName = $this.AzSKSettings.PolicyProject $enableOrgControlAttestation = $this.AzSKSettings.EnableOrgControlAttestation if([string]::IsNullOrEmpty($projectName)) { if ($this.PrintParamPolicyProjErr -eq $true -and $enableOrgControlAttestation -eq $true) { Write-Host -ForegroundColor Yellow "Could not fetch attestation-project-name. `nYou can: `n`r(a) Run Set-AzSKADOMonitoringSetting -PolicyProject '<PolicyProjectName>' or `n`r(b) Use '-PolicyProject' parameter to specify the host project containing attestation details of organization controls." $this.PrintParamPolicyProjErr = $false; } } } #If $projectName was set in the above if clause - we need to next validate whether this project has an attestattion repo as shown below. if(-not [string]::IsNullOrEmpty($projectName)) { if ([ControlStateExtension]::IsOrgAttestationProjectFound -eq $false) { #Validate if Attestation repo is available in policy project $attestationRepo = [Constants]::AttestationRepo; try { $rmContext = [ContextHelper]::GetCurrentContext(); $user = ""; $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user,$rmContext.AccessToken))) #Get attesttion repo name from controlsetting file if AttestationRepo varibale value is not empty. if ([Helpers]::CheckMember($this.ControlSettings,"AttestationRepo")) { $attestationRepo = $this.ControlSettings.AttestationRepo; } $uri = "https://dev.azure.com/{0}/{1}/_apis/git/repositories/{2}/refs?api-version=5.0" -f $this.SubscriptionContext.subscriptionid, $projectName, $attestationRepo $webRequest = Invoke-RestMethod -Uri $uri -Method Get -ContentType "application/json" -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)} [ControlStateExtension]::IsOrgAttestationProjectFound = $true # Policy project and repo found } catch { $projectName = ""; #2010 ToDO: [ControlStateExtension]::IsOrgAttestationProjectFound = $false # Policy project and repo found if ($this.PrintAttestationRepoErr -eq $true) { Write-Host -ForegroundColor Yellow "Could not find attestation repo [$($attestationRepo)] in the policy project." $this.PrintAttestationRepoErr = $false; } # eat exception. This means attestation repo was not found # attestation repo is required to scan org controls and send hasrequiredaccess as true } } } }} } elseif($this.resourceType -eq "Project" ) { $projectName = $this.resourceName } else { $projectName = $this.resourceGroupName } return $projectName; } [string] GetProjectNameFromExtStorage() { try { $rmContext = [ContextHelper]::GetCurrentContext(); $user = ""; $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user,$rmContext.AccessToken))) $uri = [Constants]::StorageUri -f $this.SubscriptionContext.subscriptionid, $this.SubscriptionContext.subscriptionid, [Constants]::OrgAttPrjExtFile $webRequestResult = Invoke-RestMethod -Uri $uri -Method Get -ContentType "application/json" -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)} #If repo is not found, we will fall into the catch block from IRM call above [ControlStateExtension]::IsOrgAttestationProjectFound = $true # Policy project found return $webRequestResult.Project } catch { #2010 ToDo: [ControlStateExtension]::IsOrgAttestationProjectFound = $false # Policy project not found return $null; } } [bool] SetProjectInExtForOrg() { $projectName = $this.InvocationContext.BoundParameters["AttestationHostProjectName"] $rmContext = [ContextHelper]::GetCurrentContext(); $user = ""; $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user, $rmContext.AccessToken))) $fileName = [Constants]::OrgAttPrjExtFile $apiURL = "https://dev.azure.com/{0}/_apis/projects/{1}?api-version=6.0" -f $($this.SubscriptionContext.SubscriptionName), $projectName; try { $responseObj = [WebRequestHelper]::InvokeGetWebRequest($apiURL) ; #$projects = $responseObj | Where-Object { $projectName -contains $_.name } #if ($null -eq $projects) { # Write-Host "$($projectName) Project not found: Incorrect project name or you do not have neccessary permission to access the project." -ForegroundColor Red # return $false #} } catch { Write-Host "$($projectName) Project not found: Incorrect project name or you do not have neccessary permission to access the project." -ForegroundColor Red return $false } $uri = [Constants]::StorageUri -f $this.SubscriptionContext.subscriptionid, $this.SubscriptionContext.subscriptionid, $fileName try { $webRequestResult = Invoke-RestMethod -Uri $uri -Method Get -ContentType "application/json" -Headers @{Authorization = ("Basic {0}" -f $base64AuthInfo) } Write-Host "Project $($webRequestResult.Project) is already configured to store attestation details for organization-specific controls." -ForegroundColor Yellow } catch { $body = @{"id" = "$fileName"; "Project" = $projectName; } | ConvertTo-Json $uri = [Constants]::StorageUri -f $this.SubscriptionContext.subscriptionid, $this.SubscriptionContext.subscriptionid, $fileName try { $webRequestResult = Invoke-RestMethod -Uri $uri -Method Put -ContentType "application/json" -Headers @{Authorization = ("Basic {0}" -f $base64AuthInfo) } -Body $body return $true; } catch { Write-Host "Error: Could not configure host project for attestation of org-specific controls because 'ADOSecurityScanner' extension is not installed in your organization." -ForegroundColor Red } } return $false; } [PSObject] GetRepoFileContent($fileName) { $projectName = $this.GetProject(); $branchName = [Constants]::AttestationDefaultBranch #Get attesttion branch name from controlsetting file if AttestationBranch varibale value is not empty. if ([Helpers]::CheckMember($this.ControlSettings,"AttestationBranch")) { $branchName = $this.ControlSettings.AttestationBranch; } $fileName = $this.CreatePath($fileName); $rmContext = [ContextHelper]::GetCurrentContext(); $user = ""; $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user,$rmContext.AccessToken))) try { $attestationRepo = [Constants]::AttestationRepo; #Get attesttion repo name from controlsetting file if AttestationRepo varibale value is not empty. if ([Helpers]::CheckMember($this.ControlSettings,"AttestationRepo")) { $attestationRepo = $this.ControlSettings.AttestationRepo; } $uri = [Constants]::GetAttRepoStorageUri -f $this.SubscriptionContext.subscriptionid, $projectName, $attestationRepo, $fileName, $branchName $webRequestResult = Invoke-RestMethod -Uri $uri -Method Get -ContentType "application/json" -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)} if ($webRequestResult) { # COmmenting this code out. We will be handling encoding-decoding to b64 at SetStateData and WriteDetailedLogs.ps1 #if($fileName -ne $this.IndexerBlobName) #{ # #convert back state data from encoded string # $attestationData = @(); # foreach ($controls in $webRequestResult) # { # if($controls.State.DataObject -is [string]) # { # $controls.State.DataObject = [System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($controls.State.DataObject)) | ConvertFrom-Json # } # $attestationData += $controls; # } # $webRequestResult = $attestationData; #} return $webRequestResult } return $null; } catch{ if ($fileName -eq $this.IndexerBlobName) { $this.FailedDownloadForControlStateIndexer = $true } return $null; } } [void] RemoveAttestationData($fileName) { $projectName = $this.GetProject(); $fileName = $this.CreatePath($fileName); $attestationRepo = [Constants]::AttestationRepo; #Get attesttion repo name from controlsetting file if AttestationRepo varibale value is not empty. if ([Helpers]::CheckMember($this.ControlSettings,"AttestationRepo")) { $attestationRepo = $this.ControlSettings.AttestationRepo; } $rmContext = [ContextHelper]::GetCurrentContext(); $user = ""; $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user,$rmContext.AccessToken))) $uri = "https://dev.azure.com/{0}/{1}/_apis/git/repositories/{2}/refs?api-version=5.0" -f $this.SubscriptionContext.subscriptionid, $projectName, $attestationRepo $webRequest = Invoke-RestMethod -Uri $uri -Method Get -ContentType "application/json" -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)} $branchId = ($webRequest.value | where {$_.name -eq 'refs/heads/master'}).ObjectId $body = $this.AttestationBody.Delete | ConvertTo-Json -Depth 10; $body = $body.Replace('{0}',$branchId) $body = $body.Replace('{1}',$fileName) $branchName = [Constants]::AttestationDefaultBranch; #Get attesttion branch name from controlsetting file if AttestationBranch varibale value is not empty. if ([Helpers]::CheckMember($this.ControlSettings,"AttestationBranch")) { $branchName = $this.ControlSettings.AttestationBranch; } $body = $body.Replace('{2}',$branchName) try { $uri = [Constants]::AttRepoStorageUri -f $this.SubscriptionContext.subscriptionid, $projectName, $attestationRepo $webRequestResult = Invoke-RestMethod -Uri $uri -Method Post -ContentType "application/json" -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)} -Body $body } catch{ Write-Host "Could not remove attastation for: " + $fileName; Write-Host $_ } } hidden [void] PurgeControlState([string] $id) { $AzSKTemp = Join-Path $([Constants]::AzSKAppFolderPath) "Temp" | Join-Path -ChildPath $this.UniqueRunId | Join-Path -ChildPath "ServerControlState"; if(-not (Test-Path $(Join-Path $AzSKTemp "ControlState"))) { New-Item -ItemType Directory -Path (Join-Path $AzSKTemp "ControlState") -ErrorAction Stop | Out-Null } else { Remove-Item -Path $(Join-Path $AzSKTemp "ControlState" | Join-Path -ChildPath '*') -Force -Recurse } $hash = [ControlStateExtension]::ComputeHashX($id); $indexerPath = Join-Path $AzSKTemp "ControlState" | Join-Path -ChildPath $this.IndexerBlobName ; $fileName = Join-Path $AzSKTemp "ControlState" | Join-Path -ChildPath ("$hash.json"); $this.UpdateControlIndexer($id, $null, $true); if($null -ne $this.ControlStateIndexer) { [JsonHelper]::ConvertToJsonCustom($this.ControlStateIndexer) | Out-File $indexerPath -Force $controlStateArray = Get-ChildItem -Path (Join-Path $AzSKTemp "ControlState"); $controlStateArray | ForEach-Object { $state = $_ $loopValue = $this.retryCount; while($loopValue -gt 0) { $loopValue = $loopValue - 1; try { $this.UploadFileContent($state.FullName); $loopValue = 0; } catch { #eat this exception and retry } } } } try { $hashFile = "$hash.json"; $this.RemoveAttestationData($hashFile) } catch { #eat this exception and retry } } hidden [ControlState[]] GetPersistedControlStates([string] $controlStateBlobName) { $AzSKTemp = Join-Path $([Constants]::AzSKAppFolderPath) "Temp" | Join-Path -ChildPath $this.UniqueRunId | Join-Path -ChildPath "ServerControlState"; if(-not (Test-Path (Join-Path $AzSKTemp "ExistingControlStates"))) { New-Item -ItemType Directory -Path (Join-Path $AzSKTemp "ExistingControlStates") -ErrorAction Stop | Out-Null } [ControlState[]] $ControlStatesJson = @() $loopValue = $this.retryCount; while($loopValue -gt 0) { $loopValue = $loopValue - 1; try { #$ControlStatesJson = @() $ControlStatesJson = $this.GetRepoFileContent($controlStateBlobName) if ($ControlStatesJson) { $this.IsPersistedControlStates = $true } $loopValue = 0; } catch { $this.IsPersistedControlStates = $false; #$ControlStatesJson = @() #eat this exception and retry } } return $ControlStatesJson } hidden [ControlState[]] MergeControlStates([ControlState[]] $persistedControlStates,[ControlState[]] $controlStates) { [ControlState[]] $computedControlStates = $controlStates; if(($computedControlStates | Measure-Object).Count -le 0) { $computedControlStates = @(); } if(($persistedControlStates | Measure-Object).Count -gt 0) { $persistedControlStates | ForEach-Object { $controlState = $_; if(($computedControlStates | Where-Object { ($_.InternalId -eq $controlState.InternalId) -and ($_.ChildResourceName -eq $controlState.ChildResourceName) } | Measure-Object).Count -le 0) { $computedControlStates += $controlState; } } } #remove the control states with null state which would be in the case of clear attestation. $computedControlStates = $computedControlStates | Where-Object { $_.State} return $computedControlStates; } hidden [void] UpdateControlIndexer([string] $id, [ControlState[]] $controlStates, [bool] $ToBeDeleted) { $this.ControlStateIndexer = $null; $retVal = $this.ComputeControlStateIndexer(); if($retVal) { $tempHash = [ControlStateExtension]::ComputeHashX($id); #take the current indexer value $filteredIndexerObject = $null; $filteredIndexerObject2 = $null; if ($this.ControlStateIndexer -and ($this.ControlStateIndexer | Measure-Object).Count -gt 0) { $filteredIndexerObject = $this.ControlStateIndexer | Where-Object { $_.HashId -eq $tempHash} #remove the current index from the list $filteredIndexerObject2 = $this.ControlStateIndexer | Where-Object { $_.HashId -ne $tempHash} } $this.ControlStateIndexer = @(); if($filteredIndexerObject2) { $this.ControlStateIndexer += $filteredIndexerObject2 } if(-not $ToBeDeleted) { $currentIndexObject = $null; #check if there is an existing index and the controlstates are present for that index resource if(($filteredIndexerObject | Measure-Object).Count -gt 0 -and ($controlStates | Measure-Object).Count -gt 0) { $currentIndexObject = $filteredIndexerObject; if(($filteredIndexerObject | Measure-Object).Count -gt 1) { $currentIndexObject = $filteredIndexerObject | Select-Object -Last 1 } $currentIndexObject.AttestedBy = [ContextHelper]::GetCurrentSessionUser(); $currentIndexObject.AttestedDate = [DateTime]::UtcNow; $currentIndexObject.Version = "1.0"; } elseif(($controlStates | Measure-Object).Count -gt 0) { $currentIndexObject = [ControlStateIndexer]::new(); $currentIndexObject.ResourceId = $id $currentIndexObject.HashId = $tempHash; $currentIndexObject.AttestedBy = [ContextHelper]::GetCurrentSessionUser(); $currentIndexObject.AttestedDate = [DateTime]::UtcNow; $currentIndexObject.Version = "1.0"; } if($null -ne $currentIndexObject) { $this.ControlStateIndexer += $currentIndexObject; } } } } [bool] HasControlStateReadAccessPermissions() { if($this.HasControlStateReadPermissions -le 0) { return $false; } else { return $true; } } [void] SetControlStateReadAccessPermissions([int] $value) { $this.HasControlStateReadPermissions = $value } [void] SetControlStateWriteAccessPermissions([int] $value) { $this.HasControlStateWritePermissions = $value } [bool] HasControlStateWriteAccessPermissions() { if($this.HasControlStateWritePermissions -le 0) { return $false; } else { return $true; } } [bool] GetControlStatePermission([string] $featureName, [string] $resourceName) { try { $this.HasControlStateWritePermissions = 0 $allowedGrpForOrgAtt = $this.ControlSettings.AllowAttestationByGroups | where { $_.ResourceType -eq "Organization" } | select-object -property GroupNames $url= "https://{0}.visualstudio.com/_apis/Contribution/HierarchyQuery?api-version=5.1-preview" -f $($this.SubscriptionContext.SubscriptionName); $postbody="{'contributionIds':['ms.vss-admin-web.org-admin-groups-data-provider'],'dataProviderContext':{'properties':{'sourcePage':{'url':'https://$($this.SubscriptionContext.SubscriptionName).visualstudio.com/_settings/groups','routeId':'ms.vss-admin-web.collection-admin-hub-route','routeValues':{'adminPivot':'groups','controller':'ContributedPage','action':'Execute'}}}}}" | ConvertFrom-Json $groupsOrgObj = [WebRequestHelper]::InvokePostWebRequest($url,$postbody); $groupsOrgObj = $groupsOrgObj.dataProviders.'ms.vss-admin-web.org-admin-groups-data-provider'.identities | where { $allowedGrpForOrgAtt.GroupNames -contains $_.displayName } if($this.CheckGroupMemberPCA($groupsOrgObj.descriptor)){ return $true; } if($featureName -ne "Organization") { $allowedGrpForAtt = $this.ControlSettings.AllowAttestationByGroups | where { $_.ResourceType -eq $featureName } | select-object -property GroupNames $url = 'https://dev.azure.com/{0}/_apis/Contribution/HierarchyQuery?api-version=5.0-preview.1' -f $($this.SubscriptionContext.SubscriptionName); $inputbody = '{"contributionIds":["ms.vss-admin-web.org-admin-groups-data-provider"],"dataProviderContext":{"properties":{"sourcePage":{"url":"","routeId":"ms.vss-admin-web.project-admin-hub-route","routeValues":{"project":"","adminPivot":"permissions","controller":"ContributedPage","action":"Execute"}}}}}' | ConvertFrom-Json $inputbody.dataProviderContext.properties.sourcePage.url = "https://dev.azure.com/$($this.SubscriptionContext.SubscriptionName)/$($resourceName)/_settings/permissions"; $inputbody.dataProviderContext.properties.sourcePage.routeValues.Project =$resourceName; $groupsObj = [WebRequestHelper]::InvokePostWebRequest($url,$inputbody); $groupsObj = $groupsObj.dataProviders."ms.vss-admin-web.org-admin-groups-data-provider".identities | where { $allowedGrpForAtt.GroupNames -contains $_.displayName } foreach ($group in $groupsObj) { if($this.CheckGroupMemberPA($group.descriptor,$resourceName)){ return $true; } } } if($this.HasControlStateWritePermissions -gt 0) { return $true } else { return $false } } catch { $this.HasControlStateWritePermissions = 0 return $false; } } [bool] CheckGroupMemberPA($descriptor,[string] $resourceName) { <# $inputbody = '{"contributionIds":["ms.vss-admin-web.org-admin-members-data-provider"],"dataProviderContext":{"properties":{"subjectDescriptor":"","sourcePage":{"url":"","routeId":"ms.vss-admin-web.collection-admin-hub-route","routeValues":{"adminPivot":"groups","controller":"ContributedPage","action":"Execute"}}}}}' | ConvertFrom-Json $inputbody.dataProviderContext.properties.subjectDescriptor = $descriptor; $inputbody.dataProviderContext.properties.sourcePage.url = "https://dev.azure.com/$($this.SubscriptionContext.SubscriptionName)/_settings/groups?subjectDescriptor=$($descriptor)"; $apiURL = "https://dev.azure.com/{0}/_apis/Contribution/HierarchyQuery?api-version=5.0-preview" -f $($this.SubscriptionContext.SubscriptionName); $groupMembersObj = [WebRequestHelper]::InvokePostWebRequest($apiURL,$inputbody); $users = $groupMembersObj.dataProviders."ms.vss-admin-web.org-admin-members-data-provider".identities | where {$_.subjectKind -eq "user"} if($null -ne $users){ $currentUser = [ContextHelper]::GetCurrentSessionUser(); $grpmember = ($users | where { $_.mailAddress -eq $currentUser } ); if ($null -ne $grpmember ) { $this.HasControlStateWritePermissions = 1 return $true; } } if($this.HasControlStateWritePermissions -gt 0) { return $true } else { return $false }#> $isUserPA=[AdministratorHelper]::GetIsCurrentUserPA($descriptor,$this.SubscriptionContext.SubscriptionName,$resourceName); if($isUserPA -eq $true){ $this.HasControlStateWritePermissions = 1 return $true; } if($this.HasControlStateWritePermissions -gt 0) { return $true } else { return $false } } [bool] CheckGroupMemberPCA($descriptor){ $isUserPCA=[AdministratorHelper]::GetIsCurrentUserPCA($descriptor,$this.SubscriptionContext.SubscriptionName); if($isUserPCA -eq $true){ $this.HasControlStateWritePermissions = 1 return $true; } if($this.HasControlStateWritePermissions -gt 0) { return $true } else { return $false } } } # SIG # Begin signature block # MIIheAYJKoZIhvcNAQcCoIIhaTCCIWUCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAn8+5K/tmHPPRx # pUivM4O3vgUOK1eRqXTnMuRxTVUDEKCCC28wggTrMIID06ADAgECAhMzAAAD53EW # vSG3L5ZCAAAAAAPnMA0GCSqGSIb3DQEBCwUAMHkxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xIzAhBgNVBAMTGk1pY3Jvc29mdCBUZXN0aW5nIFBD # QSAyMDEwMB4XDTIwMDMwNDE5NTgzOVoXDTIxMDMwMzE5NTgzOVowfDELMAkGA1UE # BhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAc # BgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdQ29kZSBTaWdu # IFRlc3QgKERPIE5PVCBUUlVTVCkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK # AoIBAQC0dFU8yYFFisft2syLgnrgoEcOrrzraGs5owrAJ8YWyYuxhhk7UUJP0YAC # wuDAlPQMHrhnEhZsqmD7DfWGzz33gxe7hvcNpHdhItPpgXiVkh3thZrWz4jfHFGc # RMW1zyebGUJ16gN5cYWsI18Pax9tBZW1YZIef2hIQNU5Vr5QhVKZVAbaqZFqJRo+ # 51czrP44ZnofEMr3Z3HBmIS7C97kkFYS/G8JpkufIuDsTchX7dWduHhMbFIem+Zx # nT7mrsps0D5hXV3L9JPe8TFm1T0iwaFy6RWFaWPelibrTryIbWk6Qrv4Lz89WMM6 # XFxlrqQVphAmhns1+rNrr6yacRCtAgMBAAGjggFnMIIBYzATBgNVHSUEDDAKBggr # BgEFBQcDAzAdBgNVHQ4EFgQUseZoPiUpJDttlBAhnIzqzbcXsK4wUAYDVR0RBEkw # R6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1ZXJ0byBSaWNv # MRYwFAYDVQQFEw0yMzAwNzIrNDU4Mzk0MB8GA1UdIwQYMBaAFN3WR4sjFC/YOGhC # oz5tw/CQ9yzQMFMGA1UdHwRMMEowSKBGoESGQmh0dHA6Ly9jcmwubWljcm9zb2Z0 # LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Rlc1BDQV8yMDEwLTA3LTAxLmNybDBX # BggrBgEFBQcBAQRLMEkwRwYIKwYBBQUHMAKGO2h0dHA6Ly93d3cubWljcm9zb2Z0 # LmNvbS9wa2kvY2VydHMvTWljVGVzUENBXzIwMTAtMDctMDEuY3J0MAwGA1UdEwEB # /wQCMAAwDQYJKoZIhvcNAQELBQADggEBAJYdTCu6GLf0F8qu4JuKidCt6hweTHFz # 012VGqDoVNN8REwov3VMjK71y8oL6wgvx29RYYqD2sKn6a/NcKUlHJjttvbXW/Az # NK4FetsfpyURFCRTS8C5hRcGZTIZfiSsJXn0N/yV/pbf/M6N4c0Q//I5f+e5lMch # 0jf6TGVLEHcXgOOH1PcS4Rd9LjAaggJG7VAOrIQaoSfgtsMn/a0CoYXeigizHb4k # sZW2nEC5JSAZ49b3Y1Pjvtr1H6xfMewXwtGCEvTq2btl8in/TV8du5cimL7VmZAa # aggJr0eFOmLCNUgGhH+Ic+sLH7G7vpkdggW9PRQ0wtQm8ofUIYhIn2swggZ8MIIE # ZKADAgECAgphEYRvAAAAAAADMA0GCSqGSIb3DQEBCwUAMIGQMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTowOAYDVQQDEzFNaWNyb3NvZnQgVGVz # dGluZyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDEwMB4XDTEwMDcwMTIx # MjMwMVoXDTI1MDcwMTIxMzMwMVoweTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh # c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD # b3Jwb3JhdGlvbjEjMCEGA1UEAxMaTWljcm9zb2Z0IFRlc3RpbmcgUENBIDIwMTAw # ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBvSHVS2YGAJIwORjKy3NC # WbHvmyeo4OhVvSmw+SQfOtHow1mJ7ZG2wegzY/ZaZBniLnwMkIAFOL8cproNai/v # J5er3vbvUPOD59fDRTciPxi1wpYRto0Sg1mLJ1EGVnW5YGoTDtUmPy2WqgXMoYc/ # vk807wxMb8wE1KHmZ80KJzOf46+bb2h8vLQMczSMWoH5h/tUHMVHbOqfV7RZ/c4Z # qXd8h0KftXmUvMt2ktuWl6FfBCQ5/qGV4Z+G417ZXFbfQ5CfyRTq0fWgW6vzCATd # KK8b4qouE6AK7dKZRCr1mUT7K6RP8bthwh0t9SUnAqh475M59F51ge7S4HYMWyPv # AgMBAAGjggHsMIIB6DAQBgkrBgEEAYI3FQEEAwIBADAdBgNVHQ4EFgQU3dZHiyMU # L9g4aEKjPm3D8JD3LNAwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0P # BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUowEEfjCIM+u5MZzK # 64V2Z/xltNEwWQYDVR0fBFIwUDBOoEygSoZIaHR0cDovL2NybC5taWNyb3NvZnQu # Y29tL3BraS9jcmwvcHJvZHVjdHMvTWljVGVzUm9vQ2VyQXV0XzIwMTAtMDYtMTcu # Y3JsMF0GCCsGAQUFBwEBBFEwTzBNBggrBgEFBQcwAoZBaHR0cDovL3d3dy5taWNy # b3NvZnQuY29tL3BraS9jZXJ0cy9NaWNUZXNSb29DZXJBdXRfMjAxMC0wNi0xNy5j # cnQwgaAGA1UdIAEB/wSBlTCBkjCBjwYJKwYBBAGCNy4DMIGBMD0GCCsGAQUFBwIB # FjFodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vUEtJL2RvY3MvQ1BTL2RlZmF1bHQu # aHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAFAAbwBsAGkAYwB5AF8A # UwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBAYQU+N90z # e1LCSGuA92ADFrbJLol+vdlYHGPT9ZLS9loEDQjuh7/rmDQ6ZXxQ5MgbKVB5VXsY # OZG1QSbzF3+YlWd8TN1y5B21BM3DSPb6r+3brl50kW2t1JqACtiLbZnmhmh5hkdJ # i8HYUfLQ7xKcP0g1CIJP9CyKil7UJv/HnMXKigTGiBaHjfVtVwG5k8roymrEirpB # DcOMVB0OZiTXxYIHDbM4v7LItZYIISdPs6+LwxwzwdroMdpj42+3dWQBumpRGQAg # qJ9i5UiBQtUM+9vLpKIRnujnWfQxbaIuIt2HRLFpHUYKGOXRlf148o+71dX3YWap # 88+ocaxkM8rkavgDNkcWSe9Dpoq8a3tS2P9BpxewDV+iSzF0JRo9UOZeciaSQDZv # rkQskxJjtdO725L6E5Fu1Ti+lGl6exRCnhPbooxCqHEGLRdiwXkrmLp+huTGAK8z # mfEt0d1JFrrDdu5kqoG3OVT2dN4JVFNpOFvCU/LNiVDCyCIcG0cSRVtDjyNckMhu # 1PcPtberjr1mcL8RkTzvonoH4pIvQk1k4IOLpdxslOj2oigApZjqCBJA3mIEZHln # wRuglg4Er74nSmL6953C0r1Vwl7T0vXnQO8izb+incAb1r6Y+45N5aVXww+PqHJB # RjvhjyBKG+1aDLVM3ixjV9P6OZkOvp4uozGCFV8wghVbAgEBMIGQMHkxCzAJBgNV # BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w # HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xIzAhBgNVBAMTGk1pY3Jvc29m # dCBUZXN0aW5nIFBDQSAyMDEwAhMzAAAD53EWvSG3L5ZCAAAAAAPnMA0GCWCGSAFl # AwQCAQUAoIGuMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcC # AQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCA8qbJeuNkcIY2qPdlE # iMMLS2dpq8vZRvpvbSQFLuSVBzBCBgorBgEEAYI3AgEMMTQwMqAUgBIATQBpAGMA # cgBvAHMAbwBmAHShGoAYaHR0cDovL3d3dy5taWNyb3NvZnQuY29tMA0GCSqGSIb3 # DQEBAQUABIIBAHkvl07W04uHaMsfzoH6cZd1fuEyCZhE6qPe/vxf/k2+3Oflw5s3 # RkcIDv08dgijRhLfk96/lGKFdzfiW3tnBjQ6zUFBKpvUviJWlGKKETs//xTQGXTM # LHaksvUHr4SdYQgYQOCqvR7ZCjrad0XZYTiWO7PRZGHKWDd+Y2FrIHTZdcXyDw+5 # xKmbV6++6MyNzdIDQJAcwdj8Md+TKEg83b9obhTiMURkEX1a58naGLGiL4rVTJ+n # xNpVLC9sEpGeaofo9InH0AUFVp5YnsxGU65mln532N7mb+JjrKyTa6QlXwydrynD # apxYP3elHxyxqNlJTeC3t7jIJL7L0MjBZA2hghLuMIIS6gYKKwYBBAGCNwMDATGC # EtowghLWBgkqhkiG9w0BBwKgghLHMIISwwIBAzEPMA0GCWCGSAFlAwQCAQUAMIIB # VQYLKoZIhvcNAQkQAQSgggFEBIIBQDCCATwCAQEGCisGAQQBhFkKAwEwMTANBglg # hkgBZQMEAgEFAAQgE104KGXoHXrseXYl8sIsiLDGuxsFf5c1eswljawNALoCBl+7 # 5NY7ZhgTMjAyMDExMjcxMTQ3NTMuMTE5WjAEgAIB9KCB1KSB0TCBzjELMAkGA1UE # BhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAc # BgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEpMCcGA1UECxMgTWljcm9zb2Z0 # IE9wZXJhdGlvbnMgUHVlcnRvIFJpY28xJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNO # OkY3N0YtRTM1Ni01QkFFMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBT # ZXJ2aWNloIIOQTCCBPUwggPdoAMCAQICEzMAAAEq6BeW+Ian76MAAAAAASowDQYJ # KoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x # EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv # bjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcNMTkx # MjE5MDExNTAyWhcNMjEwMzE3MDExNTAyWjCBzjELMAkGA1UEBhMCVVMxEzARBgNV # BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv # c29mdCBDb3Jwb3JhdGlvbjEpMCcGA1UECxMgTWljcm9zb2Z0IE9wZXJhdGlvbnMg # UHVlcnRvIFJpY28xJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOkY3N0YtRTM1Ni01 # QkFFMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIIBIjAN # BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn9+VgaSF0m3FKwcG72WZcX9RfE8X # svjmcSGa13TUoOixZtjzLngE3v6T0My/OpOg/f2/z9n420TMqPwF/kRCgbX+kl+n # MIl7zQdmrKoyjShD0S6BVjpg1U1rZPW7nV33qrWEWa7V2DG3y4PaDsikFB2FLa2l # zePccTMq9X+/ASvv8FxO7CpQequsGAdz3vV6lVHijls0qyOKRrCYzD0P+3KtNyLL # cX0ar2kSCTwSol850BpuRqe4BZOOWYGFm1GI71bWoWnCe70bmpW900pErFB23EwL # TilYZ+fHMNpzv6MiqXnfYgQLlBKe9jzizMSnHDfVBb8tp9KIOYC1hYembwIDAQAB # o4IBGzCCARcwHQYDVR0OBBYEFHD0xS10Kz+uE3bL0SQTpkj07xNpMB8GA1UdIwQY # MBaAFNVjOlyKMZDzQ3t8RhvFM2hahW1VMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6 # Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1RpbVN0YVBD # QV8yMDEwLTA3LTAxLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0 # dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljVGltU3RhUENBXzIw # MTAtMDctMDEuY3J0MAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwgw # DQYJKoZIhvcNAQELBQADggEBAIrANPQKcdWjjo5bJRus8iPxAhx/49OMFVikqDUr # YPXlnrES6+Z/6Kzo3yCP1/WeQUgAu+H6IaTHwaAZr+gD0iFc0QVg80VofAdqf9QT # DU/pON1qrLdy8sLx/zMTUJHUuFc2h+rrF+hP0csYVKD2yQ8szVND5EBBf0yKASbw # UWWGGxDWIYHXf33Hx33aH0qymoYOc73pn0CPs5sO11TpGhmuxmSJFA2deadfUj5G # 7C0u7ww3xeEktKXnCqoczeuppoy9IAhJW0rJKnMkLlmH7mQmWoV1KIgdbxD7xHoR # Ybwgtv09/7D8/J3IrdlORVdSkUD4mFaNzLOmFUbD19+PRgowggZxMIIEWaADAgEC # AgphCYEqAAAAAAACMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEG # A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj # cm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0 # aWZpY2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0xMDA3MDEyMTM2NTVaFw0yNTA3MDEy # MTQ2NTVaMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYD # VQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAk # BgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMIIBIjANBgkqhkiG # 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqR0NvHcRijog7PwTl/X6f2mUa3RUENWlCgCC # hfvtfGhLLF/Fw+Vhwna3PmYrW/AVUycEMR9BGxqVHc4JE458YTBZsTBED/FgiIRU # QwzXTbg4CLNC3ZOs1nMwVyaCo0UN0Or1R4HNvyRgMlhgRvJYR4YyhB50YWeRX4FU # sc+TTJLBxKZd0WETbijGGvmGgLvfYfxGwScdJGcSchohiq9LZIlQYrFd/XcfPfBX # day9ikJNQFHRD5wGPmd/9WbAA5ZEfu/QS/1u5ZrKsajyeioKMfDaTgaRtogINeh4 # HLDpmc085y9Euqf03GS9pAHBIAmTeM38vMDJRF1eFpwBBU8iTQIDAQABo4IB5jCC # AeIwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFNVjOlyKMZDzQ3t8RhvFM2ha # hW1VMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAPBgNV # HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2VsuP6KJcYmjRPZSQW9fOmhjEMFYG # A1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3Js # L3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNybDBaBggrBgEFBQcB # AQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kv # Y2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3J0MIGgBgNVHSABAf8EgZUw # gZIwgY8GCSsGAQQBgjcuAzCBgTA9BggrBgEFBQcCARYxaHR0cDovL3d3dy5taWNy # b3NvZnQuY29tL1BLSS9kb2NzL0NQUy9kZWZhdWx0Lmh0bTBABggrBgEFBQcCAjA0 # HjIgHQBMAGUAZwBhAGwAXwBQAG8AbABpAGMAeQBfAFMAdABhAHQAZQBtAGUAbgB0 # AC4gHTANBgkqhkiG9w0BAQsFAAOCAgEAB+aIUQ3ixuCYP4FxAz2do6Ehb7Prpsz1 # Mb7PBeKp/vpXbRkws8LFZslq3/Xn8Hi9x6ieJeP5vO1rVFcIK1GCRBL7uVOMzPRg # Eop2zEBAQZvcXBf/XPleFzWYJFZLdO9CEMivv3/Gf/I3fVo/HPKZeUqRUgCvOA8X # 9S95gWXZqbVr5MfO9sp6AG9LMEQkIjzP7QOllo9ZKby2/QThcJ8ySif9Va8v/rbl # jjO7Yl+a21dA6fHOmWaQjP9qYn/dxUoLkSbiOewZSnFjnXshbcOco6I8+n99lmqQ # eKZt0uGc+R38ONiU9MalCpaGpL2eGq4EQoO4tYCbIjggtSXlZOz39L9+Y1klD3ou # OVd2onGqBooPiRa6YacRy5rYDkeagMXQzafQ732D8OE7cQnfXXSYIghh2rBQHm+9 # 8eEA3+cxB6STOvdlR3jo+KhIq/fecn5ha293qYHLpwmsObvsxsvYgrRyzR30uIUB # HoD7G4kqVDmyW9rIDVWZeodzOwjmmC3qjeAzLhIp9cAvVCch98isTtoouLGp25ay # p0Kiyc8ZQU3ghvkqmqMRZjDTu3QyS99je/WZii8bxyGvWbWu3EQ8l1Bx16HSxVXj # ad5XwdHeMMD9zOZN+w2/XU/pnR4ZOC+8z1gFLu8NoFA12u8JJxzVs341Hgi62jbb # 01+P3nSISRKhggLPMIICOAIBATCB/KGB1KSB0TCBzjELMAkGA1UEBhMCVVMxEzAR # BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p # Y3Jvc29mdCBDb3Jwb3JhdGlvbjEpMCcGA1UECxMgTWljcm9zb2Z0IE9wZXJhdGlv # bnMgUHVlcnRvIFJpY28xJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOkY3N0YtRTM1 # Ni01QkFFMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMK # AQEwBwYFKw4DAhoDFQDqsuasofIgw/vp4+XfbXEpQndhf6CBgzCBgKR+MHwxCzAJ # BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k # MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jv # c29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMA0GCSqGSIb3DQEBBQUAAgUA42sAfDAi # GA8yMDIwMTEyNzA4MzUwOFoYDzIwMjAxMTI4MDgzNTA4WjB0MDoGCisGAQQBhFkK # BAExLDAqMAoCBQDjawB8AgEAMAcCAQACAhR+MAcCAQACAhHoMAoCBQDjbFH8AgEA # MDYGCisGAQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAI # AgEAAgMBhqAwDQYJKoZIhvcNAQEFBQADgYEAtQKBmSy9OcmmbJUs/kePRcWv4b7O # HNKP2Rk9wyongT7m1OXXbMHuqImaPDlI2JYUkwtPF7pDCNavmMw/UTvwlv/maHwk # FKhVOnSKENOwhNaTGbKXk8MiLK/exlrdubg8VLy4ihOcdrLzstxI9QLny6E2C1lz # NekQ1fs8noQQTyYxggMNMIIDCQIBATCBkzB8MQswCQYDVQQGEwJVUzETMBEGA1UE # CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z # b2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQ # Q0EgMjAxMAITMwAAASroF5b4hqfvowAAAAABKjANBglghkgBZQMEAgEFAKCCAUow # GgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCA3Ywum # HvX83jea7pT3fvfocXCojdoPbulo/2jZShtPjjCB+gYLKoZIhvcNAQkQAi8xgeow # gecwgeQwgb0EIEOYNYRa9zp+Gzm3haijlD4UwUJxoiBXjJQ/gKm4GYuZMIGYMIGA # pH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT # B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE # AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAEq6BeW+Ian76MA # AAAAASowIgQg4ONNea/rtNu5voJgRMjbJlWNEVaWQT5SBEgbKRukwkAwDQYJKoZI # hvcNAQELBQAEggEAhor/QcHV+cIX5/ezT8MJnVVqLhPjZjPNr3Iu8hQjRY6b2RJE # fiVAMJcxFdOLHd7xk/zfs043Ud5ntKnlWbI//e32f4falihdH4iAuqqBRXZQcUXr # oHzGcv2fRtOzofZb9jZO5P2T1OBQp8s69YKVOtydMtpAyVTOXjAfy2AytRGAQns1 # kZGzJhMZqWy+q4PI+SYActMqM4Lm6yuOrI0ycsgn17ZK0m/cVqZWTrfeQSjcJv4q # 2LJHiWWgYRVgtLvJGhyLDRV9CICruIyQ6ZPm0DhhTmNiwF/zj1BbmYcwQB26FnBL # lHvTF2UdzFff9k8+F2RUiWjqIvt1nwfTOOd6tQ== # SIG # End signature block |