Framework/Core/SVT/SVTResourceResolver.ps1
Set-StrictMode -Version Latest class SVTResourceResolver: AzSKRoot { [string[]] $ResourceNames = @(); [string] $ResourceType = ""; [ResourceTypeName] $ResourceTypeName = [ResourceTypeName]::All; [Hashtable] $Tag = $null; [string] $TagName = ""; [string[]] $TagValue = ""; hidden [string[]] $ResourceGroups = @(); [ResourceTypeName] $ExcludeResourceTypeName = [ResourceTypeName]::All; [string[]] $ExcludeResourceNames = @(); [System.Collections.Generic.List[SVTResource]] $ExcludedResources = @(); [int] $MaxObjectsToScan; [System.Collections.Generic.List[SVTResource]] $SVTResources = @(); [int] $SVTResourcesFoundCount = 0; [bool] $IsAIEnabled = $false; [bool] $IsAutomatedFixUndoCmd = $false; [string] $ResourcePath; [string] $BuildsFolderPath; [string] $ReleasesFolderPath; [bool] $BatchScan; [string] $organizationName hidden [string[]] $ProjectNames = @(); hidden [string[]] $BuildNames = @(); hidden [string[]] $ReleaseNames = @(); hidden [string[]] $AgentPools = @(); hidden [string[]] $ServiceConnections = @(); hidden [string[]] $VariableGroups = @(); hidden [PSObject] $ControlSettings; #Local variable for longrunningscan for command parameter [bool] $allowLongRunningScan = $false #Local variables for longrunningscan for controlsettings variables [bool] $isAllowLongRunningScanInPolicy = $true [int] $longRunningScanCheckPoint = 1000; hidden [string[]] $serviceIds = @(); [bool] $includeAdminControls = $false; [bool] $isUserPCA = $false; [bool] $skipOrgUserControls = $false [bool] $baselineConfigurationForce = $false; [bool] $UseIncrementalScan = $false [DateTime] $IncrementalDate = 0 [bool] $UsePartialCommits=$false; [bool] $DoNotRefetchResources=$false; [bool] $isPartialScanActive=$false; [bool] $isDnrrAllProject = $false; [bool] $shouldFetchResource = $true; [PSObject] $nonScannedResources; hidden [string[]] $BuildIds = @(); hidden [string[]] $ReleaseIds = @(); hidden [string[]] $AgentPoolIds = @(); hidden [string[]] $ServiceConnectionIds = @(); hidden [string[]] $VariableGroupIds = @(); hidden [bool] $isServiceIdBasedScan = $false; #Common svt resources hidden [string[]] $RepoNames = @(); hidden [string[]] $SecureFileNames = @(); hidden [string[]] $FeedNames = @(); hidden [string[]] $EnvironmentNames = @(); SVTResourceResolver([string]$organizationName, $ProjectNames, $BuildNames, $ReleaseNames, $AgentPools, $ServiceConnectionNames, $VariableGroupNames, $MaxObj, $ScanAllResources, $PATToken, $ResourceTypeName, $AllowLongRunningScan, $ServiceIds, $IncludeAdminControls, $skipOrgUserControls, $RepoNames, $SecureFileNames, $FeedNames, $EnvironmentNames,$BuildsFolderPath,$ReleasesFolderPath,$UsePartialCommits,$DoNotRefetchResources,$BatchScan, $UseIncrementalScan, $IncrementalDate): Base($organizationName, $PATToken) { $this.MaxObjectsToScan = $MaxObj #default = 0 => scan all if "*" specified... $this.SetallTheParamValues($organizationName, $ProjectNames, $BuildNames, $ReleaseNames, $AgentPools, $ServiceConnectionNames, $VariableGroupNames, $ScanAllResources, $PATToken, $ResourceTypeName, $AllowLongRunningScan, $ServiceIds, $IncludeAdminControls, $BuildsFolderPath,$ReleasesFolderPath,$UsePartialCommits,$DoNotRefetchResources,$BatchScan, $UseIncrementalScan, $IncrementalDate); $this.skipOrgUserControls = $skipOrgUserControls $this.RepoNames += $this.ConvertToStringArray($RepoNames); $this.SecureFileNames += $this.ConvertToStringArray($SecureFileNames); $this.FeedNames += $this.ConvertToStringArray($FeedNames); [PartialScanManager]::ClearInstance(); $this.EnvironmentNames += $this.ConvertToStringArray($EnvironmentNames); } #Constructor for Set-AzSKADOSecurityStatus SVTResourceResolver([string]$organizationName, $ProjectNames, $ResourceNames, $ExcludeResourceNames, $PATToken, $ResourceTypeName, $UndoFix, $ControlId): Base($organizationName, $PATToken) { if($UndoFix) { if (!$this.ControlSettings) { $this.ControlSettings = [ConfigurationManager]::LoadServerConfigFile("ControlSettings.json"); } if($this.ControlSettings.AutomatedFix.RevertDeletedResourcesControlList -contains $ControlId) { #When controls undo fix is called, resources need to be fetched from deleted list (only for controls ids in RevertDeletedResourcesControlList) $this.IsAutomatedFixUndoCmd = $true } } $this.organizationName = $organizationName $this.ProjectNames = $ProjectNames $this.ResourceTypeName = $ResourceTypeName if (-not [string]::IsNullOrEmpty($ResourceNames)) { $this.ResourceNames += $this.ConvertToStringArray($ResourceNames); } if (-not [string]::IsNullOrEmpty($ExcludeResourceNames)) { $this.ExcludeResourceNames += $this.ConvertToStringArray($ExcludeResourceNames); } $this.SetallTheParamValues($ResourceTypeName) } #Constructor for Set-AzSKADOBaselineConfigurations SVTResourceResolver($OrganizationName, $ProjectNames, $BuildNames, $ReleaseNames, $ServiceConnectionNames,$RepoNames, $SecureFileNames, $FeedNames, $EnvironmentNames,$AgentPoolNames, $VariableGroupNames, $ResourceTypeName, $PATToken, $Force, $ScanAllResources, $IsSabc): Base($OrganizationName, $PATToken){ $this.organizationName = $organizationName $this.ProjectNames = $ProjectNames $this.ResourceTypeName = $ResourceTypeName $this.SetallTheParamValues($this.ProjectNames,$BuildNames, $ReleaseNames, $ServiceConnectionNames,$RepoNames, $SecureFileNames, $FeedNames, $EnvironmentNames,$AgentPoolNames, $VariableGroupNames, $ResourceTypeName, $Force, $ScanAllResources) } [void] SetallTheParamValues([string]$organizationName, $ProjectNames, $BuildNames, $ReleaseNames, $AgentPools, $ServiceConnectionNames, $VariableGroupNames, $ScanAllResources, $PATToken, $ResourceTypeName, $AllowLongRunningScan, $ServiceIds, $IncludeAdminControls,$BuildsFolderPath,$ReleasesFolderPath,$UsePartialCommits,$DoNotRefetchResources,$BatchScan, $UseIncrementalScan, $IncrementalDate) { $this.organizationName = $organizationName $this.ResourceTypeName = $ResourceTypeName $this.allowLongRunningScan = $AllowLongRunningScan $this.includeAdminControls = $IncludeAdminControls $this.BuildsFolderPath = $BuildsFolderPath.Trim() $this.UsePartialCommits=$UsePartialCommits $this.DoNotRefetchResources=$DoNotRefetchResources $this.ReleasesFolderPath = $ReleasesFolderPath.Trim() $this.UseIncrementalScan = $UseIncrementalScan if (-not [string]::IsNullOrWhiteSpace($IncrementalDate)) { $this.IncrementalDate = $IncrementalDate } else { $this.IncrementalDate = [datetime] 0 } $this.BatchScan = $BatchScan if (-not [string]::IsNullOrEmpty($ProjectNames)) { $this.ProjectNames += $this.ConvertToStringArray($ProjectNames); if ($this.ProjectNames.Count -eq 0) { throw [SuppressedException] "The parameter 'ProjectNames' does not contain any string." } } elseif ($ResourceTypeName -eq [ResourceTypeName]::Project -or $ResourceTypeName -eq [ResourceTypeName]::Org_Project_User) { $this.ProjectNames = "*" } if (-not [string]::IsNullOrEmpty($BuildNames)) { $this.BuildNames += $this.ConvertToStringArray($BuildNames); if ($this.BuildNames.Count -eq 0) { throw [SuppressedException] "The parameter 'BuildNames' does not contain any string." } } elseif ($ResourceTypeName -eq [ResourceTypeName]::Build -or $ResourceTypeName -eq [ResourceTypeName]::Build_Release) { $this.BuildNames = "*" } if (-not [string]::IsNullOrEmpty($ReleaseNames)) { $this.ReleaseNames += $this.ConvertToStringArray($ReleaseNames); if ($this.ReleaseNames.Count -eq 0) { throw [SuppressedException] "The parameter 'ReleaseNames' does not contain any string." } } elseif ($ResourceTypeName -eq [ResourceTypeName]::Release -or $ResourceTypeName -eq [ResourceTypeName]::Build_Release) { $this.ReleaseNames = "*" } if (-not [string]::IsNullOrEmpty($ServiceConnectionNames)) { $this.ServiceConnections += $this.ConvertToStringArray($ServiceConnectionNames); if ($this.ServiceConnections.Count -eq 0) { throw [SuppressedException] "The parameter 'ServiceConnectionNames' does not contain any string." } } elseif ($ResourceTypeName -eq [ResourceTypeName]::ServiceConnection -or $ResourceTypeName -eq [ResourceTypeName]::SvcConn_AgentPool_VarGroup_CommonSVTResources) { $this.ServiceConnections = "*" } if (-not [string]::IsNullOrEmpty($AgentPools)) { $this.AgentPools += $this.ConvertToStringArray($AgentPools); if ($this.AgentPools.Count -eq 0) { throw [SuppressedException] "The parameter 'AgentPools' does not contain any string." } } elseif ($ResourceTypeName -eq [ResourceTypeName]::AgentPool -or $ResourceTypeName -eq [ResourceTypeName]::SvcConn_AgentPool_VarGroup_CommonSVTResources) { $this.AgentPools = "*" } if (-not [string]::IsNullOrEmpty($VariableGroupNames)) { $this.VariableGroups += $this.ConvertToStringArray($VariableGroupNames); if ($this.VariableGroups.Count -eq 0) { throw [SuppressedException] "The parameter 'VariableGroupNames' does not contain any string." } } elseif ($ResourceTypeName -eq [ResourceTypeName]::VariableGroup -or $ResourceTypeName -eq [ResourceTypeName]::SvcConn_AgentPool_VarGroup_CommonSVTResources) { $this.VariableGroups = "*" } if (-not [string]::IsNullOrEmpty($ServiceIds)) { $this.serviceIds += $this.ConvertToStringArray($ServiceIds); if ($this.serviceIds.Count -eq 0) { throw [SuppressedException] "The parameter 'ServiceId' does not contain any string." } } #User should always provide project name (comma separated list or '*') to scan builds in an org. Else no controls will be scanned if -rtn is 'Build' #if (-not [string]::IsNullOrEmpty($ResourceTypeName) -and $ResourceTypeName -ne "All" -and ([string]::IsNullOrEmpty($ProjectNames))) { # $this.ProjectNames = "*" #} if ($ScanAllResources -and [string]::IsNullOrEmpty($ServiceIds)) { #ScanAllResources should scan all artifacts within the targeted projects (if provided explicitly) if ([string]::IsNullOrEmpty($ProjectNames)) { $this.ProjectNames = "*" } $this.BuildNames = "*" $this.ReleaseNames = "*" $this.AgentPools = "*" $this.ServiceConnections = "*" $this.VariableGroups = "*" $this.RepoNames = "*" $this.SecureFileNames = "*" $this.FeedNames = "*" $this.EnvironmentNames = "*" } if (( $this.MaxObjectsToScan -eq 0 -or $this.MaxObjectsToScan -gt $this.longRunningScanCheckPoint) -and ($this.ProjectNames -eq "*" -or $this.BuildNames -eq "*" -or $this.ReleaseNames -eq "*" -or $this.ServiceConnections -eq "*" -or $this.AgentPools -eq "*" -or $this.VariableGroups -eq "*")) { $this.PublishCustomMessage("Using '*' can take a long time for the scan to complete in larger projects. `nYou may want to provide a comma-separated list of projects, builds, releases, service connections, agent pools and variable groups. `n ", [MessageType]::Warning); <# BUGBUG: [Aug-2020] Removing this until we can determine the right approach to init org-policy-url for ADO. if (!$this.ControlSettings) { $this.ControlSettings = [ConfigurationManager]::LoadServerConfigFile("ControlSettings.json"); } #fetch control settings to check whether large scans are allowed in the org $this.isAllowLongRunningScanInPolicy = $this.ControlSettings.IsAllowLongRunningScan; $this.longRunningScanCheckPoint = $this.ControlSettings.LongRunningScanCheckPoint; #> } } #Called for Set-AzSKADOBaselineConfigurations [void] SetallTheParamValues($ProjectNames,$BuildNames,$ReleaseNames, $ServiceConnectionNames,$RepoNames, $SecureFileNames, $FeedNames, $EnvironmentNames,$AgentPoolNames, $VariableGroupNames, $ResourceTypeName,$Force, $ScanAllResources){ $this.baselineConfigurationForce = $Force if ($ResourceTypeName -eq [ResourceTypeName]::Project ){ if (-not [string]::IsNullOrEmpty($ProjectNames)) { $this.ProjectNames += $this.ConvertToStringArray($ProjectNames); if ($this.ProjectNames.Count -eq 0) { throw [SuppressedException] "The parameter 'ProjectNames' does not contain any string." } } } if (-not [string]::IsNullOrEmpty($BuildNames)) { $this.BuildNames += $this.ConvertToStringArray($BuildNames); if ($this.BuildNames.Count -eq 0) { throw [SuppressedException] "The parameter 'BuildNames' does not contain any string." } } elseif ($ResourceTypeName -eq [ResourceTypeName]::Build -or $ResourceTypeName -eq [ResourceTypeName]::Build_Release ){ $this.BuildNames = "*" } if (-not [string]::IsNullOrEmpty($ReleaseNames)) { $this.ReleaseNames += $this.ConvertToStringArray($ReleaseNames); if ($this.ReleaseNames.Count -eq 0) { throw [SuppressedException] "The parameter 'ReleaseNames' does not contain any string." } } elseif ($ResourceTypeName -eq [ResourceTypeName]::Release -or $ResourceTypeName -eq [ResourceTypeName]::Build_Release ){ $this.ReleaseNames = "*" } if (-not [string]::IsNullOrEmpty($ServiceConnectionNames)) { $this.ServiceConnections += $this.ConvertToStringArray($ServiceConnectionNames); if ($this.ServiceConnections.Count -eq 0) { throw [SuppressedException] "The parameter 'ServiceConnectionNames' does not contain any string." } } elseif ($ResourceTypeName -eq [ResourceTypeName]::ServiceConnection -or $ResourceTypeName -eq [ResourceTypeName]::SvcConn_AgentPool_VarGroup_CommonSVTResources ){ $this.ServiceConnections = "*" } if (-not [string]::IsNullOrEmpty($AgentPoolNames)) { $this.AgentPools += $this.ConvertToStringArray($AgentPoolNames); if ($this.AgentPools.Count -eq 0) { throw [SuppressedException] "The parameter 'AgentPools' does not contain any string." } } elseif ($ResourceTypeName -eq [ResourceTypeName]::AgentPool -or $ResourceTypeName -eq [ResourceTypeName]::SvcConn_AgentPool_VarGroup_CommonSVTResources ){ $this.AgentPools = "*" } if (-not [string]::IsNullOrEmpty($VariableGroupNames)) { $this.VariableGroups += $this.ConvertToStringArray($VariableGroupNames); if ($this.VariableGroups.Count -eq 0) { throw [SuppressedException] "The parameter 'VariableGroupNames' does not contain any string." } } elseif ($ResourceTypeName -eq [ResourceTypeName]::VariableGroup -or $ResourceTypeName -eq [ResourceTypeName]::SvcConn_AgentPool_VarGroup_CommonSVTResources ){ $this.VariableGroups = "*" } if ($ScanAllResources){ $this.BuildNames = "*" $this.ReleaseNames = "*" $this.AgentPools = "*" $this.ServiceConnections = "*" $this.VariableGroups = "*" $this.RepoNames = "*" $this.SecureFileNames = "*" $this.FeedNames = "*" $this.EnvironmentNames = "*" } } # Method called for Set-AzSKADOSecurityStatus, invoked from constructor [void] SetallTheParamValues($ResourceTypeName) { if ($ResourceTypeName -eq [ResourceTypeName]::Build ) { if([string]::IsNullOrWhitespace($this.ResourceNames)) { $this.BuildNames = "*" } else{ $this.BuildNames = $this.ResourceNames } } elseif ($ResourceTypeName -eq [ResourceTypeName]::Release) { if([string]::IsNullOrWhitespace($this.ResourceNames)) { $this.ReleaseNames = "*" } else{ $this.ReleaseNames = $this.ResourceNames } } elseif ($ResourceTypeName -eq [ResourceTypeName]::ServiceConnection) { if([string]::IsNullOrWhitespace($this.ResourceNames)) { $this.ServiceConnections = "*" } else{ $this.ServiceConnections = $this.ResourceNames } } elseif ($ResourceTypeName -eq [ResourceTypeName]::AgentPool) { if([string]::IsNullOrWhitespace($this.ResourceNames)) { $this.AgentPools = "*" } else{ $this.AgentPools = $this.ResourceNames } } elseif ($ResourceTypeName -eq [ResourceTypeName]::VariableGroup) { if([string]::IsNullOrWhitespace($this.ResourceNames)) { $this.VariableGroups = "*" } else{ $this.VariableGroups = $this.ResourceNames } } elseif ($ResourceTypeName -eq [ResourceTypeName]::Repository) { if([string]::IsNullOrWhitespace($this.ResourceNames)) { $this.RepoNames = "*" } else{ $this.RepoNames = $this.ResourceNames } } elseif ($ResourceTypeName -eq [ResourceTypeName]::SecureFile) { if([string]::IsNullOrWhitespace($this.ResourceNames)) { $this.SecureFileNames = "*" } else{ $this.SecureFileNames = $this.ResourceNames } } elseif ($ResourceTypeName -eq [ResourceTypeName]::Feed) { if([string]::IsNullOrWhitespace($this.ResourceNames)) { $this.FeedNames = "*" } else{ $this.FeedNames = $this.ResourceNames } } elseif ($ResourceTypeName -eq [ResourceTypeName]::Environment) { if([string]::IsNullOrWhitespace($this.ResourceNames)) { $this.EnvironmentNames = "*" } else{ $this.EnvironmentNames = $this.ResourceNames } } } [void] LoadResourcesForScan() { #Call APIS for Organization,User/Builds/Releases/ServiceConnections $organizationId = ""; if ([RemoteReportHelper]::IsAIOrgTelemetryEnabled()) { $this.IsAIEnabled = $true; } #Checking if org name is correct try { if (-not [string]::IsNullOrWhiteSpace($env:RefreshToken) -and -not [string]::IsNullOrWhiteSpace($env:ClientSecret)) { $apiURL = "https://app.vssps.visualstudio.com/_apis/accounts" $responseObj = ""; $responseObj = [WebRequestHelper]::InvokeGetWebRequest($apiURL) ; if (-not [string]::IsNullOrEmpty($responseObj) -and ($responseObj | Measure-Object).Count -gt 0) { $organizationId = ($responseObj | Where-Object {$_.accountname -eq $this.organizationname}).AccountId } Remove-Variable responseObj; } else { $apiURL = "https://dev.azure.com/{0}/_apis/connectionData" -f $this.organizationname $user = "" $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user, [ContextHelper]::currentContext.AccessToken))) $headers = @{ "Authorization"= ("Basic " + $base64AuthInfo); "Content-Type"="application/json" }; $responseObj = Invoke-RestMethod -Method Get -Uri $apiURL -Headers $headers -UseBasicParsing $organizationId = $responseObj.instanceId; Remove-Variable responseObj; } } catch { $user = [ContextHelper]::GetCurrentSessionUser(); $this.PublishCustomMessage("Organization not found: Incorrect organization name or '$($user)' account does not have necessary permission to access the organization. Use -ResetCredentials parameter in command to login with another account. `n", [MessageType]::Warning); throw [SuppressedException] "The remote server returned an error: (404) Not Found." } if ($this.ResourceTypeName -in ([ResourceTypeName]::Organization, [ResourceTypeName]::All, [ResourceTypeName]::Org_Project_User) -and ([string]::IsNullOrEmpty($this.serviceIds)) ) { if($PSCmdlet.MyInvocation.MyCommand.Name -eq "Set-AzSKADOBaselineConfigurations" -and $this.IsResourceEligibleForBaselineConfig([ResourceTypeName]::Organization,$this.organizationName) -eq $false){ $this.PublishCustomMessage([Constants]::BaselineConfigurationErrorMsgOrg, [MessageType]::Warning); return; } #First condition if 'includeAdminControls' switch is passed or user is admin(PCA). #Second condition if explicitly -rtn flag passed to org or Org_Project_User #Third condition if 'gads' contains only admin scan parame, then no need to ask for includeAdminControls switch if (-not $this.skipOrgUserControls) { if (($this.includeAdminControls -or $this.isAdminControlScan())) { #Select Org/User by default... $link = "https://dev.azure.com/$($this.organizationName)/_settings" $this.AddSVTResource($this.organizationName, $null ,"ADO.Organization", "organization/$($organizationId)", $null, $link); } elseif ( ($this.ResourceTypeName -in ([ResourceTypeName]::Organization, [ResourceTypeName]::Org_Project_User)) -or ( $this.BuildNames.Count -eq 0 -and $this.ReleaseNames.Count -eq 0 -and $this.ServiceConnections.Count -eq 0 -and $this.AgentPools.Count -eq 0 -and $this.VariableGroups.Count -eq 0) ) { if($PSCmdlet.MyInvocation.MyCommand.Name -eq "Set-AzSKADOBaselineConfigurations"){ $this.PublishCustomMessage("You have requested baseline configurations for organization controls. However, you do not have admin permissions. Hence, stopping baseline configurations for organization controls.", [MessageType]::Warning); } else{ $this.PublishCustomMessage("You have requested scan for organization controls. However, you do not have admin permission. Use '-IncludeAdminControls' if you'd still like to scan them. (Some controls may not scan correctly due to access issues.)", [MessageType]::Info); $this.PublishCustomMessage("`r`n"); } } } } if (-not $this.skipOrgUserControls) { if ($this.ResourceTypeName -in ([ResourceTypeName]::User, [ResourceTypeName]::All, [ResourceTypeName]::Org_Project_User, [ResourceTypeName]::Build_Release_SvcConn_AgentPool_VarGroup_User_CommonSVTResources)) { $link = "https://dev.azure.com/$($this.organizationName)/_settings/users" $this.AddSVTResource($this.organizationName, $null,"ADO.User", "organization/$($organizationId)/user", $null, $link); } } $topNQueryString = "" if ($this.MaxObjectsToScan -ne 0) { #Add this to QS only if $MaxObj is specified. If so, this will download only $maxObj configs. $topNQueryString = '&$top='+ $this.MaxObjectsToScan } #Get project resources if ($this.ProjectNames.Count -gt 0) { $this.PublishCustomMessage("Querying api for resources to be scanned. This may take a while..."); $this.PublishCustomMessage("Getting project configurations..."); #TODO: By default api return only 100 projects. Added $top=1000 to fetch first 1000 projects. If there are morethan 1000 projects, pagination is implemented to fetch them $apiURL = 'https://dev.azure.com/{0}/_apis/projects?$top=1000&api-version=6.0' -f $($this.OrganizationContext.OrganizationName); $responseObj = ""; try { $responseObj = [WebRequestHelper]::InvokeGetWebRequest($apiURL) ; } catch { Write-Host 'Project not found: Incorrect project name or you do not have necessary permission to access the project.' -ForegroundColor Red throw; } if (([Helpers]::CheckMember($responseObj, "count") -and $responseObj[0].count -gt 0) -or (($responseObj | Measure-Object).Count -gt 0 -and [Helpers]::CheckMember($responseObj[0], "name"))) { if($this.ProjectNames -eq "*") { $projects = $responseObj } else { $projects = $responseObj | Where-Object { $this.ProjectNames -contains $_.name } } $responseObj = $null; Remove-Variable responseObj; $nProj = $this.MaxObjectsToScan; if (!$projects) { Write-Host 'No project found to perform the scan.' -ForegroundColor Red } if($projects -and $PSCmdlet.MyInvocation.MyCommand.Name -eq "Set-AzSKADOBaselineConfigurations" -and $this.ProjectNames -eq "*" -and $this.IsResourceEligibleForBaselineConfig([ResourceTypeName]::Project,$this.ProjectNames) -eq $false){ $this.PublishCustomMessage([Constants]::BaselineConfigurationErrorMsgOrg, [MessageType]::Warning); return; } $TotalSvc = 0; $ScannableSvc = 0; foreach ($thisProj in $projects) { $projectName = $thisProj.name $projectId = $thisProj.id; [Hashtable] $projectData = @{ projectName = $projectName; repositories = -1; testPlan = -1; build = -1; release = -1; taskGroups = -1; agentPools = -1; variableGroups = -1; serviceConnections = -1; }; if($this.UsePartialCommits -and $this.DoNotRefetchResources -and $this.isDnrrAllProject -eq $false){ [PartialScanManager] $partialScanMngr = [PartialScanManager]::GetInstance(); if(($partialScanMngr.IsPartialScanInProgress($this.OrganizationContext.OrganizationName, $false) -eq [ActiveStatus]::Yes) ){ Write-Host "Resuming scan from last commit. Fetching unscanned resources..." -ForegroundColor Yellow $this.nonScannedResources = $partialScanMngr.GetNonScannedResources(); $this.IsPartialScanActive=$true; } else { $this.IsPartialScanActive=$false; } } if($this.IsPartialScanActive -and $this.nonScannedResources.Count -ne 0 -and $this.isDnrrAllProject -eq $false){ #The tracker is loaded in one go, so no need to load it again when we are looping through multiple projects if($null -ne $projects -and ($projects | Measure-Object).Count -gt 1){ $this.isDnrrAllProject = $true; } $progressCount=1 foreach($nonScannedResource in $this.nonScannedResources){ $nonScannedResourceType=$this.FindResourceTypeFromPartialScan($nonScannedResource.Id) $nonScannedresourceLink=$this.CreateResourceLinkFromPartialScan($nonScannedResource.Id,$nonScannedResourceType,$this.organizationName,$nonScannedResource.ProjectName,$projectId) if($nonScannedResourceType -eq "ADO.Release" -or $nonScannedResourceType -eq "ADO.AgentPool"){ $this.AddSVTResource($nonScannedResource.Name,$nonScannedResource.ProjectName,$nonScannedResourceType, $nonScannedResource.Id,$null , $nonScannedresourceLink) } if ($progressCount%100 -eq 0) { Write-Progress -Activity "Fetching $($progressCount) of $($this.nonScannedResources.Count) unscanned resources " -Status "Progress: " -PercentComplete ($progressCount / $this.nonScannedResources.Count * 100) } $progressCount++; } Write-Progress -Activity "All resources fetched" -Status "Ready" -Completed #to be used with release and agent pool to check if we need to call apis for fetching resources or it has been already done via the tracker $this.shouldFetchResource = $false } if ($this.ResourceTypeName -in ([ResourceTypeName]::Project, [ResourceTypeName]::All, [ResourceTypeName]::Org_Project_User) -and ([string]::IsNullOrEmpty($this.serviceIds))) { if($this.ProjectNames -ne "*" -and $PSCmdlet.MyInvocation.MyCommand.Name -eq "Set-AzSKADOBaselineConfigurations" -and $this.IsResourceEligibleForBaselineConfig([ResourceTypeName]::Project,$thisProj.name) -eq $false){ $msg = [Constants]::BaselineConfigurationErrorMsgProj -f $($thisProj.name) $this.PublishCustomMessage($msg, [MessageType]::Warning); continue; } #First condition if 'includeAdminControls' switch is passed or user is PCA or User is PA. #Second condition if explicitly -rtn flag passed to org or Org_Project_User #Adding $this.isAdminControlScan() check in the end in case $this.isUserPCA is not checked (this happens when u scan using -svcid flag and org controls are not resolved/scanned) if ( ($this.includeAdminControls -or $this.isUserPCA -or $this.isUserPA($projectName) -or $this.isAdminControlScan())) { $link = $thisProj.url.Replace('/_apis/projects', '') + '/_settings/' $resourceId = "organization/$organizationId/project/$projectId" $this.AddSVTResource($thisProj.name, $this.organizationName,"ADO.Project", $resourceId, $thisProj, $link); } #Third condition if 'gads' contains only admin scan parame, then no need to ask for includeAdminControls switch elseif ( ($this.ResourceTypeName -in ([ResourceTypeName]::Project, [ResourceTypeName]::Org_Project_User)) -or ( $this.BuildNames.Count -eq 0 -and $this.ReleaseNames.Count -eq 0 -and $this.ServiceConnections.Count -eq 0 -and $this.AgentPools.Count -eq 0 -and $this.VariableGroups.Count -eq 0) ) { if($PSCmdlet.MyInvocation.MyCommand.Name -eq "Set-AzSKADOBaselineConfigurations"){ $this.PublishCustomMessage("You have requested baseline configurations for project controls. However, you do not have admin permissions. Hence, stopping baseline configurations.", [MessageType]::Warning); } else{ $this.PublishCustomMessage("`r`n"); $this.PublishCustomMessage("You have requested scan for project controls. However, you do not have admin permission. Use '-IncludeAdminControls' if you'd still like to scan them. (Some controls may not scan correctly due to access issues.)", [MessageType]::Info); } } } #check if long running scan allowed or not. if(!$this.isAllowLongRunningScanCheck()) { return; } if($this.serviceIds.Count -gt 0) { $inputBuildNames = @() $inputReleaseNames = @() $inputSvcNames = @() $inputAgentPoolNames = @() $inputVargrpNames = @() $inputRepoNames = @() $inputFeedNames = @() $inputEnvNames = @() $inputSecFileNames = @() $inputBuildNames = $this.BuildNames; $this.BuildNames =@(); $inputReleaseNames = $this.ReleaseNames; $this.ReleaseNames =@(); $inputSvcNames = $this.ServiceConnections; $this.ServiceConnections =@(); $inputAgentPoolNames = $this.AgentPools; $this.AgentPools =@(); $inputVargrpNames = $this.VariableGroups; $this.VariableGroups =@(); $inputRepoNames = $this.RepoNames; $this.RepoNames =@(); $inputFeedNames = $this.FeedNames; $this.FeedNames =@(); $inputEnvNames = $this.EnvironmentNames ; $this.EnvironmentNames =@(); $inputSecFileNames = $this.SecureFileNames ; $this.SecureFileNames =@(); $this.PublishCustomMessage("Getting service associated resources..."); foreach ($thisServiceId in $this.serviceIds) { $this.FetchServiceAssociatedResources($thisServiceId, $projectName,$inputBuildNames,$inputReleaseNames,$inputSvcNames,$inputAgentPoolNames,$inputVargrpNames,$inputRepoNames,$inputFeedNames,$inputEnvNames,$inputSecFileNames); } } if ($this.BuildNames.Count -gt 0 -and ($this.ResourceTypeName -in ([ResourceTypeName]::Build, [ResourceTypeName]::All, [ResourceTypeName]::Build_Release, [ResourceTypeName]::Build_Release_SvcConn_AgentPool_VarGroup_User_CommonSVTResources))) { if ($this.ProjectNames -ne "*") { $this.PublishCustomMessage("Getting build configurations..."); } #When Undo fix for build/release inactive controls is called, resources need to be fetched from deleted list if ($this.IsAutomatedFixUndoCmd) { $url = 'https://dev.azure.com/{0}/{1}/_build/deleted?__rt=fps&__ver=2 ' -f $($this.OrganizationContext.OrganizationName), $thisProj.name $responseObj = @([WebRequestHelper]::InvokeGetWebRequest($url)); $buildDefnsObj = @() if([Helpers]::CheckMember($responseObj,"fps.dataProviders.data") -and $responseObj.fps.dataProviders.data.'ms.vss-build-web.deleted-pipelines-data-provider' -and [Helpers]::CheckMember($responseObj.fps.dataProviders.data.'ms.vss-build-web.deleted-pipelines-data-provider',"pipelines") -and $responseObj.fps.dataProviders.data.'ms.vss-build-web.deleted-pipelines-data-provider'.pipelines) { $buildDefnsObj = $responseObj.fps.dataProviders.data."ms.vss-build-web.deleted-pipelines-data-provider".pipelines; } if ($buildDefnsObj.count -gt 0) { foreach ($bldDef in $buildDefnsObj) { $buildResourceId = "organization/$organizationId/project/$projectId/build/$($bldDef.id)"; $this.AddSVTResource($bldDef.name, $thisProj.name, "ADO.Build", $buildResourceId, $bldDef, $null); } $buildDefnsObj = $null; Remove-Variable buildDefnsObj; } } else { if(-not [string]::IsNullOrEmpty($this.BuildsFolderPath)){ # Validate folder path is valid $path = $this.BuildsFolderPath; $this.BuildsFolderPath = $this.BuildsFolderPath.Replace(' ','%20').Replace('\','%5C') $buildFoldersURL = "https://dev.azure.com/{0}/{1}/_apis/build/folders/{2}?api-version=6.1-preview.2" -f $($this.OrganizationContext.OrganizationName), $thisProj.name, $this.BuildsFolderPath $buildFoldersObj = [WebRequestHelper]::InvokeGetWebRequest($buildFoldersURL) if($null -eq $buildFoldersObj -or (![Helpers]::CheckMember($buildFoldersObj[0],"Path"))){ $this.PublishCustomMessage("Folder path not found. Please validate the -BuildsFolderPath provided in the command. `n", [MessageType]::Warning); } else { if($this.BatchScan){ $this.addBuildsToSvtInBatchScan($projectName,$projectId,$path) } else { #Iterate on each folder to get applicale build definition if folders count is le 100 if ([string]::IsNullOrEmpty($topNQueryString)) { $topNQueryString = '&$top=10000' } $nObj=$this.MaxObjectsToScan; if($buildFoldersObj.Count -le 100) { $folderCount=1 foreach($path in $buildFoldersObj.Path) { $formattedPath = $path.Replace(' ','%20').Replace('\','%5C') $buildDefByFolderURL = ('https://dev.azure.com/{0}/{1}/_apis/build/definitions?path={2}&queryOrder=lastModifiedDescending'+$topNQueryString) -f $($this.OrganizationContext.OrganizationName), $thisProj.name, $formattedPath Write-Progress -Activity "Searching in folder $($folderCount) of $($buildFoldersObj.Count) : $($path) " -Status "Progress: " -PercentComplete ($folderCount/ $buildFoldersObj.Count * 100) $this.addResourceToSVT($buildDefByFolderURL,"build",$projectName,$organizationId,$projectId,$true,$false,$null,[ref]$nObj) #if($nObj -eq 0) {break;} $folderCount++; } Write-Progress -Activity "All builds fetched" -Status "Ready" -Completed } else { $buildDefURL = ("https://dev.azure.com/{0}/{1}/_apis/build/definitions?queryOrder=lastModifiedDescending&api-version=6.0" + $topNQueryString) -f $($this.OrganizationContext.OrganizationName), $thisProj.name; $this.addResourceToSVT($buildDefURL,"build",$projectName, $organizationId, $projectId, $true, $true, $path,[ref]$nObj) } } } } elseif ($this.BuildNames -eq "*") { if($this.BatchScan){ $this.addBuildsToSvtInBatchScan($projectName,$projectId,$null); } else { if ([string]::IsNullOrEmpty($topNQueryString)) { $topNQueryString = '&$top=10000' $buildDefnURL = ("https://dev.azure.com/{0}/{1}/_apis/build/definitions?queryOrder=lastModifiedDescending&api-version=6.0" +$topNQueryString) -f $($this.OrganizationContext.OrganizationName), $thisProj.name; } else { $buildDefnURL = ("https://dev.azure.com/{0}/{1}/_apis/build/definitions?api-version=6.0" +$topNQueryString) -f $($this.OrganizationContext.OrganizationName), $thisProj.name; } $nObj=$this.MaxObjectsToScan $this.addResourceToSVT($buildDefnURL,"build",$projectName,$organizationId,$projectId,$false,$false,$null,[ref]$nObj); } } else { $nObj=$this.MaxObjectsToScan; $buildDefnURL = ""; #If service id based scan then will break the loop after one run because, sending all build ids to api as comma separated in one go. for ($i = 0; $i -lt $this.BuildNames.Count; $i++) { #If service id based scan then send all build ids to api as comma separated in one go. if ($this.isServiceIdBasedScan -eq $true) { $buildDefnURL = "https://{0}.visualstudio.com/{1}/_apis/build/definitions?definitionIds={2}&api-version=6.0" -f $($this.OrganizationContext.OrganizationName), $projectName, ($this.BuildIds -join ","); } else { #If normal scan (not service id based) then send each build name in api one by one. $buildDefnURL = "https://{0}.visualstudio.com/{1}/_apis/build/definitions?name={2}&api-version=6.0" -f $($this.OrganizationContext.OrganizationName), $projectName, $this.BuildNames[$i]; } $buildDefnsObj = [WebRequestHelper]::InvokeGetWebRequest($buildDefnURL) if (([Helpers]::CheckMember($buildDefnsObj, "count") -and $buildDefnsObj[0].count -gt 0) -or (($buildDefnsObj | Measure-Object).Count -gt 0 -and [Helpers]::CheckMember($buildDefnsObj[0], "name"))) { foreach ($bldDef in $buildDefnsObj) { $link = $bldDef.url.split('?')[0].replace('_apis/build/Definitions/', '_build?definitionId='); $buildResourceId = "organization/$organizationId/project/$projectId/build/$($bldDef.id)"; $this.AddSVTResource($bldDef.name, $bldDef.project.name, "ADO.Build", $buildResourceId, $bldDef, $link); if (--$nObj -eq 0) { break; } } $buildDefnsObj = $null; Remove-Variable buildDefnsObj; } #If service id based scan then no need to run loop as all the build ids has been sent to api as comma separated list in one go. so break the loop. if ($this.isServiceIdBasedScan -eq $true) { break; } } } } #Initialysing null to SecurityNamespaceId variable for new scan, it is static variable, setting once only in svc class and same value is applicable for all the svc con withing org [Build]::SecurityNamespaceId = $null; } #check if long running scan allowed or not. if(!$this.isAllowLongRunningScanCheck()) { return; } if ($this.ReleaseNames.Count -gt 0 -and ($this.ResourceTypeName -in ([ResourceTypeName]::Release, [ResourceTypeName]::All, [ResourceTypeName]::Build_Release, [ResourceTypeName]::Build_Release_SvcConn_AgentPool_VarGroup_User_CommonSVTResources)) -and $this.shouldFetchResource -eq $true) { if ($this.ProjectNames -ne "*") { $this.PublishCustomMessage("Getting release configurations..."); } #When Undo fix for build/release inactive controls is called, resources need to be fetched from deleted list if ($this.IsAutomatedFixUndoCmd) { $accessToken = [RemoteApiHelper]::GetAccessToken() $apiURL = "https://vsrm.dev.azure.com/{0}/_apis/Contribution/HierarchyQuery/project/{1}" -f $($this.organizationName), $projectId $inputbody = "{'contributionIds':['ms.vss-releaseManagement-web.deleted-definitions-data-provider'],'dataProviderContext':{'properties':{'sourcePage':{'url':'https://dev.azure.com/$($this.organizationName)/$projectName/_release?view=deleted','routeId':'ms.vss-releaseManagement-web.hub-explorer-3-default-route','routeValues':{'project':'$projectName','viewname':'hub-explorer-3-view','controller':'ContributedPage','action':'Execute'}}}}}" | ConvertFrom-Json $headers = @{ "Authorization"= ("Bearer " + $accessToken); "Accept"="application/json;api-version=5.0-preview.1;excludeUrls=true;enumsAsNumbers=true;msDateFormat=true;noArrayWrap=true"; "content-type"="application/json"; }; $responseObj = [WebRequestHelper]::InvokePostWebRequest($apiURL,$headers, $inputbody); $releaseDefnsObj = @() if([Helpers]::CheckMember($responseObj,"dataProviders") -and $responseObj.dataProviders.'ms.vss-releaseManagement-web.deleted-definitions-data-provider' -and [Helpers]::CheckMember($responseObj.dataProviders.'ms.vss-releaseManagement-web.deleted-definitions-data-provider',"releaseDefinitions") -and $responseObj.dataProviders.'ms.vss-releaseManagement-web.deleted-definitions-data-provider'.releaseDefinitions) { $releaseDefnsObj = $responseObj.dataProviders."ms.vss-releaseManagement-web.deleted-definitions-data-provider".releaseDefinitions; } if ($releaseDefnsObj.count -gt 0) { foreach ($relDef in $releaseDefnsObj) { $releaseResourceId = "organization/$organizationId/project/$projectId/release/$($relDef.id)"; $this.AddSVTResource($relDef.name, $projectName, "ADO.Release", $releaseResourceId, $relDef, $null); } $releaseDefnsObj = $null; Remove-Variable releaseDefnsObj; } } else { if(-not [string]::IsNullOrEmpty($this.ReleasesFolderPath)){ # Validate folder path is valid $path = $this.ReleasesFolderPath; $this.ReleasesFolderPath = $this.ReleasesFolderPath.Replace(' ','%20').Replace('\','%5C') $releasesFoldersURL = "https://vsrm.dev.azure.com/{0}/{1}/_apis/release/folders/{2}?api-version=6.1-preview.2" -f $($this.OrganizationContext.OrganizationName), $thisProj.name, $this.ReleasesFolderPath $releasesFoldersObj = [WebRequestHelper]::InvokeGetWebRequest($releasesFoldersURL) if($null -eq $releasesFoldersObj -or (![Helpers]::CheckMember($releasesFoldersObj[0],"Path"))){ $this.PublishCustomMessage("Folder path not found. Please validate the -ReleasesFolderPath provided in the command. `n", [MessageType]::Warning); } else { if($this.BatchScan){ $this.addReleasesToSvtInBatchScan($projectName,$projectId,$path) } else { #API doesnt provide all folders in a path, fallback to fetch all resources and then filter $nObj=$this.MaxObjectsToScan $releaseDefURL = ("https://vsrm.dev.azure.com/{0}/{1}/_apis/release/definitions?api-version=6.0" ) -f $($this.OrganizationContext.OrganizationName), $thisProj.name; $this.addResourceToSVT($releaseDefURL,"release",$projectName, $organizationId, $projectId, $true, $true, $path,[ref]$nObj) } } } elseif ($this.ReleaseNames -eq "*") { if($this.BatchScan){ $this.addReleasesToSvtInBatchScan($projectName,$projectId,$null); } else { $nObj=$this.MaxObjectsToScan $releaseDefnURL = ("https://vsrm.dev.azure.com/{0}/{1}/_apis/release/definitions?api-version=6.0") -f $($this.OrganizationContext.OrganizationName), $projectName; $this.addResourceToSVT($releaseDefnURL,"release",$projectName,$organizationId,$projectId,$false,$false,$null,[ref]$nObj); } } else { try { $nObj=$this.MaxObjectsToScan $releaseDefnsObj = $null; #If service id based scan then will break the loop after one run because, sending all release ids to api as comma separated in one go. for ($i = 0; $i -lt $this.ReleaseNames.Count; $i++) { #If service id based scan then send all release ids to api as comma separated in one go. if ($this.isServiceIdBasedScan -eq $true) { $url = "https://vsrm.dev.azure.com/{0}/{1}/_apis/release/definitions?definitionIdFilter={2}&api-version=6.0" -f $($this.OrganizationContext.OrganizationName), $projectName, ($this.ReleaseIds -join ","); } else { #If normal scan (not service id based) then send each release name in api one by one. $url = "https://vsrm.dev.azure.com/{0}/{1}/_apis/release/definitions?searchText={2}&isExactNameMatch=true&api-version=6.0" -f $($this.OrganizationContext.OrganizationName), $projectName, $this.ReleaseNames[$i]; } $releaseDefnsObj = [WebRequestHelper]::InvokeGetWebRequest($url); foreach ($relDef in $releaseDefnsObj) { $link = "https://dev.azure.com/{0}/{1}/_release?_a=releases&view=mine&definitionId={2}" -f $this.OrganizationContext.OrganizationName, $projectName, $relDef.url.split('/')[-1]; $releaseResourceId = "organization/$organizationId/project/$projectId/release/$($relDef.id)"; $this.AddSVTResource($relDef.name, $projectName, "ADO.Release", $releaseResourceId, $null, $link); if (--$nObj -eq 0) { break; } } #If service id based scan then no need to run loop as all the release ids has been sent to api as comma separated list in one go. so break the loop. if ($this.isServiceIdBasedScan -eq $true) { break; } } } catch { #Write-Error $_.Exception.Message; Write-Warning "Release pipelines for the project [$($projectName)] could not be fetched."; } } } #Initialysing null to SecurityNamespaceId variable for new scan, it is static variable, setting once only in release class and same value is applicable for all the release pipelines withing org [Release]::SecurityNamespaceId = $null; } #check if long running scan allowed or not. if(!$this.isAllowLongRunningScanCheck()) { return; } #Note: $topNQueryString is currently not supported in the SvcConn and AgentPool APIs. if ($this.ServiceConnections.Count -gt 0 -and ($this.ResourceTypeName -in ([ResourceTypeName]::ServiceConnection, [ResourceTypeName]::All, [ResourceTypeName]::Build_Release_SvcConn_AgentPool_VarGroup_User_CommonSVTResources, [ResourceTypeName]::SvcConn_AgentPool_VarGroup_CommonSVTResources))) { if ($this.ProjectNames -ne "*") { $this.PublishCustomMessage("Getting service endpoint configurations..."); } # Here we are fetching all the svc conns in the project and then filtering out. But in build & release we fetch them individually unless '*' is used for fetching all of them. $serviceEndpointURL = ("https://dev.azure.com/{0}/{1}/_apis/serviceendpoint/endpoints?includeDetails=True&api-version=6.0-preview.4") -f $($this.organizationName), $($projectName); $serviceEndpointObj = [WebRequestHelper]::InvokeGetWebRequest($serviceEndpointURL) $TotalSvc += ($serviceEndpointObj | Measure-Object).Count # service connection count here $projectData["serviceConnections"] = ($serviceEndpointObj | Measure-Object).Count; if (([Helpers]::CheckMember($serviceEndpointObj, "count") -and $serviceEndpointObj[0].count -gt 0) -or (($serviceEndpointObj | Measure-Object).Count -gt 0 -and [Helpers]::CheckMember($serviceEndpointObj[0], "name"))) { # Currently get only Azure Connections as all controls are applicable for same $Connections = $null; if ($this.ServiceConnections -eq "*") { $Connections = $serviceEndpointObj #| Where-Object { ($_.type -eq "azurerm" -or $_.type -eq "azure" -or $_.type -eq "git" -or $_.type -eq "github" -or $_.type -eq "externaltfs" -or $_.type -eq "externalnpmregistry" -or $_.type -eq "generic" -or $_.type -eq "externalnugetfeed" -or $_.type -eq "PRSS" -or $_.type -eq "ESRPScan") } } else { #If service id based scan then filter with serviceconnection ids if ($this.isServiceIdBasedScan -eq $true) { $Connections = $serviceEndpointObj | Where-Object { ($this.ServiceConnectionIds -eq $_.Id) } # ($_.type -eq "azurerm" -or $_.type -eq "azure" -or $_.type -eq "git" -or $_.type -eq "github" -or $_.type -eq "externaltfs" -or $_.type -eq "externalnpmregistry" -or $_.type -eq "generic" -or $_.type -eq "externalnugetfeed" -or $_.type -eq "PRSS" -or $_.type -eq "ESRPScan") -and } else { $Connections = $serviceEndpointObj | Where-Object { ($this.ServiceConnections -eq $_.name) } # ($_.type -eq "azurerm" -or $_.type -eq "azure" -or $_.type -eq "git" -or $_.type -eq "github" -or $_.type -eq "externaltfs" -or $_.type -eq "externalnpmregistry" -or $_.type -eq "generic" -or $_.type -eq "externalnugetfeed" -or $_.type -eq "PRSS" -or $_.type -eq "ESRPScan" -or $_.type -eq "servicefabric") -and } } $ScannableSvc += ($connections | Measure-Object).Count #Initialising null to SecurityNamespaceId variable for new scan, it is static variable, setting once only in svc class and same value is applicable for all the svc con withing org [ServiceConnection]::SecurityNamespaceId = $null; $serviceEndpointObj = $null; Remove-Variable serviceEndpointObj; $nObj = $this.MaxObjectsToScan foreach ($connectionObject in $Connections) { $resourceId = "organization/$organizationId/project/$projectId/serviceconnection/$($connectionObject.Id)"; $link = "https://dev.azure.com/$($this.organizationName)/$projectId/_settings/adminservices?resourceId=$($connectionObject.Id)"; $this.AddSVTResource($connectionObject.name, $projectName, "ADO.ServiceConnection", $resourceId, $connectionObject, $link); if (--$nObj -eq 0) { break; } } } } #check if long running scan allowed or not. if(!$this.isAllowLongRunningScanCheck()) { return; } if ($this.AgentPools.Count -gt 0 -and ($this.ResourceTypeName -in ([ResourceTypeName]::AgentPool, [ResourceTypeName]::All, [ResourceTypeName]::Build_Release_SvcConn_AgentPool_VarGroup_User_CommonSVTResources, [ResourceTypeName]::SvcConn_AgentPool_VarGroup_CommonSVTResources)) -and $this.shouldFetchResource -eq $true) { if ($this.ProjectNames -ne "*") { $this.PublishCustomMessage("Getting agent pools configurations..."); } # Here we are fetching all the agent pools in the project and then filtering out. But in build & release we fetch them individually unless '*' is used for fetching all of them. $agentPoolsDefnURL = "https://dev.azure.com/{0}/{1}/_apis/distributedtask/queues?api-version=6.1-preview.1" -f $($this.OrganizationContext.OrganizationName), $projectName; try { $agentPoolsDefnsObj = [WebRequestHelper]::InvokeGetWebRequest($agentPoolsDefnURL); if (($agentPoolsDefnsObj | Measure-Object).Count -gt 0 ) { $nObj = $this.MaxObjectsToScan $projectData["agentPools"] = ($agentPoolsDefnsObj | Measure-Object).Count if ($this.AgentPools -eq "*") { # We need to filter out legacy agent pools (Hosted, Hosted VS 2017 etc.) as they are not visible to user on the portal. As a result, they won't be able to remediate their respective controls $taskAgentQueues = $agentPoolsDefnsObj | where-object{$_.pool.isLegacy -eq $false}; } else { #If service id based scan then filter with agent pool ids if ($this.isServiceIdBasedScan -eq $true) { $taskAgentQueues = $agentPoolsDefnsObj | Where-Object {($_.pool.isLegacy -eq $false) -and ($this.AgentPoolIds -contains $_.Id) } } else { $taskAgentQueues = $agentPoolsDefnsObj | Where-Object {($_.pool.isLegacy -eq $false) -and ($this.AgentPools -contains $_.name) } } } #Filtering out "Azure Pipelines" agent pool from scan as it is created by ADO by default and some of its settings are not editable (grant access to all pipelines, auto-provisioning etc.) $taskAgentQueues = $taskAgentQueues | where-object{$_.name -ne "Azure Pipelines"}; foreach ($taq in $taskAgentQueues) { $resourceId = "https://dev.azure.com/{0}/_apis/securityroles/scopes/distributedtask.agentqueuerole/roleassignments/resources/{1}_{2}" -f $($this.OrganizationContext.OrganizationName), $($taq.projectId), $taq.id $agtpoolResourceId = "organization/$organizationId/project/$projectId/agentpool/$($taq.id)"; $link = "https://dev.azure.com/{0}/{1}/_settings/agentqueues?queueId={2}&view=security" -f $($this.OrganizationContext.OrganizationName), $($taq.projectId), $taq.id $this.AddSVTResource($taq.name, $projectName, "ADO.AgentPool", $agtpoolResourceId, $null, $link); if (--$nObj -eq 0) { break; } } $taskAgentQueues = $null; Remove-Variable taskAgentQueues; } } catch { Write-Warning "Agent pools for the project [$($projectName)] could not be fetched."; } } #check if long running scan allowed or not. if(!$this.isAllowLongRunningScanCheck()) { return; } if ($this.VariableGroups.Count -gt 0 -and ($this.ResourceTypeName -in ([ResourceTypeName]::VariableGroup, [ResourceTypeName]::All, [ResourceTypeName]::Build_Release_SvcConn_AgentPool_VarGroup_User_CommonSVTResources, [ResourceTypeName]::SvcConn_AgentPool_VarGroup_CommonSVTResources))) { if ($this.ProjectNames -ne "*") { $this.PublishCustomMessage("Getting variable group configurations..."); } try { if ($this.VariableGroups -eq "*") { $variableGroupURL = ("https://dev.azure.com/{0}/{1}/_apis/distributedtask/variablegroups?api-version=6.1-preview.2" +$topNQueryString) -f $($this.organizationName), $projectId; $variableGroupObj = [WebRequestHelper]::InvokeGetWebRequest($variableGroupURL) if($this.UseIncrementalScan){ $timestamp = (Get-Date) $incrementalScanHelperObj = [IncrementalScanHelper]::new($this.OrganizationContext.OrganizationName, $projectName, $this.IncrementalDate, $true, $timestamp) $incrementalScanHelperObj.SetContext($projectId, $this.OrganizationContext) $variableGroupObj = $incrementalScanHelperObj.GetModifiedCommonSvtFromAudit("VariableGroup",$variableGroupObj) } if (([Helpers]::CheckMember($variableGroupObj, "count") -and $variableGroupObj[0].count -gt 0) -or (($variableGroupObj | Measure-Object).Count -gt 0 -and [Helpers]::CheckMember($variableGroupObj[0], "name"))) { foreach ($group in $variableGroupObj) { $resourceId = "organization/$organizationId/project/$projectId/variablegroup/$($group.Id)"; $link = ("https://dev.azure.com/{0}/{1}/_library?itemType=VariableGroups&view=VariableGroupView&variableGroupId={2}") -f $($this.organizationName), $projectName, $($group.Id); $this.AddSVTResource($group.name, $projectName, "ADO.VariableGroup", $resourceId, $group, $link); } $variableGroupObj = $null } } else { for ($i = 0; $i -lt $this.VariableGroups.Count; $i++) { # This API does not support multiple variable group names at one go. $variableGroupURL = ("https://dev.azure.com/{0}/{1}/_apis/distributedtask/variablegroups?groupName={2}&api-version=6.0-preview.2") -f $($this.organizationName), $projectId, $this.VariableGroups[$i]; $variableGroupObj = [WebRequestHelper]::InvokeGetWebRequest($variableGroupURL) if (([Helpers]::CheckMember($variableGroupObj, "count") -and $variableGroupObj[0].count -gt 0) -or (($variableGroupObj | Measure-Object).Count -gt 0 -and [Helpers]::CheckMember($variableGroupObj[0], "name"))) { $varGroup = $null; #If service id based scan then filter with variablegroup ids if ($this.isServiceIdBasedScan -eq $true) { $varGroup = $variableGroupObj | Where-Object { $this.VariableGroupIds -eq $_.Id } } else { $varGroup = $variableGroupObj | Where-Object { $this.VariableGroups -eq $_.name } } foreach ($group in $varGroup) { $resourceId = "organization/$organizationId/project/$projectId/variablegroup/$($group.Id)"; $link = ("https://dev.azure.com/{0}/{1}/_library?itemType=VariableGroups&view=VariableGroupView&variableGroupId={2}") -f $($this.organizationName), $projectName, $($group.Id); $this.AddSVTResource($group.name, $projectName, "ADO.VariableGroup", $resourceId, $group, $link); } } } } } catch { Write-Warning "Variable groups for the project [$($projectName)] could not be fetched."; } } #Creating resource in common resource resolver if ($this.RepoNames.count -gt 0 -or $this.SecureFileNames.count -gt 0 -or $this.FeedNames.count -gt 0 -or $this.EnvironmentNames.count -gt 0 -or ($this.ResourceTypeName -in ([ResourceTypeName]::Repository, [ResourceTypeName]::SecureFile, [ResourceTypeName]::Feed, [ResourceTypeName]::Environment,[ResourceTypeName]::Build_Release_SvcConn_AgentPool_VarGroup_User_CommonSVTResources, [ResourceTypeName]::SvcConn_AgentPool_VarGroup_CommonSVTResources))) { $commonSVTResourceResolverObj = [CommonSVTResourceResolver]::new($this.organizationName, $organizationId, $projectId,$this.OrganizationContext, $this.IsAutomatedFixUndoCmd); $commonSVTResourceList = $commonSVTResourceResolverObj.LoadResourcesForScan($projectName, $this.RepoNames, $this.SecureFileNames, $this.FeedNames, $this.EnvironmentNames, $this.ResourceTypeName, $this.MaxObjectsToScan, $this.isServiceIdBasedScan); foreach ($commonSVTRsrc in $commonSVTResourceList) { $this.SVTResources.add($commonSVTRsrc) } } #Fetch only those resources for which data obj backup is available in local if([ControlHelper]::ControlFixBackup.Count -gt 0 -and $PSCmdlet.MyInvocation.MyCommand.Name -eq "Set-AzSKADOSecurityStatus") { $this.SVTResources = $this.SVTResources | Where-Object {[ControlHelper]::ControlFixBackup.ResourceId -contains $_.ResourceId} if ($this.ResourceNames.count -gt 0) { $this.SVTResources = $this.SVTResources | Where-Object {$this.ResourceNames -contains $_.ResourceName} } if ($this.ExcludeResourceNames.count -gt 0) { $this.SVTResources = $this.SVTResources | Where-Object {$this.ExcludeResourceNames -notcontains $_.ResourceName} } #Filter backup of only applicable resources [ControlHelper]::ControlFixBackup = @([ControlHelper]::ControlFixBackup | Where-Object {$this.SVTResources.ResourceId -contains $_.ResourceId}) } # getting all the resources count # and sending them to telemetry as well $scanSource = [AzSKSettings]::GetInstance().GetScanSource(); # Disabling resource telemetry for SDL scan. if($env:FetchInventoryDetails -eq $true -and $this.IsAIEnabled -eq $true -and $scanSource -ne 'SDL') { [InventoryHelper]::GetResourceCount($this.organizationName, $projectName, $projectId, $projectData); } #check if long running scan allowed or not. if(!$this.isAllowLongRunningScanCheck()) { return; } if (--$nProj -eq 0) { break; } #nProj is set to MaxObj before loop. } #Display count of total svc and svcs to be scanned #sending the details to telemetry as well if ($TotalSvc -gt 0) { #$this.PublishCustomMessage("Total service connections: $TotalSvc"); #$this.PublishCustomMessage("Total service connections that will be scanned: $ScannableSvc"); $properties = @{ "TotalServiceConnections" = $TotalSvc; "ScannableServiceConnections" = $ScannableSvc; } [AIOrgTelemetryHelper]::PublishEvent( "Service Connections count",$properties, @{}) } } } $this.SVTResourcesFoundCount = $this.SVTResources.Count } [bool] isAllowLongRunningScanCheck() { if ($this.SVTResources.count -gt $this.longRunningScanCheckPoint) { if (!$this.isAllowLongRunningScanInPolicy) { Write-Host ([Constants]::LongRunningScanStopByPolicyMsg) -ForegroundColor Yellow; $this.SVTResources = $null return $false; } elseif(!$this.allowLongRunningScan) { Write-Host ([Constants]::LongRunningScanStopMsg -f $this.longRunningScanCheckPoint) -ForegroundColor Yellow; $this.SVTResources = $null return $false; } } return $true; } #method for Set-AzSKADOBaselineConfigurations to check if org/proj are old [bool] IsResourceEligibleForBaselineConfig($resourceType,$resourceName){ if($this.baselineConfigurationForce){ return $true; } #if rtn is Org or Org and Project both, we first find the oldest project and check number of pipelines. In case rtn is project and * is given as project names, then also we find the oldest project pipeline if($resourceType -eq [ResourceTypeName]::Organization -or $resourceType -eq [ResourceTypeName]::Org_Project_User -or($resourceType -eq [ResourceTypeName]::Project -and $resourceName -eq "*")){ $apiURL = 'https://dev.azure.com/{0}/_apis/projects?$top=1000&api-version=6.0' -f $($this.OrganizationContext.OrganizationName); $responseObj = ""; try { $responseObj = [WebRequestHelper]::InvokeGetWebRequest($apiURL); $responseObj = $responseObj | Sort-Object -Property lastUpdateTime; $oldestProject = $responseObj[0].name; $buildURL = "https://dev.azure.com/{0}/{1}/_apis/build/definitions?api-version=6.0" -f $($this.OrganizationContext.OrganizationName), $oldestProject; $builds = @([WebRequestHelper]::InvokeGetWebRequest($buildURL)); $releaseURL = "https://vsrm.dev.azure.com/{0}/{1}/_apis/release/definitions?api-version=6.0" -f $($this.OrganizationContext.OrganizationName), $oldestProject; $releases = @([WebRequestHelper]::InvokeGetWebRequest($releaseURL)); if(($builds.Count -ge 1 -and [Helpers]::CheckMember($builds[0],"id")) -or ($releases.Count -ge 1 -and [Helpers]::CheckMember($releases[0],"id"))){ return $false; } } catch{ } } #We reach here when -rtn is just project and we have project names (either one or multiple), we check the number of pipelines in the current project #This will be checked once for each project else{ $buildURL = "https://dev.azure.com/{0}/{1}/_apis/build/definitions?api-version=6.0" -f $($this.OrganizationContext.OrganizationName), $resourceName; $releaseURL = "https://vsrm.dev.azure.com/{0}/{1}/_apis/release/definitions?api-version=6.0" -f $($this.OrganizationContext.OrganizationName), $resourceName; $releases = @([WebRequestHelper]::InvokeGetWebRequest($releaseURL)); $builds = @([WebRequestHelper]::InvokeGetWebRequest($buildURL)); if(($builds.Count -ge 1 -and [Helpers]::CheckMember($builds[0],"id")) -or ($releases.Count -ge 1 -and [Helpers]::CheckMember($releases[0],"id"))){ return $false; } } return $true; } [void] AddSVTResource([string] $name, [string] $resourceGroupName, [string] $resourceType, [string] $resourceId, [PSObject] $resourceDetailsObj, $resourceLink) { $svtResource = [SVTResource]::new(); $svtResource.ResourceName = $name; if ($resourceGroupName) { $svtResource.ResourceGroupName = $resourceGroupName; } $svtResource.ResourceType = $resourceType; $svtResource.ResourceId = $resourceId; $svtResource.ResourceTypeMapping = ([SVTMapping]::AzSKADOResourceMapping | Where-Object { $_.ResourceType -eq $resourceType } | Select-Object -First 1) if ($resourceDetailsObj) { $svtResource.ResourceDetails = $resourceDetailsObj; if(![Helpers]::CheckMember($svtResource.ResourceDetails,'ResourceLink')){ $svtResource.ResourceDetails | Add-Member -Name 'ResourceLink' -Type NoteProperty -Value $resourceLink; } } else { $svtResource.ResourceDetails = New-Object -TypeName psobject -Property @{ ResourceLink = $resourceLink } } $this.SVTResources.Add($svtResource) } [void] FetchServiceAssociatedResources($svcId, $projectName,$inputBuildNames,$inputReleaseNames,$inputSvcNames,$inputAgentPoolNames,$inputVargrpNames,$inputRepoNames,$inputFeedNames,$inputEnvNames,$inputSecFileNames) { $metaInfo = [MetaInfoProvider]::Instance; $rsrcList = $metaInfo.FetchServiceAssociatedResources($svcId, $projectName, $this.ResourceTypeName); $bFoundSvcMappedObjects = $false if ($null -ne $rsrcList) { $this.isServiceIdBasedScan = $true; if ($this.ResourceTypeName -in ([ResourceTypeName]::Build, [ResourceTypeName]::All, [ResourceTypeName]::Build_Release, [ResourceTypeName]::Build_Release_SvcConn_AgentPool_VarGroup_User_CommonSVTResources)) { if ($rsrcList.Builds -and $rsrcList.Builds.Count -gt 0) { if ($inputBuildNames -ne "*") { $rsrcList.Builds = @($rsrcList.Builds | Where { $_.buildDefinitionName -in $inputBuildNames }); } if ($rsrcList.Builds -and $rsrcList.Builds.Count -gt 0) { $this.BuildNames += $rsrcList.Builds.buildDefinitionName $this.BuildIds += $rsrcList.Builds.buildDefinitionId $bFoundSvcMappedObjects = $true } } } if ($this.ResourceTypeName -in ([ResourceTypeName]::Release, [ResourceTypeName]::All, [ResourceTypeName]::Build_Release, [ResourceTypeName]::Build_Release_SvcConn_AgentPool_VarGroup_User_CommonSVTResources)) { if ($rsrcList.Releases -and $rsrcList.Releases.Count -gt 0) { if ($inputReleaseNames -ne "*") { $rsrcList.Releases = @($rsrcList.Releases | Where { $_.releaseDefinitionName -in $inputReleaseNames }); } if ($rsrcList.Releases -and $rsrcList.Releases.Count -gt 0) { $this.ReleaseNames += $rsrcList.Releases.releaseDefinitionName $this.ReleaseIds += $rsrcList.Releases.releaseDefinitionId $bFoundSvcMappedObjects = $true } } } if ($this.ResourceTypeName -in ([ResourceTypeName]::ServiceConnection, [ResourceTypeName]::All, [ResourceTypeName]::Build_Release_SvcConn_AgentPool_VarGroup_User_CommonSVTResources,[ResourceTypeName]::SvcConn_AgentPool_VarGroup_CommonSVTResources)) { if ($rsrcList.ServiceConnections -and $rsrcList.ServiceConnections.Count -gt 0) { if ($inputSvcNames-ne "*") { $rsrcList.ServiceConnections = @($rsrcList.ServiceConnections | Where { $_.serviceConnectionName -in $inputSvcNames }); } if ($rsrcList.ServiceConnections -and $rsrcList.ServiceConnections.Count -gt 0) { $this.ServiceConnections += $rsrcList.ServiceConnections.serviceConnectionName $this.ServiceConnectionIds += $rsrcList.ServiceConnections.ServiceConnectionId $bFoundSvcMappedObjects = $true } } } if ($this.ResourceTypeName -in ([ResourceTypeName]::AgentPool, [ResourceTypeName]::All, [ResourceTypeName]::Build_Release_SvcConn_AgentPool_VarGroup_User_CommonSVTResources,[ResourceTypeName]::SvcConn_AgentPool_VarGroup_CommonSVTResources)) { if ($rsrcList.AgentPools -and $rsrcList.AgentPools.Count -gt 0) { if ($inputAgentPoolNames -ne "*") { $rsrcList.AgentPools = @($rsrcList.AgentPools | Where { $_.agentPoolName -in $inputAgentPoolNames }); } if ($rsrcList.AgentPools -and $rsrcList.AgentPools.Count -gt 0) { $this.AgentPools += $rsrcList.AgentPools.agentPoolName $this.AgentPoolIds += $rsrcList.AgentPools.agentPoolId $bFoundSvcMappedObjects = $true } } } if ($this.ResourceTypeName -in ([ResourceTypeName]::VariableGroup, [ResourceTypeName]::All, [ResourceTypeName]::Build_Release_SvcConn_AgentPool_VarGroup_User_CommonSVTResources,[ResourceTypeName]::SvcConn_AgentPool_VarGroup_CommonSVTResources)) { if ($rsrcList.VariableGroups -and $rsrcList.VariableGroups.Count -gt 0) { if ($inputVargrpNames -ne "*") { $rsrcList.VariableGroups = @($rsrcList.VariableGroups | Where { $_.variableGroupName -in $inputVargrpNames }); } if ($rsrcList.VariableGroups -and $rsrcList.VariableGroups.Count -gt 0) { $this.VariableGroups += $rsrcList.VariableGroups.variableGroupName $this.VariableGroupIds += $rsrcList.VariableGroups.variableGroupId $bFoundSvcMappedObjects = $true } } } #TODO: Remove this try catch in 2110 try { if ($this.ResourceTypeName -in ([ResourceTypeName]::Repository, [ResourceTypeName]::All, [ResourceTypeName]::Build_Release_SvcConn_AgentPool_VarGroup_User_CommonSVTResources, [ResourceTypeName]::SvcConn_AgentPool_VarGroup_CommonSVTResources)) { if ($rsrcList.Repositories -and $rsrcList.Repositories.Count -gt 0) { if ($inputRepoNames -ne "*") { $rsrcList.Repositories = @($rsrcList.repositories | Where { $_.repoName -in $inputRepoNames }); } if ($rsrcList.Repositories -and $rsrcList.Repositories.Count -gt 0) { $this.RepoNames += $rsrcList.Repositories.repoName $bFoundSvcMappedObjects = $true } } } if ($this.ResourceTypeName -in ([ResourceTypeName]::Feed, [ResourceTypeName]::All, [ResourceTypeName]::Build_Release_SvcConn_AgentPool_VarGroup_User_CommonSVTResources, [ResourceTypeName]::SvcConn_AgentPool_VarGroup_CommonSVTResources)) { if ($rsrcList.Feeds -and $rsrcList.Feeds.Count -gt 0) { if ($inputFeedNames -ne "*") { $rsrcList.Feeds = @($rsrcList.Feeds | Where { $_.feedName -in $inputFeedNames }); } if ($rsrcList.Feeds -and $rsrcList.Feeds.Count -gt 0) { $this.FeedNames += $rsrcList.Feeds.feedName $bFoundSvcMappedObjects = $true } } } if ($this.ResourceTypeName -in ([ResourceTypeName]::SecureFile, [ResourceTypeName]::All, [ResourceTypeName]::Build_Release_SvcConn_AgentPool_VarGroup_User_CommonSVTResources, [ResourceTypeName]::SvcConn_AgentPool_VarGroup_CommonSVTResources)) { if ($rsrcList.SecureFiles -and $rsrcList.SecureFiles.Count -gt 0) { if ($inputSecFileNames -ne "*") { $rsrcList.SecureFiles = @($rsrcList.SecureFiles | Where { $_.secureFileName -in $inputSecFileNames }); } if ($rsrcList.SecureFiles -and $rsrcList.SecureFiles.Count -gt 0) { $this.SecureFileNames += $rsrcList.SecureFiles.secureFileName $bFoundSvcMappedObjects = $true } } } if ($this.ResourceTypeName -in ([ResourceTypeName]::Environment, [ResourceTypeName]::All, [ResourceTypeName]::Build_Release_SvcConn_AgentPool_VarGroup_User_CommonSVTResources, [ResourceTypeName]::SvcConn_AgentPool_VarGroup_CommonSVTResources)) { if ($rsrcList.Environments -and $rsrcList.Environments.Count -gt 0) { if ($inputEnvNames -ne "*") { $rsrcList.Environments = @($rsrcList.Environments | Where { $_.environmentName -in $inputEnvNames }); } if ($rsrcList.Environments -and $rsrcList.Environments.Count -gt 0) { $this.EnvironmentNames += $rsrcList.Environments.environmentName $bFoundSvcMappedObjects = $true } } } } catch{ #eat the exception } } if ($bFoundSvcMappedObjects -eq $false) { $this.PublishCustomMessage("Could not find any objects mapped to the provided service id : $svcId", [MessageType]::Warning); } } #check for PCA group members [bool] isAdminControlScan() { $allowedAdminGrp = $null; if (!$this.ControlSettings) { $this.ControlSettings = [ConfigurationManager]::LoadServerConfigFile("ControlSettings.json"); } if ([Helpers]::CheckMember($this.ControlSettings, "AllowAdminControlScanForGroups")) { $allowedAdminGrp = $this.ControlSettings.AllowAdminControlScanForGroups | where { $_.ResourceType -eq "Organization" } | select-object -property GroupNames } $this.isUserPCA = [AdministratorHelper]::isUserOrgAdminMember($this.organizationName, $allowedAdminGrp); return $this.isUserPCA; } #check for PA group members [bool] isUserPA($project) { $allowedAdminGrp = $null; if (!$this.ControlSettings) { $this.ControlSettings = [ConfigurationManager]::LoadServerConfigFile("ControlSettings.json"); } if ([Helpers]::CheckMember($this.ControlSettings, "AllowAdminControlScanForGroups")) { $allowedAdminGrp = $this.ControlSettings.AllowAdminControlScanForGroups | where { $_.ResourceType -eq "Project" } | select-object -property GroupNames } return [AdministratorHelper]::isUserProjectAdminMember($this.organizationName, $project, $allowedAdminGrp); } # getting resources count and sending them to telemetry as well [void] GetResourceCount($projectName, $organizationId, $projectId, $projectData) { try{ # fetching the repository count of a project $resourceURL = "https://dev.azure.com/$($this.organizationName)/$($projectName)/_apis/git/repositories?api-version=6.1-preview.1" $responseList = [WebRequestHelper]::InvokeGetWebRequest($resourceURL) ; $projectData['repositories'] = ($responseList | Measure-Object).Count # fetching the testPlan count of a project $resourceURL = "https://dev.azure.com/$($this.organizationName)/$($projectName)/_apis/testplan/plans?api-version=6.0-preview.1" $responseList = [WebRequestHelper]::InvokeGetWebRequest($resourceURL) ; $projectData['testPlan'] = ($responseList | Measure-Object).Count # fetching the taskGroups count of a project $resourceURL = "https://dev.azure.com/$($this.organizationName)/$($projectName)/_apis/distributedtask/taskgroups?api-version=6.0-preview.1" $responseList = [WebRequestHelper]::InvokeGetWebRequest($resourceURL) ; $projectData['taskGroups'] = ($responseList | Measure-Object).Count # fetch the builds count $resourceURL = ("https://dev.azure.com/{0}/{1}/_apis/build/definitions?api-version=6.0&queryOrder=lastModifiedDescending&`$top=10000") -f $($this.OrganizationContext.OrganizationName), $projectName; $responseList = [WebRequestHelper]::InvokeGetWebRequest($resourceURL); $projectData['build'] = ($responseList | Measure-Object).Count # fetch the release count $resourceURL = ("https://vsrm.dev.azure.com/{0}/{1}/_apis/release/definitions?api-version=6.0&`$top=10000") -f $($this.OrganizationContext.OrganizationName), $projectName; $responseList = [WebRequestHelper]::InvokeGetWebRequest($resourceURL); $projectData['release'] = ($responseList | Measure-Object).Count; # fetch the agent pools count if($projectData["agentPools"] -eq -1) { $agentPoolsDefnURL = "https://dev.azure.com/{0}/{1}/_apis/distributedtask/queues?api-version=6.0-preview.1" -f $($this.OrganizationContext.OrganizationName), $projectName; $agentPoolsDefnsObj = [WebRequestHelper]::InvokeGetWebRequest($agentPoolsDefnURL); $projectData["agentPools"] = ($agentPoolsDefnsObj | Measure-Object).Count } # fetch the variable groups count if ($projectData["variableGroups"] -eq -1) { $variableGroupURL = ("https://dev.azure.com/{0}/{1}/_apis/distributedtask/variablegroups?api-version=6.1-preview.2") -f $($this.organizationName), $projectId; $variableGroupObj = [WebRequestHelper]::InvokeGetWebRequest($variableGroupURL) if (([Helpers]::CheckMember($variableGroupObj, "count") -and $variableGroupObj[0].count -gt 0) -or (($variableGroupObj | Measure-Object).Count -gt 0 -and [Helpers]::CheckMember($variableGroupObj[0], "name"))) { $varGroups = $variableGroupObj $projectData["variableGroups"] = ($varGroups | Measure-Object).Count } } } catch {} [AIOrgTelemetryHelper]::PublishEvent("Projects resources count", $projectData, @{}) } #only for build and release [void] addResourceToSVT([string] $resourceDfnUrl, [string] $resourceType, [string] $projectName, [string] $organizationId, [string]$projectId, [bool] $isFolderPathGiven, [bool] $isFolderSizegt100,[string] $path,[ref] $nObj){ [System.Uri] $validatedUri = $null; $orginalUri = ""; $skipCount = 0 $batchCount = 1; #$nObj = $this.MaxObjectsToScan $timestamp = (Get-Date) # to break out from looping and making further API calls after first call, when not all resources in first fetch are modified after threshold date $breakLoop = $false $resourcesFromAuditAdded = $false; $modifiedResources = @() if (!$this.ControlSettings) { $this.ControlSettings = [ConfigurationManager]::LoadServerConfigFile("ControlSettings.json"); } if($this.UseIncrementalScan -eq $true){ $incrementalScanHelperObjAudit = [IncrementalScanHelper]::new($this.OrganizationContext, $projectId,$projectName, $this.IncrementalDate); if($resourceType -eq "build"){ if($incrementalScanHelperObjAudit.ShouldDiscardOldIncScan('Build') -eq $false){ $buildIdsFromAudit = @($incrementalScanHelperObjAudit.GetAuditTrailsForBuilds()); $buildIdsFromAttestation = @($incrementalScanHelperObjAudit.GetAttestationAfterInc($projectName,'Build')); #perform union in case attested resources also appear in audit logs $buildIdsFromAudit = @($buildIdsFromAudit + $buildIdsFromAttestation | select -uniq) if($buildIdsFromAudit.Count -gt 0 -and $null -ne $buildIdsFromAudit[0]){ #get only those builds that have been modified before latest scan, builds modified after will be captured later $modifiedResources=@($incrementalScanHelperObjAudit.GetModifiedBuildsFromAudit($buildIdsFromAudit,$projectName)) } } else{ $this.PublishCustomMessage("Last full scan for incremental scan for builds was found to be older than $($this.ControlSettings.IncrementalScan.IncrementalScanValidForDays) days. Therefore, initiating complete scan for builds.") } } else { if($incrementalScanHelperObjAudit.ShouldDiscardOldIncScan('Release') -eq $false){ $releaseIdsFromAudit = @($incrementalScanHelperObjAudit.GetAuditTrailsForReleases()); $releaseIdsFromAttestation = @($incrementalScanHelperObjAudit.GetAttestationAfterInc($projectName,'Release')); $releaseIdsFromAudit = @($releaseIdsFromAudit + $releaseIdsFromAttestation | select -uniq) if($releaseIdsFromAudit.Count -gt 0 -and $null -ne $releaseIdsFromAudit[0]){ $modifiedResources=@($incrementalScanHelperObjAudit.GetModifiedReleasesFromAudit($releaseIdsFromAudit,$projectName)) } } else{ $this.PublishCustomMessage("Last full scan for incremental scan for releases was found to be older than $($this.ControlSettings.IncrementalScan.IncrementalScanValidForDays) days. Therefore, initiating complete scan for releases.") } } } while ([System.Uri]::TryCreate($resourceDfnUrl, [System.UriKind]::Absolute, [ref] $validatedUri)) { if ([string]::IsNullOrWhiteSpace($orginalUri)) { $orginalUri = $validatedUri.AbsoluteUri; } $progressCount = 0; $applicableDefnsObj=@(); $skipCount += 10000; $responseAndUpdatedUri = [WebRequestHelper]::InvokeWebRequestForResourcesInBatch($validatedUri, $orginalUri, $skipCount,$resourceType); #API response with resources $resourceDefnsObj = @($responseAndUpdatedUri[0]); #count of all resources fetched $totalCount = $resourceDefnsObj.Count #updated URI: null when there is no continuation token $resourceDfnUrl = $responseAndUpdatedUri[1]; if($isFolderPathGiven -and $isFolderSizegt100) { $applicableDefnsObj = $resourceDefnsObj | Where-Object {$_.path -eq "\$($path)" -or $_.path -replace '\s','' -match [System.Text.RegularExpressions.Regex]::Escape("$($path -replace '\s','')")} } #in case its not a folder based scan or folder cnt <100 else { $applicableDefnsObj=$resourceDefnsObj; } if ( (($applicableDefnsObj | Measure-Object).Count -gt 0 -and [Helpers]::CheckMember($applicableDefnsObj[0], "name")) -or ([Helpers]::CheckMember($applicableDefnsObj, "count") -and $applicableDefnsObj[0].count -gt 0)) { if($resourceType -eq "build"){ $tempLink=($applicableDefnsObj[0].url -split('Definitions/'))[0].replace('_apis/build/', '_build?definitionId='); } else { $tempLink = "https://dev.azure.com/{0}/{1}/_release?_a=releases&view=mine&definitionId=" -f $this.OrganizationContext.OrganizationName, $projectName; } if($this.UseIncrementalScan -eq $true) { $updateTimestamp = $true if(-not [string]::IsNullOrWhiteSpace($resourceDfnUrl)) { $updateTimestamp = $false } $incrementalScanHelperObj = [IncrementalScanHelper]::new($this.OrganizationContext.OrganizationName, $projectName, $this.IncrementalDate, $updateTimestamp, $timestamp) if($resourceType -eq "build") { $applicableDefnsObj = @($incrementalScanHelperObj.GetModifiedBuilds($applicableDefnsObj)) if($applicableDefnsObj.Count -lt $totalCount -and $updateTimestamp -eq $false) { # a continuation token was previously found but no need for making more API calls as even some resources in the first batch are unmodified since last threshold timestamp # update Incremental Scan Helper Object data member UpdateTime to $true, then call function to Update Timestamp $incrementalScanHelperObj.UpdateTime = $true $incrementalScanHelperObj.UpdateTimeStamp("Build") $breakLoop = $true } } else { $applicableDefnsObj = @($incrementalScanHelperObj.GetModifiedReleases($applicableDefnsObj)) } if($modifiedResources.Count -gt 0 -and $resourcesFromAuditAdded -eq $false){ $applicableDefnsObj+=$modifiedResources; $resourcesFromAuditAdded = $true; } } if($applicableDefnsObj.Count -lt $nObj.Value) { $nObj.Value = $applicableDefnsObj.Count; } foreach ($resourceDef in $applicableDefnsObj) { #$link = $resourceDef.url.split('?')[0].replace('_apis/build/Definitions/', '_build?definitionId='); $link=$tempLink+$resourceDef.id $resourceId = "organization/$organizationId/project/$projectId/$($resourceType)/$($resourceDef.id)"; if($resourceType -eq "build"){ $this.AddSVTResource($resourceDef.name, $resourceDef.project.name, "ADO.Build", $resourceId, $resourceDef, $link); } else { $this.AddSVTResource($resourceDef.name, $projectName, "ADO.Release", $resourceId, $null, $link); } if ($progressCount%100 -eq 0) { Write-Progress -Activity "Fetching $($resourceType)s in batches. This may take time. Fetched $($progressCount) of $(($applicableDefnsObj | Measure-Object).Count) $($resourceType)s of batch $($batchCount) " -Status "Progress: " -PercentComplete ($progressCount / ($applicableDefnsObj | Measure-Object).Count * 100) } $progressCount = $progressCount + 1; if (--$nObj.Value -eq 0) { break; } } $batchCount = $batchCount + 1; } else { break; } if ($nObj.Value -eq 0) { break; } if($breakLoop -eq $true) { break; } } Write-Progress -Activity "All $($resourceType)s fetched" -Status "Ready" -Completed $resourceDefnsObj = $null; $applicableDefnsObj=$null; Remove-Variable resourceDefnsObj; Remove-Variable applicableDefnsObj; } [string] FindResourceTypeFromPartialScan($nonScannedResourceId){ $type=""; switch -wildcard ($nonScannedResourceId) { "*/release/*" {$type="ADO.Release"; Break} "*/agentpool/*" {$type="ADO.AgentPool"; Break} Default {} } return $type; } [string] CreateResourceLinkFromPartialScan($nonScannedResourceId,$resourceType,$orgName,$projName,$projId){ $resourceLink="https://dev.azure.com/{0}/" -f $($orgName); switch ($resourceType) { "ADO.Release" { $definitionId=($nonScannedResourceId -split('/release/'))[1]; $resourceLink+=$projName+"/_release?_a=releases&view=mine&definitionId="+$definitionId; Break } "ADO.AgentPool" { $definitionId=($nonScannedResourceId -split('/agentpool/'))[1]; $resourceLink+=$projId+"/_settings/agentqueues?queueId="+$definitionId+"&view=security"; Break } Default {} } return $resourceLink } [void] FetchControlFixBackupFile($orgName, $projName, $internalId) { [ControlHelper]::ControlFixBackup = @() $BackupControlStateRootFolder = (Join-Path $([Constants]::AzSKAppFolderPath) "TempState" | Join-Path -ChildPath "BackupControlState"); if($internalId -match "Organization") { $BackupControlStateControlJson = (Join-Path $BackupControlStateRootFolder $orgName) } else { $BackupControlStateControlJson = (Join-Path (Join-Path $BackupControlStateRootFolder $orgName) $projName) } $fileName = $internalId + ".json" if(Test-Path (Join-Path $BackupControlStateControlJson $fileName)) { [ControlHelper]::ControlFixBackup += Get-Content (Join-Path $BackupControlStateControlJson $fileName) -Raw | ConvertFrom-Json } else { $this.PublishCustomMessage("`nBackup of control data object not found. Please run GADS with -PrepareforControlFix param to generate the backup.",[MessageType]::Warning); } } [void] addBuildsToSvtInBatchScan($ProjectName,$ProjectId,$Path){ if($PSCmdlet.MyInvocation.BoundParameters.ContainsKey("BatchScanMultipleProjects")){ [BatchScanManagerForMultipleProjects] $batchScanMngr = [BatchScanManagerForMultipleProjects]:: GetInstance(); } else { [BatchScanManager] $batchScanMngr = [BatchScanManager]:: GetInstance(); } $batchStatus= $batchScanMngr.GetBatchStatus(); if(-not("BuildCurrentContinuationToken" -in $batchStatus.PSobject.Properties.Name)){ Write-Error -Message "A previous batch with different resource type was found to still be in progress. You can either run the command with the same parameters as the previous batch or if you wish to scan with the current new command you should clear the given folders: %LOCALAPPDATA%/Microsoft/AzSK.ADO/Tempstate/BatchScanData/[org_name] and %LOCALAPPDATA%/Microsoft/AzSK.ADO/Tempstate/PartialScanData/[org_name]" -Category InvalidArgument } #all builds have been scanned if([string]::IsNullOrEmpty($batchStatus.BuildCurrentContinuationToken) -and $batchStatus.Skip -gt 0){ return; } $topNQueryString = '&$top={0}' -f $batchScanMngr.GetBatchSize(); if($null -ne $batchStatus.BuildCurrentContinuationToken){ $buildDefURL= ("https://dev.azure.com/{0}/{1}/_apis/build/definitions?queryOrder=lastModifiedDescending&api-version=6.0&%24skip={2}&continuationToken={3}" +$topNQueryString) -f $($this.OrganizationContext.OrganizationName), $ProjectName, $batchStatus.Skip, $batchStatus.BuildCurrentContinuationToken; } else { $buildDefURL= ("https://dev.azure.com/{0}/{1}/_apis/build/definitions?queryOrder=lastModifiedDescending&api-version=6.0&%24skip={2}" +$topNQueryString) -f $($this.OrganizationContext.OrganizationName), $ProjectName, $batchStatus.Skip; } $updatedUriAndContToken=[WebRequestHelper]:: InvokeWebRequestForContinuationToken($buildDefURL,$buildDefURL,$null,'build'); $continuationToken=$updatedUriAndContToken[0]; $buildDefnsObj=$updatedUriAndContToken[2]; if($null -ne $Path){ $buildDefnsObj = $buildDefnsObj | Where-Object {$_.path -eq "\$($Path)" -or $_.path -replace '\s','' -match [System.Text.RegularExpressions.Regex]::Escape("$($Path -replace '\s','')")} } $progressCount=1 if (([Helpers]::CheckMember($buildDefnsObj, "count") -and $buildDefnsObj[0].count -gt 0) -or (($buildDefnsObj | Measure-Object).Count -gt 0 -and [Helpers]::CheckMember($buildDefnsObj[0], "name"))) { foreach ($bldDef in $buildDefnsObj) { $link = $bldDef.url.split('?')[0].replace('_apis/build/Definitions/', '_build?definitionId='); $buildResourceId = "organization/$($this.OrganizationContext.OrganizationId)/project/$ProjectId/build/$($bldDef.id)"; $this.AddSVTResource($bldDef.name, $bldDef.project.name, "ADO.Build", $buildResourceId, $bldDef, $link); if ($progressCount%100 -eq 0) { Write-Progress -Activity "Fetched $($progressCount) out of $(($buildDefnsObj | Measure-Object).Count) builds " -Status "Progress: " -PercentComplete ($progressCount / ($buildDefnsObj | Measure-Object).Count * 100) } $progressCount+=1 } $buildDefnsObj = $null; Remove-Variable buildDefnsObj; } Write-Progress -Activity "All builds fetched" -Status "Ready" -Completed $batchStatus.BuildNextContinuationToken=$continuationToken; $batchStatus.TokenLastModifiedTime=[DateTime]::UtcNow; $batchScanMngr.BatchScanTrackerObj = $batchStatus; $batchScanMngr.WriteToBatchTrackerFile(); } [void] addReleasesToSvtInBatchScan($ProjectName,$ProjectId,$Path){ if($PSCmdlet.MyInvocation.BoundParameters.ContainsKey("BatchScanMultipleProjects")){ [BatchScanManagerForMultipleProjects] $batchScanMngr = [BatchScanManagerForMultipleProjects]:: GetInstance(); } else { [BatchScanManager] $batchScanMngr = [BatchScanManager]:: GetInstance(); } $batchStatus= $batchScanMngr.GetBatchStatus(); if(-not("ReleaseCurrentContinuationToken" -in $batchStatus.PSobject.Properties.Name)){ Write-Error -Message "A previous batch with different resource type was found to still be in progress. You can either run the command with the same parameters as the previous batch or if you wish to scan with the current new command you should clear the given folders: %LOCALAPPDATA%/Microsoft/AzSK.ADO/Tempstate/BatchScanData/[org_name] and %LOCALAPPDATA%/Microsoft/AzSK.ADO/Tempstate/PartialScanData/[org_name]" -Category InvalidArgument } #all releases have been scanned if([string]::IsNullOrEmpty($batchStatus.ReleaseCurrentContinuationToken) -and $batchStatus.Skip -gt 0){ return; } $topNQueryString = '&$top={0}' -f $batchScanMngr.GetBatchSize(); if($null -ne $batchStatus.ReleaseCurrentContinuationToken){ $releaseDefURL= ("https://vsrm.dev.azure.com/{0}/{1}/_apis/release/definitions?api-version=6.0&continuationToken={2}" +$topNQueryString) -f $($this.OrganizationContext.OrganizationName), $ProjectName, $batchStatus.ReleaseCurrentContinuationToken; } else { $releaseDefURL= ("https://vsrm.dev.azure.com/{0}/{1}/_apis/release/definitions?api-version=6.0" +$topNQueryString ) -f $($this.OrganizationContext.OrganizationName), $ProjectName; } $updatedUriAndContToken=[WebRequestHelper]:: InvokeWebRequestForContinuationToken($releaseDefURL,$releaseDefURL,$null,'release'); $continuationToken=$updatedUriAndContToken[0]; $releaseDefnsObj=$updatedUriAndContToken[2]; if($null -ne $Path){ $releaseDefnsObj = $releaseDefnsObj | Where-Object {$_.path -eq "\$($Path)" -or $_.path -replace '\s','' -match [System.Text.RegularExpressions.Regex]::Escape("$($Path -replace '\s','')")} } $progressCount=1 if (([Helpers]::CheckMember($releaseDefnsObj, "count") -and $releaseDefnsObj[0].count -gt 0) -or (($releaseDefnsObj | Measure-Object).Count -gt 0 -and [Helpers]::CheckMember($releaseDefnsObj[0], "name"))) { $tempLink = "https://dev.azure.com/{0}/{1}/_release?_a=releases&view=mine&definitionId=" -f $this.OrganizationContext.OrganizationName, $projectName; foreach ($releaseDef in $releaseDefnsObj) { $link = $tempLink+$releaseDef.id $releaseResourceId = "organization/$($this.OrganizationContext.OrganizationId)/project/$ProjectId/release/$($releaseDef.id)"; $this.AddSVTResource($releaseDef.name, $ProjectName, "ADO.Release", $releaseResourceId, $null, $link); if ($progressCount%100 -eq 0){ Write-Progress -Activity "Fetched $($progressCount) out of $(($releaseDefnsObj | Measure-Object).Count) releases " -Status "Progress: " -PercentComplete ($progressCount / ($releaseDefnsObj | Measure-Object).Count * 100) } $progressCount+=1 } $releaseDefnsObj = $null; Remove-Variable releaseDefnsObj; } Write-Progress -Activity "All releases fetched" -Status "Ready" -Completed $batchStatus.ReleaseNextContinuationToken=$continuationToken; $batchStatus.TokenLastModifiedTime=[DateTime]::UtcNow; $batchScanMngr.BatchScanTrackerObj = $batchStatus; $batchScanMngr.WriteToBatchTrackerFile(); } } # SIG # Begin signature block # MIInvwYJKoZIhvcNAQcCoIInsDCCJ6wCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCKMNkLpc3EX1dB # Y9oEh+QxfV1azTko5KGVYwwo2orqc6CCDXYwggX0MIID3KADAgECAhMzAAADrzBA # DkyjTQVBAAAAAAOvMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjMxMTE2MTkwOTAwWhcNMjQxMTE0MTkwOTAwWjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDOS8s1ra6f0YGtg0OhEaQa/t3Q+q1MEHhWJhqQVuO5amYXQpy8MDPNoJYk+FWA # hePP5LxwcSge5aen+f5Q6WNPd6EDxGzotvVpNi5ve0H97S3F7C/axDfKxyNh21MG # 0W8Sb0vxi/vorcLHOL9i+t2D6yvvDzLlEefUCbQV/zGCBjXGlYJcUj6RAzXyeNAN # xSpKXAGd7Fh+ocGHPPphcD9LQTOJgG7Y7aYztHqBLJiQQ4eAgZNU4ac6+8LnEGAL # go1ydC5BJEuJQjYKbNTy959HrKSu7LO3Ws0w8jw6pYdC1IMpdTkk2puTgY2PDNzB # tLM4evG7FYer3WX+8t1UMYNTAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQURxxxNPIEPGSO8kqz+bgCAQWGXsEw # RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW # MBQGA1UEBRMNMjMwMDEyKzUwMTgyNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci # tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG # CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu # Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0 # MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAISxFt/zR2frTFPB45Yd # mhZpB2nNJoOoi+qlgcTlnO4QwlYN1w/vYwbDy/oFJolD5r6FMJd0RGcgEM8q9TgQ # 2OC7gQEmhweVJ7yuKJlQBH7P7Pg5RiqgV3cSonJ+OM4kFHbP3gPLiyzssSQdRuPY # 1mIWoGg9i7Y4ZC8ST7WhpSyc0pns2XsUe1XsIjaUcGu7zd7gg97eCUiLRdVklPmp # XobH9CEAWakRUGNICYN2AgjhRTC4j3KJfqMkU04R6Toyh4/Toswm1uoDcGr5laYn # TfcX3u5WnJqJLhuPe8Uj9kGAOcyo0O1mNwDa+LhFEzB6CB32+wfJMumfr6degvLT # e8x55urQLeTjimBQgS49BSUkhFN7ois3cZyNpnrMca5AZaC7pLI72vuqSsSlLalG # OcZmPHZGYJqZ0BacN274OZ80Q8B11iNokns9Od348bMb5Z4fihxaBWebl8kWEi2O # PvQImOAeq3nt7UWJBzJYLAGEpfasaA3ZQgIcEXdD+uwo6ymMzDY6UamFOfYqYWXk # ntxDGu7ngD2ugKUuccYKJJRiiz+LAUcj90BVcSHRLQop9N8zoALr/1sJuwPrVAtx # HNEgSW+AKBqIxYWM4Ev32l6agSUAezLMbq5f3d8x9qzT031jMDT+sUAoCw0M5wVt # CUQcqINPuYjbS1WgJyZIiEkBMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq # hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x # EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv # bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 # IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG # EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG # A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg # Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC # CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03 # a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr # rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg # OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy # 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9 # sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh # dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k # A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB # w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn # Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90 # lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w # ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o # ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD # VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa # BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny # bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG # AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t # L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV # HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3 # dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG # AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl # AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb # C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l # hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6 # I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0 # wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560 # STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam # ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa # J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah # XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA # 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt # Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr # /Xmfwb1tbWrJUnMTDXpQzTGCGZ8wghmbAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp # Z25pbmcgUENBIDIwMTECEzMAAAOvMEAOTKNNBUEAAAAAA68wDQYJYIZIAWUDBAIB # BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIB7I+qr/qpScOwanWEARet55 # TWY/Y+ObnxIY6IQXxLGrMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A # cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB # BQAEggEAVvtXLG3JHCIcQPeoxZ/oyEMLVTorg0Ixn8X+0z+VTfbcOCKApoLms6CQ # E40WNHTS9tv2hpgMEmOwHt4naS3nHoC4Xesfx8W5ug2qTqDnj9EcXK4GzrcpiA+d # 1+mT9P4aI/9N1KTcfkvyRu8BNGsO/EumMDichzhV6lCItOOKcXTmw+QrjODJ87Y7 # QbF9aO3mWAvPoL15WHMoDJuUKFl0xSGJ/pXoQ4JLC3Nx25qgSDi9pcGr5/pR8Z8+ # ddBhlQi4/YgF8DvIKBlepkCwe1bLhMhE17ZaZQDwNc8X5HylNkzCd2i+irE45hZ2 # ZMjFjpTAK5KpebR8qrySTsLgnz0kVKGCFykwghclBgorBgEEAYI3AwMBMYIXFTCC # FxEGCSqGSIb3DQEHAqCCFwIwghb+AgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFZBgsq # hkiG9w0BCRABBKCCAUgEggFEMIIBQAIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl # AwQCAQUABCAvj5tiWFhETM5lIIUntKqXftYQD+EGhtKpKkdS1LfbiQIGZdX+oZFd # GBMyMDI0MDMxMTEwNDMzMi42MDlaMASAAgH0oIHYpIHVMIHSMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl # bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNO # OjNCRDQtNEI4MC02OUMzMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBT # ZXJ2aWNloIIReDCCBycwggUPoAMCAQICEzMAAAHlj2rA8z20C6MAAQAAAeUwDQYJ # KoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x # EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv # bjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcNMjMx # MDEyMTkwNzM1WhcNMjUwMTEwMTkwNzM1WjCB0jELMAkGA1UEBhMCVVMxEzARBgNV # BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv # c29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxhbmQgT3Bl # cmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjozQkQ0LTRC # ODAtNjlDMzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZTCC # AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKl74Drau2O6LLrJO3HyTvO9 # aXai//eNyP5MLWZrmUGNOJMPwMI08V9zBfRPNcucreIYSyJHjkMIUGmuh0rPV5/2 # +UCLGrN1P77n9fq/mdzXMN1FzqaPHdKElKneJQ8R6cP4dru2Gymmt1rrGcNe800C # cD6d/Ndoommkd196VqOtjZFA1XWu+GsFBeWHiez/PllqcM/eWntkQMs0lK0zmCfH # +Bu7i1h+FDRR8F7WzUr/7M3jhVdPpAfq2zYCA8ZVLNgEizY+vFmgx+zDuuU/GChD # K7klDcCw+/gVoEuSOl5clQsydWQjJJX7Z2yV+1KC6G1JVqpP3dpKPAP/4udNqpR5 # HIeb8Ta1JfjRUzSv3qSje5y9RYT/AjWNYQ7gsezuDWM/8cZ11kco1JvUyOQ8x/JD # kMFqSRwj1v+mc6LKKlj//dWCG/Hw9ppdlWJX6psDesQuQR7FV7eCqV/lfajoLpPN # x/9zF1dv8yXBdzmWJPeCie2XaQnrAKDqlG3zXux9tNQmz2L96TdxnIO2OGmYxBAA # ZAWoKbmtYI+Ciz4CYyO0Fm5Z3T40a5d7KJuftF6CToccc/Up/jpFfQitLfjd71cS # +cLCeoQ+q0n0IALvV+acbENouSOrjv/QtY4FIjHlI5zdJzJnGskVJ5ozhji0YRsc # v1WwJFAuyyCMQvLdmPddAgMBAAGjggFJMIIBRTAdBgNVHQ4EFgQU3/+fh7tNczEi # fEXlCQgFOXgMh6owHwYDVR0jBBgwFoAUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXwYD # VR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9j # cmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3JsMGwG # CCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDovL3d3dy5taWNyb3NvZnQu # Y29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIw # MjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcD # CDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQADggIBADP6whOFjD1ad8Gk # EJ9oLBuvfjndMyGQ9R4HgBKSlPt3pa0XVLcimrJlDnKGgFBiWwI6XOgw82hdolDi # MDBLLWRMTJHWVeUY1gU4XB8OOIxBc9/Q83zb1c0RWEupgC48I+b+2x2VNgGJUsQI # yPR2PiXQhT5PyerMgag9OSodQjFwpNdGirna2rpV23EUwFeO5+3oSX4JeCNZvgyU # OzKpyMvqVaubo+Glf/psfW5tIcMjZVt0elswfq0qJNQgoYipbaTvv7xmixUJGTbi # xYifTwAivPcKNdeisZmtts7OHbAM795ZvKLSEqXiRUjDYZyeHyAysMEALbIhdXgH # Eh60KoZyzlBXz3VxEirE7nhucNwM2tViOlwI7EkeU5hudctnXCG55JuMw/wb7c71 # RKimZA/KXlWpmBvkJkB0BZES8OCGDd+zY/T9BnTp8si36Tql84VfpYe9iHmy7Pqq # xqMF2Cn4q2a0mEMnpBruDGE/gR9c8SVJ2ntkARy5SfluuJ/MB61yRvT1mUx3lypp # O22ePjBjnwoEvVxbDjT1jhdMNdevOuDeJGzRLK9HNmTDC+TdZQlj+VMgIm8ZeEIR # NF0oaviF+QZcUZLWzWbYq6yDok8EZKFiRR5otBoGLvaYFpxBZUE8mnLKuDlYobjr # xh7lnwrxV/fMy0F9fSo2JxFmtLgtMIIHcTCCBVmgAwIBAgITMwAAABXF52ueAptJ # mQAAAAAAFTANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT # Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m # dCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNh # dGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgyMjI1WhcNMzAwOTMwMTgzMjI1 # WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH # UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD # Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCAiIwDQYJKoZIhvcNAQEB # BQADggIPADCCAgoCggIBAOThpkzntHIhC3miy9ckeb0O1YLT/e6cBwfSqWxOdcjK # NVf2AX9sSuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+uDZnhUYjDLWNE893MsAQGOhg # fWpSg0S3po5GawcU88V29YZQ3MFEyHFcUTE3oAo4bo3t1w/YJlN8OWECesSq/XJp # rx2rrPY2vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhiJdxqD89d9P6OU8/W7IVWTe/d # vI2k45GPsjksUZzpcGkNyjYtcI4xyDUoveO0hyTD4MmPfrVUj9z6BVWYbWg7mka9 # 7aSueik3rMvrg0XnRm7KMtXAhjBcTyziYrLNueKNiOSWrAFKu75xqRdbZ2De+JKR # Hh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXftnIv231fgLrbqn427DZM9itu # qBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8P0zbr17C89XYcz1DTsEzOUyO # ArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMYctenIPDC+hIK12NvDMk2ZItb # oKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9stQcxWv2XFJRXRLbJbqvUAV6 # bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/eKtFtvUeh17aj54WcmnGrnu3tz5q4i6t # AgMBAAGjggHdMIIB2TASBgkrBgEEAYI3FQEEBQIDAQABMCMGCSsGAQQBgjcVAgQW # BBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4EFgQUn6cVXQBeYl2D9OXSZacb # UzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYz # aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnku # aHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIA # QwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2 # VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwu # bWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEw # LTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93 # d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYt # MjMuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38Kq3hLB9nATEkW+Geckv8qW/q # XBS2Pk5HZHixBpOXPTEztTnXwnE2P9pkbHzQdTltuw8x5MKP+2zRoZQYIu7pZmc6 # U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gngugnue99qb74py27YP0h1AdkY3m2CDPVt # I1TkeFN1JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQJL1AoL8ZthISEV09J+BAljis # 9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1ijbCHcNhcy4sa3tuPywJeBTp # kbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP9pEB9s7GdP32THJvEKt1MMU0 # sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkkvnNtyo4JvbMBV0lUZNlz138e # W0QBjloZkWsNn6Qo3GcZKCS6OEuabvshVGtqRRFHqfG3rsjoiV5PndLQTHa1V1QJ # sWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g75LcVv7TOPqUxUYS8vwLBgqJ7 # Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr4A245oyZ1uEi6vAnQj0llOZ0 # dFtq0Z4+7X6gMTN9vMvpe784cETRkPHIqzqKOghif9lwY1NNje6CbaUFEMFxBmoQ # tB1VM1izoXBm8qGCAtQwggI9AgEBMIIBAKGB2KSB1TCB0jELMAkGA1UEBhMCVVMx # EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT # FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxh # bmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjoz # QkQ0LTRCODAtNjlDMzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy # dmljZaIjCgEBMAcGBSsOAwIaAxUA942iGuYFrsE4wzWDd85EpM6RiwqggYMwgYCk # fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH # UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD # Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIF # AOmY3b8wIhgPMjAyNDAzMTEwOTMyNDdaGA8yMDI0MDMxMjA5MzI0N1owdDA6Bgor # BgEEAYRZCgQBMSwwKjAKAgUA6ZjdvwIBADAHAgEAAgIKdzAHAgEAAgIRWjAKAgUA # 6ZovPwIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAID # B6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBACD1pIiOCZfp5eHrq28I # LU4mI//4VWVqQnOJ6CORLbYkckL4o9XZrhNK57+djLiRNdKBEXoZx0KtZDNB2KI9 # O2bcklZXYnMQBfeHkAHrgWNNVB22ucLP8nBv+icXM0q8OKByM7dTprCoIHNq1oWW # On1QUhJ7zjDDsFkQt8aHChRXMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMx # EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT # FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUt # U3RhbXAgUENBIDIwMTACEzMAAAHlj2rA8z20C6MAAQAAAeUwDQYJYIZIAWUDBAIB # BQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQx # IgQg/naLaa+rV/k2TUmMO445/Yooat1XeHGR5anUHadt//swgfoGCyqGSIb3DQEJ # EAIvMYHqMIHnMIHkMIG9BCAVqdP//qjxGFhe2YboEXeb8I/pAof01CwhbxUH9U69 # 7TCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw # DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x # JjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB5Y9q # wPM9tAujAAEAAAHlMCIEICzdWXbSKu1w4P5HQop5giHzJlz+yQPfmJosZUheWk9x # MA0GCSqGSIb3DQEBCwUABIICAHeX3qudB+lauaBtzKgIubZWqNY8gmtyrUt2acyp # Rq+Bvx7brJSaunPw7eeQm5Ua1LcsABLJ1BpO6dxuWeWQyOeOe8e7biApdjr8COtZ # MO2FkvQO87MNP0zZ1b6ZvC0G8xK3uOUpb6N8UjvJjxX+Zhota4KzQJK6i5MPOLB0 # fKAZL89kttyVQghjK+OPOuJ+SR/Q1k557TOcb6rYJvVL/wOq+s0bTGpVfOfAFaEn # qZNBjGXikNdk/7J3aaaDPEN4ehfgjpo+bV4JWfQmQ+juIvF//RB9EqSdfIsR7MHe # enDnETdliKQEjH/RTqx1h/INm3Dc0XuEc8noU4jeLOAOXHzx8CqRFmL0YKyYL+tT # H3dEQaoXerDGVgmToZWh1S0TwB3UhvDlzzwlHVHvoONVxFoxAF+tACFSGYxRhLB3 # 0XxrMvzjS15hFfqF+InDM9o1jD5HLoCIilNnCP61Z613IWpoK+Gp94GNyGvIzrxU # QRqAV0h1mgKLNIbU+7UkoOQnG8q7gTpFINAC+szo0uPGfwm8o83stnqmW1ausIaw # UBgRsFTo+Ge4TC1lUKKIc9DsRolyf3qgpHuzlLx4Fq+t6CQur1jV+BLp2V9QHLc1 # xM+lPYnLVZtx4SnUEeuvujlTGijcBl7makrN1yZO0tNDAJMEZxYH+KqwwLwPD9Fu # W6qV # SIG # End signature block |