Framework/Helpers/IncrementalScanHelper.ps1
Set-StrictMode -Version Latest class IncrementalScanHelper { hidden [string] $OrganizationName = $null; hidden [string] $ProjectName = $null; hidden [string] $ProjectId = $null; hidden $OrganizationContext = $null; [PSObject] $ControlSettings; hidden [string] $AzSKTempStatePath = (Join-Path $([Constants]::AzSKAppFolderPath) "IncrementalScan"); hidden [string] $CAScanProgressSnapshotsContainerName = [Constants]::CAScanProgressSnapshotsContainerName; hidden [string] $ScanSource = $null; $StorageContext = $null; $ControlStateBlob = $null; $ContainerObject = $null; hidden [string] $IncrementalScanTimestampFile=$null; hidden [string] $CATempFile = $null; hidden [string] $MasterFilePath; hidden [PSObject] $ResourceTimestamps = $null; hidden [bool] $FirstScan = $false; hidden [datetime] $IncrementalDate = 0; hidden [datetime] $LastFullScan = 0; hidden [bool] $ShouldDiscardOldScan = $false; [bool] $UpdateTime = $true; hidden [datetime] $Timestamp = 0; [bool] $isPartialScanActive = $false; [bool] $IsFullScanInProgress = $false; static [PSObject] $auditSchema = $null [bool] $isIncFileAlreadyAvailable = $false; IncrementalScanHelper([string] $organizationName, [string] $projectName, [datetime] $incrementalDate, [bool] $updateTimestamp, [datetime] $timestamp) { $this.OrganizationName = $organizationName $this.ProjectName = $projectName $this.IncrementalScanTimestampFile = $([Constants]::IncrementalScanTimeStampFile) $this.ScanSource = [AzSKSettings]::GetInstance().GetScanSource() $this.CATempFile = "CATempLocal.json" # temporary file to store Json Data to upload to container (in CA) $this.IncrementalDate = $incrementalDate $this.MasterFilePath = (Join-Path (Join-Path (Join-Path $this.AzSKTempStatePath $this.OrganizationName) $this.projectName) $this.IncrementalScanTimestampFile) $this.UpdateTime = $updateTimestamp $this.Timestamp = $timestamp $this.ControlSettings = [ConfigurationManager]::LoadServerConfigFile("ControlSettings.json"); if($PSCmdlet.MyInvocation.BoundParameters.ContainsKey("UsePartialCommits")){ [PartialScanManager] $partialScanMngr = [PartialScanManager]::GetInstance(); if(($partialScanMngr.IsPartialScanInProgress($this.OrganizationName, $false) -eq [ActiveStatus]::Yes)){ $this.isPartialScanActive = $true } } if($null -eq [IncrementalScanHelper]::auditSchema){ [IncrementalScanHelper]::auditSchema = [ConfigurationManager]::LoadServerConfigFile("IncrementalScanAudits.json") } } IncrementalScanHelper($organizationContext, [string] $projectId,[string] $projectName, [datetime] $incrementalDate) { $this.OrganizationName = $organizationContext.OrganizationName $this.OrganizationContext = $organizationContext $this.ProjectId = $projectId $this.IncrementalScanTimestampFile = $([Constants]::IncrementalScanTimeStampFile) $this.ScanSource = [AzSKSettings]::GetInstance().GetScanSource() $this.CATempFile = "CATempLocal.json" # temporary file to store Json Data to upload to container (in CA) $this.IncrementalDate = $incrementalDate $this.ProjectName = $projectName $this.MasterFilePath = (Join-Path (Join-Path (Join-Path $this.AzSKTempStatePath $this.OrganizationName) $this.projectName) $this.IncrementalScanTimestampFile) $this.ControlSettings = [ConfigurationManager]::LoadServerConfigFile("ControlSettings.json"); if($PSCmdlet.MyInvocation.BoundParameters.ContainsKey("UsePartialCommits")){ [PartialScanManager] $partialScanMngr = [PartialScanManager]::GetInstance(); if(($partialScanMngr.IsPartialScanInProgress($this.OrganizationName, $false) -eq [ActiveStatus]::Yes)){ $this.isPartialScanActive = $true } } } hidden [datetime] GetThresholdTime([string] $resourceType) { # function to retrieve threshold time from storage, based on scan source. $latestScan = 0 if($this.ScanSource -ne "CA" -and $this.ScanSource -ne "CICD") { if(![string]::isnullorwhitespace($this.OrganizationName)) { if(Test-Path $this.MasterFilePath) { # File exists. Retrieve last timestamp. $this.ResourceTimestamps = Get-Content $this.MasterFilePath | ConvertFrom-Json if(-not ([Helpers]::CheckMember($this.ResourceTimestamps, $resourceType)) -or $null -eq $this.ResourceTimestamps.$resourceType -or [datetime]$this.ResourceTimestamps.$resourceType.LastScanTime -eq 0) { # Previous timestamp does not exist for this resource in the existing file. $this.FirstScan = $true } } else { #file does not exist $this.FirstScan = $true } } } elseif ($this.ScanSource -eq 'CA') { $this.MasterFilePath = (Join-Path (Join-Path (Join-Path $this.AzSKTempStatePath $this.OrganizationName) $this.ProjectName) $this.IncrementalScanTimestampFile) $tempPath = Join-Path $([Constants]::AzSKAppFolderPath) $this.CATempFile $blobPath = Join-Path (Join-Path (Join-Path "IncrementalScan" $this.OrganizationName) $this.ProjectName) $this.IncrementalScanTimestampFile try { #Validate if Storage is found $keys = Get-AzStorageAccountKey -ResourceGroupName $env:StorageRG -Name $env:StorageName $this.StorageContext = New-AzStorageContext -StorageAccountName $env:StorageName -StorageAccountKey $keys[0].Value -Protocol Https $this.ContainerObject = Get-AzStorageContainer -Context $this.StorageContext -Name $this.CAScanProgressSnapshotsContainerName -ErrorAction SilentlyContinue if($null -ne $this.ContainerObject) { #container exists $this.ControlStateBlob = Get-AzStorageBlob -Container $this.CAScanProgressSnapshotsContainerName -Context $this.StorageContext -Blob $blobPath -ErrorAction SilentlyContinue if($null -ne $this.ControlStateBlob) { # File exists. Copy existing timestamp file locally Get-AzStorageBlobContent -CloudBlob $this.ControlStateBlob.ICloudBlob -Context $this.StorageContext -Destination $tempPath -Force $this.ResourceTimestamps = Get-ChildItem -Path $tempPath -Force | Get-Content | ConvertFrom-Json #Delete the local file Remove-Item -Path $tempPath if(-not ([Helpers]::CheckMember($this.ResourceTimestamps, $resourceType)) -or $null -eq $this.ResourceTimestamps.$resourceType -or [datetime]$this.ResourceTimestamps.$resourceType.LastScanTime -eq 0) { # Previous timestamp does not exist for current resource in existing file. $this.FirstScan = $true } } else { # File does not exist. $this.FirstScan = $true } } else { # Container does not exist $this.FirstScan = $true } } catch { write-host "Exception when trying to find/create incremental scan container: $_." } } elseif($this.ScanSource -eq 'CICD'){ if (Test-Path env:incrementalScanURI) { #Uri is created in cicd task based on jobid $uri = $env:incrementalScanURI } else { $uri = [Constants]::StorageUri -f $this.OrgName, $this.OrgName, "IncrementalScanFile" } try { #check if file already in extension sotrage $webRequestResult = [WebRequestHelper]::InvokeGetWebRequest($uri) if($null -ne $webRequestResult){ $this.ResourceTimestamps = $webRequestResult | ConvertFrom-Json if(-not ([Helpers]::CheckMember($this.ResourceTimestamps, $resourceType)) -or $null -eq $this.ResourceTimestamps.$resourceType -or [datetime]$this.ResourceTimestamps.$resourceType.LastScanTime -eq 0) { # Previous timestamp does not exist for this resource in the existing file. $this.FirstScan = $true $this.isIncFileAlreadyAvailable = $true; } } else{ $this.FirstScan = $true $this.isIncFileAlreadyAvailable = $false; } } catch { $this.FirstScan = $true $this.isIncFileAlreadyAvailable = $false; } } if(-not $this.FirstScan) { if($this.isPartialScanActive){ $latestScan = [datetime]$this.ResourceTimestamps.$resourceType.LastPartialTime #to check if full scan is currently in progress, if we dont check this and give -dt switch full scan wont work if($this.ResourceTimestamps.$resourceType.IsFullScanInProgress){ $this.IsFullScanInProgress = $true } else{ $this.IsFullScanInProgress = $false } } else { $latestScan = [datetime]$this.ResourceTimestamps.$resourceType.LastScanTime $this.IsFullScanInProgress = $false } $this.LastFullScan = [datetime]$this.ResourceTimestamps.$resourceType.LastFullScanTime } if($this.IncrementalDate -ne 0) { # user input of incremental date to be used for scanning incrementally. $latestScan = $this.IncrementalDate if($this.ScanSource -eq 'CA'){ $FromTimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById("Asia/Kolkata") $latestScan = [DateTime]::SpecifyKind((Get-Date $latestScan), [DateTimeKind]::Unspecified) $latestScan = [System.TimeZoneInfo]::ConvertTimeToUtc($latestScan, $FromTimeZone) } } return $latestScan } UpdateTimeStamp([string] $resourceType) { # Updates timestamp of current scan to storage, based on scan source. if($this.UpdateTime -ne $true) { return; } if($this.isPartialScanActive){ return; } if($this.ScanSource -ne "CA" -and $this.ScanSource -ne "CICD") { if($this.FirstScan -eq $true) { # Check if file exists if((-not (Test-Path ($this.AzSKTempStatePath))) -or (-not (Test-Path (Join-Path $this.AzSKTempStatePath $this.OrganizationName))) -or (-not (Test-Path $this.MasterFilePath))) { # Incremental Scan happening first time locally OR Incremental Scan happening first time for Org OR first time for current Project New-Item -Type Directory -Path (Join-Path (Join-Path $this.AzSKTempStatePath $this.OrganizationName) $this.ProjectName) -ErrorAction Stop | Out-Null $this.ResourceTimestamps = [IncrementalScanTimestamps]::new() $resourceScanTimes = [IncrementalTimeStampsResources]@{ LastScanTime = $this.Timestamp; LastFullScanTime = $this.Timestamp; LastPartialTime = "0001-01-01T00:00:00.0000000"; IsFullScanInProgress = $false } $this.ResourceTimestamps.$resourceType = $resourceScanTimes [JsonHelper]::ConvertToJsonCustom($this.ResourceTimestamps) | Out-File $this.MasterFilePath -Force } else { # File exists for Organization and Project but first time scan for current resource type $this.ResourceTimestamps = Get-ChildItem -Path $this.MasterFilePath -Force | Get-Content | ConvertFrom-Json $resourceScanTimes = [IncrementalTimeStampsResources]@{ LastScanTime = $this.Timestamp; LastFullScanTime = $this.Timestamp; LastPartialTime = "0001-01-01T00:00:00.0000000"; IsFullScanInProgress = $false } $this.ResourceTimestamps.$resourceType = $resourceScanTimes [JsonHelper]::ConvertToJsonCustom($this.ResourceTimestamps) | Out-File $this.MasterFilePath -Force } } else { # Not a first time scan for the current resource $this.ResourceTimestamps = Get-ChildItem -Path $this.MasterFilePath -Force | Get-Content | ConvertFrom-Json $previousScanTime = $this.ResourceTimestamps.$resourceType.LastScanTime; $this.ResourceTimestamps.$resourceType.LastPartialTime= $previousScanTime if($this.IsFullScanInProgress -eq $false){ $this.ResourceTimestamps.$resourceType.IsFullScanInProgress = $false } #if old scan, we trigger full scan, store full scan value, also reset upc scan time if($this.ShouldDiscardOldScan){ $this.ResourceTimestamps.$resourceType.LastFullScanTime = $this.Timestamp $this.ResourceTimestamps.$resourceType.LastPartialTime = "0001-01-01T00:00:00.0000000"; $this.ResourceTimestamps.$resourceType.IsFullScanInProgress = $true } $this.ResourceTimestamps.$resourceType.LastScanTime = $this.Timestamp [JsonHelper]::ConvertToJsonCustom($this.ResourceTimestamps) | Out-File $this.MasterFilePath -Force } } elseif ($this.ScanSource -eq 'CA') { $tempPath = Join-Path $([Constants]::AzSKAppFolderPath) $this.CATempFile $blobPath = Join-Path (Join-Path (Join-Path "IncrementalScan" $this.OrganizationName) $this.ProjectName) $this.IncrementalScanTimestampFile if ($this.FirstScan -eq $true) { # Check if container object does not exist if($null -eq $this.ContainerObject) { # Container does not exist, create container. $this.ContainerObject = New-AzStorageContainer -Name $this.CAScanProgressSnapshotsContainerName -Context $this.StorageContext -ErrorAction SilentlyContinue if ($null -eq $this.ContainerObject ) { $this.PublishCustomMessage("Could not find/create partial scan container in storage.", [MessageType]::Warning); } $this.ResourceTimestamps = [IncrementalScanTimestamps]::new() } if($null -eq $this.ControlStateBlob) { $this.ResourceTimestamps = [IncrementalScanTimestamps]::new() } else { Get-AzStorageBlobContent -CloudBlob $this.ControlStateBlob.ICloudBlob -Context $this.StorageContext -Destination $tempPath -Force $this.ResourceTimestamps = Get-ChildItem -Path $tempPath -Force | Get-Content | ConvertFrom-Json #Delete the local file Remove-Item -Path $tempPath } $resourceScanTimes = [IncrementalTimeStampsResources]@{ LastScanTime = $this.Timestamp; LastFullScanTime = $this.Timestamp; LastPartialTime = "0001-01-01T00:00:00.0000000"; IsFullScanInProgress = $false } $this.ResourceTimestamps.$resourceType = $resourceScanTimes [JsonHelper]::ConvertToJsonCustom($this.ResourceTimestamps) | Out-File $tempPath -Force Set-AzStorageBlobContent -File $tempPath -Container $this.ContainerObject.Name -Blob $blobPath -Context $this.StorageContext -Force Remove-Item -Path $tempPath } else { Get-AzStorageBlobContent -CloudBlob $this.ControlStateBlob.ICloudBlob -Context $this.StorageContext -Destination $tempPath -Force $this.ResourceTimestamps = Get-ChildItem -Path $tempPath -Force | Get-Content | ConvertFrom-Json $previousScanTime = $this.ResourceTimestamps.$resourceType.LastScanTime; $this.ResourceTimestamps.$resourceType.LastPartialTime = $previousScanTime if($this.IsFullScanInProgress -eq $false){ $this.ResourceTimestamps.$resourceType.IsFullScanInProgress = $false } if($this.ShouldDiscardOldScan){ $this.ResourceTimestamps.$resourceType.LastFullScanTime = $this.Timestamp $this.ResourceTimestamps.$resourceType.LastPartialTime = "0001-01-01T00:00:00.0000000"; $this.ResourceTimestamps.$resourceType.IsFullScanInProgress = $true } # Delete the local file Remove-Item -Path $tempPath $this.ResourceTimestamps.$resourceType.LastScanTime = $this.Timestamp [JsonHelper]::ConvertToJsonCustom($this.ResourceTimestamps) | Out-File $tempPath -Force Set-AzStorageBlobContent -File $tempPath -Container $this.ContainerObject.Name -Blob $blobPath -Context $this.StorageContext -Force Remove-Item -Path $tempPath } } elseif($this.ScanSource -eq 'CICD'){ $incrementalScanPayload = $null if($this.FirstScan -eq $true){ #first scan for the pipeline for all resources if($this.isIncFileAlreadyAvailable -eq $false){ $this.ResourceTimestamps = [IncrementalScanTimestamps]::new() } #will be called for both scenarios: first scan for the resource as well as for the entire pipeline $resourceScanTimes = [IncrementalTimeStampsResources]@{ LastScanTime = $this.Timestamp; LastFullScanTime = $this.Timestamp; LastPartialTime = "0001-01-01T00:00:00.0000000"; IsFullScanInProgress = $false } $this.ResourceTimestamps.$resourceType = $resourceScanTimes $incrementalScanPayload = [JsonHelper]::ConvertToJsonCustom($this.ResourceTimestamps) } #not a first scan else{ $previousScanTime = $this.ResourceTimestamps.$resourceType.LastScanTime; $this.ResourceTimestamps.$resourceType.LastPartialTime= $previousScanTime if($this.IsFullScanInProgress -eq $false){ $this.ResourceTimestamps.$resourceType.IsFullScanInProgress = $false } #if old scan, we trigger full scan, store full scan value, also reset upc scan time if($this.ShouldDiscardOldScan){ $this.ResourceTimestamps.$resourceType.LastFullScanTime = $this.Timestamp $this.ResourceTimestamps.$resourceType.LastPartialTime = "0001-01-01T00:00:00.0000000"; $this.ResourceTimestamps.$resourceType.IsFullScanInProgress = $true } $this.ResourceTimestamps.$resourceType.LastScanTime = $this.Timestamp $incrementalScanPayload = [JsonHelper]::ConvertToJsonCustom($this.ResourceTimestamps) } try{ $rmContext = [ContextHelper]::GetCurrentContext(); $user = ""; $uri = ""; $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user,$rmContext.AccessToken))) $body = ""; if (Test-Path env:incrementalScanURI) { $uri = $env:incrementalScanURI $JobId =""; $JobId = $uri.Replace('?','/').Split('/')[$JobId.Length -2] #if the incremental scan is already present need to update the existing file if ($this.FirstScan -eq $false -or $this.isIncFileAlreadyAvailable -eq $true){ $body = @{"id" = $Jobid; "__etag"=-1; "value"= $incrementalScanPayload;} | ConvertTo-Json } else{ $body = @{"id" = $Jobid; "value"= $incrementalScanPayload;} | ConvertTo-Json } } else { $uri = [Constants]::StorageUri -f $this.OrgName, $this.OrgName, "IncrementalScanFile" if ($this.FirstScan -eq $false -or $this.isIncFileAlreadyAvailable -eq $true){ $body = @{"id" = "IncrementalScanFile";"__etag"=-1; "value"= $incrementalScanPayload;} | ConvertTo-Json } else{ $body = @{"id" = "IncrementalScanFile"; "value"= $incrementalScanPayload;} | ConvertTo-Json } } $webRequestResult = Invoke-WebRequest -Uri $uri -Method Put -ContentType "application/json" -Headers @{Authorization = ("Basic {0}" -f $base64AuthInfo) } -Body $body } catch{ Write-Host "Error updating Incremental Scan file: $($_)" } } } [bool] IsIncScanOld($resourceType){ $this.GetThresholdTime($resourceType) if($this.FirstScan){ return $false; } if($this.LastFullScan.AddDays($this.ControlSettings.IncrementalScan.IncrementalScanValidForDays) -lt [DateTime]::UtcNow){ return $true; } return $false; } [bool] ShouldDiscardOldIncScan($resourceType){ $this.ShouldDiscardOldScan = $false if($this.IsIncScanOld($resourceType)){ if($PSCmdlet.MyInvocation.BoundParameters.ContainsKey('Force')){ $this.ShouldDiscardOldScan = $false } else{ $this.ShouldDiscardOldScan = $true } } return $this.ShouldDiscardOldScan; } [System.Object[]] GetModifiedBuilds($buildDefnsObj) { # Function to filter builds that have been modified after threshold time $latestBuildScan = $this.GetThresholdTime("Build") if($this.FirstScan -eq $true -and $this.IncrementalDate -eq 0) { $this.UpdateTimeStamp("Build") return $buildDefnsObj } #if inc scan last time is 0 or if this is a full scan partial checkpoint, return all builds if($this.isPartialScanActive -and ($latestBuildScan -eq 0 -or $this.IsFullScanInProgress)){ return $buildDefnsObj } #if scan is old and no upc file found, simply return all builds, update scan time for full scans and last scan if($this.ShouldDiscardOldIncScan('Build') -and -not($this.isPartialScanActive)){ $this.UpdateTimeStamp("Build") return $buildDefnsObj } $newBuildDefns = @() if ([datetime] $buildDefnsObj[0].createdDate -lt $latestBuildScan) { # first resource is modified before the threshold time => all consequent are also modified before threshold # return empty list $this.UpdateTimeStamp("Build") return $newBuildDefns } #Binary search [int] $low = 0 # start index of array [int] $high = $buildDefnsObj.length - 1 # last index of array [int] $size = $buildDefnsObj.length # total length of array [int] $breakIndex = 0 while($low -le $high) { [int] $mid = ($low + $high)/2 # seeking the middle of the array [datetime] $modifiedDate = [datetime]($buildDefnsObj[$mid].createdDate) if($modifiedDate -ge $latestBuildScan) { # modified date is after the threshold time if(($mid + 1) -eq $size) { # all fetched build defs are modified after threshold time # return unmodified $this.UpdateTimeStamp("Build") return $buildDefnsObj } else { # mid point is not the last build defn if([datetime]($buildDefnsObj[$mid+1].createdDate) -lt $latestBuildScan) { # changing point found $breakIndex = $mid break } else { # search on right half $low = $mid + 1 } } } elseif ($modifiedDate -lt $latestBuildScan) { if($mid -eq 0) { # All fetched builds have been modified before the threshold return $newBuildDefns } else { if([datetime]($buildDefnsObj[$mid - 1].createdDate) -ge $latestBuildScan) { # changing point found $breakIndex = $mid - 1 break } else { # search on left half $high = $mid - 1 } } } } $newBuildDefns = @($buildDefnsObj[0..$breakIndex]) $this.UpdateTimeStamp("Build") return $newBuildDefns } [System.Object[]] GetModifiedReleases($releaseDefnsObj) { $latestReleaseScan = $this.GetThresholdTime("Release") if($this.FirstScan -eq $true -and $this.IncrementalDate -eq 0) { $this.UpdateTimeStamp("Release") return $releaseDefnsObj } if($this.isPartialScanActive -and ($latestReleaseScan -eq 0 -or $this.IsFullScanInProgress)){ return $releaseDefnsObj } if($this.ShouldDiscardOldIncScan('Release')){ $this.UpdateTimeStamp("Release") return $releaseDefnsObj } $newReleaseDefns = @() # Searching Linearly foreach ($releaseDefn in $releaseDefnsObj) { if ([datetime]($releaseDefn.modifiedOn) -ge $latestReleaseScan) { $newReleaseDefns += @($releaseDefn) } } $this.UpdateTimeStamp("Release") return $newReleaseDefns } #Get all resources attested after the latest scan [System.Object[]] GetAttestationAfterInc($projectName, $resourceType){ $resourceIds = @(); #if parameter not specified, wont be fetching these resources if(-not($PSCmdlet.MyInvocation.BoundParameters.ContainsKey('ScanAttestedResources'))){ return $resourceIds } $latestResourceScan = $this.GetThresholdTime($resourceType) if($this.ScanSource -ne 'CA'){ $latestResourceScan=$latestResourceScan.ToUniversalTime(); } $latestResourceScan =Get-Date $latestResourceScan -Format s if($this.FirstScan -eq $true -and $this.IncrementalDate -eq 0){ return $resourceIds; } [ControlStateExtension] $ControlStateExt = [ControlStateExtension]::new($this.OrganizationContext, $PSCmdlet.MyInvocation); $output = $ControlStateExt.RescanComputeControlStateIndexer($projectName, 'ADO.'+$resourceType); $output | ForEach-Object { if($_.AttestedDate -gt $latestResourceScan){ try { $resourceIds += ($_.ResourceId -split ($resourceType.ToLower() + "/"))[1] } catch { } } } return $resourceIds } [System.Object[]] GetAuditTrailsForBuilds(){ $latestBuildScan = $this.GetThresholdTime("Build") if($this.ScanSource -ne 'CA'){ $latestBuildScan=$latestBuildScan.ToUniversalTime(); } $latestBuildScan =Get-Date $latestBuildScan -Format s $buildIds = @(); if($this.FirstScan -eq $true -and $this.IncrementalDate -eq 0){ return $buildIds; } $auditUrl = "https://auditservice.dev.azure.com/{0}/_apis/audit/auditlog?startTime={1}&api-version=6.0-preview.1" -f $this.OrganizationName, $latestBuildScan try { $response = [WebRequestHelper]::InvokeGetWebRequest($auditUrl); $auditTrails = $response.decoratedAuditLogEntries; $modifiedBuilds = $auditTrails | Where-Object {$_.actionId -eq 'Security.ModifyPermission' -and $_.data.NamespaceName -eq 'Build' -and $_.data.Token -match $this.ProjectId+"/" } $restrictedBroaderGroups = @{} $broaderGroups = $this.ControlSettings.Build.RestrictedBroaderGroupsForBuild $broaderGroups.psobject.properties | foreach { $restrictedBroaderGroups[$_.Name] = $_.Value } $modifiedBuilds | foreach { $group = ($_.data.SubjectDisplayName -split("\\"))[1] if($group -in $restrictedBroaderGroups.keys ){ if($_.data.ChangedPermission -in $restrictedBroaderGroups[$group]){ $buildIds += (($_.data.Token -split("/"))[-1]) } } } $buildIds = $buildIds | Select -Unique } catch { } return $buildIds; } [System.Object[]] GetModifiedBuildsFromAudit($buildIds, $projectName){ $totalBuilds = $buildIds.Count $buildDefnObj =@() $newBuildDefns = @(); $queryIdCount = 0; $currentbuildIds = "" $buildIds | foreach { if($totalBuilds -lt 100){ $queryIdCount++; $currentbuildIds=$currentbuildIds+$_+"," if($queryIdCount -eq $totalBuilds){ $buildDefnURL = "https://{0}.visualstudio.com/{1}/_apis/build/definitions?definitionIds={2}&api-version=6.0" -f $($this.OrganizationName), $projectName, $currentbuildIds; try { $buildDefnObj += ([WebRequestHelper]::InvokeGetWebRequest($buildDefnURL)); } catch { } } } else { $queryIdCount++; $currentbuildIds=$currentbuildIds+$_+","; if($queryIdCount -eq 100){ $buildDefnURL = "https://{0}.visualstudio.com/{1}/_apis/build/definitions?definitionIds={2}&api-version=6.0" -f $($this.OrganizationName), $projectName, $currentbuildIds; try { $buildDefnObj += ([WebRequestHelper]::InvokeGetWebRequest($buildDefnURL)); $queryIdCount =0; $currentbuildIds=""; $totalBuilds -=100; } catch { } } } } $latestBuildScan = $this.GetThresholdTime("Build"); foreach ($buildDefn in $buildDefnObj) { if ([Helpers]::CheckMember($buildDefn,'CreatedDate') -and [datetime]($buildDefn.CreatedDate) -lt $latestBuildScan) { $newBuildDefns += @($buildDefn) } } return $newBuildDefns; } [System.Object[]] GetAuditTrailsForReleases(){ $latestReleaseScan = $this.GetThresholdTime("Release"); if($this.ScanSource -ne 'CA'){ $latestReleaseScan=$latestReleaseScan.ToUniversalTime(); } $latestReleaseScan = Get-Date $latestReleaseScan -Format s $releaseIds = @(); if($this.FirstScan -eq $true -and $this.IncrementalDate -eq 0){ return $releaseIds; } $auditUrl = "https://auditservice.dev.azure.com/{0}/_apis/audit/auditlog?startTime={1}&api-version=6.0-preview.1" -f $this.OrganizationName, $latestReleaseScan try { $response = [WebRequestHelper]::InvokeGetWebRequest($auditUrl); $auditTrails = $response.decoratedAuditLogEntries; $modifiedReleases = $auditTrails | Where-Object {$_.actionId -eq 'Security.ModifyPermission' -and $_.data.NamespaceName -eq 'ReleaseManagement' -and $_.data.Token -match $this.ProjectId+"/" } $restrictedBroaderGroups = @{} $broaderGroups = $this.ControlSettings.Release.RestrictedBroaderGroupsForRelease $broaderGroups.psobject.properties | foreach { $restrictedBroaderGroups[$_.Name] = $_.Value } $modifiedReleases| foreach { $group = ($_.data.SubjectDisplayName -split("\\"))[1] if($group -in $restrictedBroaderGroups.keys ){ if($_.data.ChangedPermission -in $restrictedBroaderGroups[$group]){ $releaseIds += (($_.data.Token -split("/"))[-1]) } } } $releaseIds = $releaseIds | Select -Unique } catch { } return $releaseIds; } [System.Object[]] GetModifiedReleasesFromAudit($releaseIds, $projectName){ $totalReleases = $releaseIds.Count $newReleaseDefns = @(); $releaseDefnObj =@() $queryIdCount = 0; $currentReleaseIds = "" $releaseIds | foreach { if($totalReleases -lt 100){ $queryIdCount++; $currentReleaseIds=$currentReleaseIds+$_+"," if($queryIdCount -eq $totalReleases){ $releaseDefnURL = "https://vsrm.dev.azure.com/{0}/{1}/_apis/release/definitions?definitionIdFilter={2}&api-version=6.0" -f $($this.OrganizationName), $projectName, $currentReleaseIds; try { $releaseDefnObj += ([WebRequestHelper]::InvokeGetWebRequest($releaseDefnURL)); } catch { } } } else { $queryIdCount++; $currentReleaseIds=$currentReleaseIds+$_+","; if($queryIdCount -eq 100){ $releaseDefnURL = "https://vsrm.dev.azure.com/{0}/{1}/_apis/release/definitions?definitionIdFilter={2}&api-version=6.0" -f $($this.OrganizationName), $projectName, $currentReleaseIds; try { $releaseDefnObj += ([WebRequestHelper]::InvokeGetWebRequest($releaseDefnURL)); $queryIdCount =0; $currentReleaseIds=""; $totalReleases -=100; } catch { } } } } $latestReleaseScan = $this.GetThresholdTime("Release"); foreach ($releaseDefn in $releaseDefnObj) { if ([Helpers]::CheckMember($releaseDefn,'modifiedOn') -and [datetime]($releaseDefn.modifiedOn) -lt $latestReleaseScan) { $newReleaseDefns += @($releaseDefn) } } return $newReleaseDefns; } #common function to get modified resource ids from audits for common svts and variable group [System.Object[]] GetModifiedCommonSvtAuditTrails($resourceType){ $resourceIds = @() #get last scan of the resources $latestScan = $this.GetThresholdTime($resourceType) if($this.ScanSource -ne 'CA'){ $latestScan=$latestScan.ToUniversalTime(); } $latestScan = Get-Date $latestScan -Format s $auditUrl = "https://auditservice.dev.azure.com/{0}/_apis/audit/auditlog?startTime={1}&api-version=6.0-preview.1" -f $this.OrganizationName, $latestScan try { $response = [WebRequestHelper]::InvokeGetWebRequest($auditUrl); $auditTrails = $response.decoratedAuditLogEntries; #get modified resources from filter $modifiedResources = $this.GetModifiedResourcesFilter($resourceType,$auditTrails) $modifiedResources | foreach { #extract resource ids from modified resources $resourceIds+=($_.data.([IncrementalScanHelper]::auditSchema.$resourceType.AuditEvents.($_.actionId)[1]) -split("/"))[-1] if($resourceType -eq "GitRepositories"){ #to handle events of permission changes on branches $resourceIds+=(($_.data.([IncrementalScanHelper]::auditSchema.$resourceType.AuditEvents.($_.actionId)[1]) -split("/refs"))[0]) -split("/")[-1] #to handle events of new repository creation $resourceIds+=($_.data.([IncrementalScanHelper]::auditSchema.$resourceType.AuditEvents.($_.actionId)[1]) -split("\."))[-1] } } $resourceIds = $resourceIds | Select -Unique } catch { } return $resourceIds } #function to filter audits according to resource type [System.Object[]] GetModifiedResourcesFilter($resourceType,$auditTrails){ $resourceTypeInFilter = $resourceType #in case of secure file and variable group the resource type in audits is library, for other resources the name is same if($resourceType -eq "SecureFile" -or $resourceType -eq "VariableGroup"){ $resourceTypeInFilter = "Library" } if($resourceType -eq "GitRepositories"){ $resourceTypeInFilter = "Git Repositories" } $modifiedResources = $auditTrails | Where-Object {$_.actionId -in [IncrementalScanHelper]::auditSchema.$resourceType.AuditEvents.PSObject.Properties.Name -and ([IncrementalScanHelper]::auditSchema.$resourceType.AuditEvents.($_.actionId)[0] -eq $true -or( $_.Data.([IncrementalScanHelper]::auditSchema.$resourceType.AuditEvents.($_.actionId)[0]) -eq $resourceTypeInFilter -or $_.Data.([IncrementalScanHelper]::auditSchema.$resourceType.AuditEvents.($_.actionId)[0]) -eq "repository" -or $_.Data.([IncrementalScanHelper]::auditSchema.$resourceType.AuditEvents.($_.actionId)[0]) -eq $resourceType))} return $modifiedResources } #function to get modified resources [System.Object[]] GetModifiedCommonSvtFromAudit($resourceType,$response){ $latestScan = $this.GetThresholdTime($resourceType) $latestScan =Get-Date $latestScan -Format s #$response = [WebRequestHelper]::InvokeGetWebRequest($url); #if this a first scan return all resources if($this.FirstScan -eq $true -and $this.IncrementalDate -eq 0){ $this.UpdateTimeStamp($resourceType) return $response } #if partial scan is active and last scan is 0 or this is a full scan in progress return all resources if($this.isPartialScanActive -and ($latestScan -eq 0 -or $this.IsFullScanInProgress)){ return $response } #if this is a old scan return all resources if($this.ShouldDiscardOldIncScan($resourceType)){ $this.UpdateTimeStamp($resourceType) return $response } #get ids from above functions $modifiedResourceIds = @($this.GetModifiedCommonSvtAuditTrails($resourceType)); if($resourceType -eq "GitRepositories"){ $modifiedResourceIdsFromAttestation = @($this.GetAttestationAfterInc($this.ProjectName,"Repository")) } else{ $modifiedResourceIdsFromAttestation = @($this.GetAttestationAfterInc($this.ProjectName,$resourceType)) } $modifiedResourceIds = @($modifiedResourceIds + $modifiedResourceIdsFromAttestation | select -uniq) $modifiedResources = @() #if we get some ids from audit trails add them to modified resource obj if($modifiedResourceIds.Count -gt 0 -and $null -ne $modifiedResourceIds[0]){ #filter all ids from audit trails in the api response $modifiedResources = @($response | Where-Object{$modifiedResourceIds -contains $_.id}) #to capture events that dont come in audits but is reflected in api responses such as new resource created, properties of resources edited etc. if([Helpers]::CheckMember([IncrementalScanHelper]::auditSchema.$resourceType, "ApiResponseFilter")){ $modifiedResources +=$response | Where-Object{$modifiedResourceIds -notcontains $_.id -and [datetime]($_.([IncrementalScanHelper]::auditSchema.$resourceType.ApiResponseFilter)) -gt $latestScan} } } #in case no ids were obtained from audits check from response for corresponding api response filtee if present else{ if([Helpers]::CheckMember([IncrementalScanHelper]::auditSchema.$resourceType, "ApiResponseFilter")){ $modifiedResources += $response | Where-Object{[datetime]($_.([IncrementalScanHelper]::auditSchema.$resourceType.ApiResponseFilter)) -gt $latestScan} } } $this.UpdateTimeStamp($resourceType) return $modifiedResources } [void] SetContext($projectId,$organizationContext){ $this.ProjectId = $projectId $this.OrganizationContext = $organizationContext } } # SIG # Begin signature block # MIIoKgYJKoZIhvcNAQcCoIIoGzCCKBcCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDu0RnMQpTI+qM0 # h0LTH/dgIIVOYsJ8iDSKXQeNVLNl26CCDXYwggX0MIID3KADAgECAhMzAAADrzBA # 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 # /Xmfwb1tbWrJUnMTDXpQzTGCGgowghoGAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp # Z25pbmcgUENBIDIwMTECEzMAAAOvMEAOTKNNBUEAAAAAA68wDQYJYIZIAWUDBAIB # BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIHEbGJPGGbxp2qp+hW8ueI8/ # dclQYL2yXz7hbt1hdP5CMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A # cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB # BQAEggEAVxOFAM4ohQApzRYFWhVNi5AufXLBHMzlax8qdXXLCHfqNRhbIIkIEeVV # Qg+Gq4b88UY9Qys6OYIGb3McQoP6RjlqFOTe9YfY4qVxZaiwjwlhV5fT/Z6JAlm2 # SA2G5IhhmEAkFmX7QBgv66BVWTMN/nHY1PlRNKp9tcQ2etyR1TSVisXhzqbtv5GE # VyBXk0u7+SCnBHf5LZsS5wVfVU0z9P0gY3R1tWsmV+OzHaLYphKRWPSrJldbON2/ # 5wh2XrD2EN80SfV/QwwOGKE0t8wtkTZcjRitwEOKcNxSp58q8rXtRMztpWMiE9uj # uhNbF9wYxgYYtgFZmcfb/et2/Uqt7aGCF5QwgheQBgorBgEEAYI3AwMBMYIXgDCC # F3wGCSqGSIb3DQEHAqCCF20wghdpAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsq # hkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl # AwQCAQUABCBVElBH/5Ok+viHpb3plXrd7VNnW4T6JdyuC0UqIhz9NAIGZbwSqMDl # GBMyMDI0MDIwOTA1MTM1MC42NzZaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l # cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046ODYwMy0w # NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wg # ghHqMIIHIDCCBQigAwIBAgITMwAAAfGzRfUn6MAW1gABAAAB8TANBgkqhkiG9w0B # AQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD # VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yMzEyMDYxODQ1 # NTVaFw0yNTAzMDUxODQ1NTVaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz # aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv # cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z # MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046ODYwMy0wNUUwLUQ5NDcxJTAjBgNV # BAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEB # AQUAA4ICDwAwggIKAoICAQCxulCZttIf8X97rW9/J+Q4Vg9PiugB1ya1/DRxxLW2 # hwy4QgtU3j5fV75ZKa6XTTQhW5ClkGl6gp1nd5VBsx4Jb+oU4PsMA2foe8gP9bQN # PVxIHMJu6TYcrrn39Hddet2xkdqUhzzySXaPFqFMk2VifEfj+HR6JheNs2LLzm8F # DJm+pBddPDLag/R+APIWHyftq9itwM0WP5Z0dfQyI4WlVeUS+votsPbWm+RKsH4F # QNhzb0t/D4iutcfCK3/LK+xLmS6dmAh7AMKuEUl8i2kdWBDRcc+JWa21SCefx5SP # hJEFgYhdGPAop3G1l8T33cqrbLtcFJqww4TQiYiCkdysCcnIF0ZqSNAHcfI9SAv3 # gfkyxqQNJJ3sTsg5GPRF95mqgbfQbkFnU17iYbRIPJqwgSLhyB833ZDgmzxbKmJm # dDabbzS0yGhngHa6+gwVaOUqcHf9w6kwxMo+OqG3QZIcwd5wHECs5rAJZ6PIyFM7 # Ad2hRUFHRTi353I7V4xEgYGuZb6qFx6Pf44i7AjXbptUolDcVzYEdgLQSWiuFajS # 6Xg3k7Cy8TiM5HPUK9LZInloTxuULSxJmJ7nTjUjOj5xwRmC7x2S/mxql8nvHSCN # 1OED2/wECOot6MEe9bL3nzoKwO8TNlEStq5scd25GA0gMQO+qNXV/xTDOBTJ8zBc # GQIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFLy2xe59sCE0SjycqE5Erb4YrS1gMB8G # A1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCG # Tmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUy # MFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4w # XAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2Vy # dHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwG # A1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQD # AgeAMA0GCSqGSIb3DQEBCwUAA4ICAQDhSEjSBFSCbJyl3U/QmFMW2eLPBknnlsfI # D/7gTMvANEnhq08I9HHbbqiwqDEHSvARvKtL7j0znICYBbMrVSmvgDxU8jAGqMyi # LoM80788So3+T6IZV//UZRJqBl4oM3bCIQgFGo0VTeQ6RzYL+t1zCUXmmpPmM4xc # ScVFATXj5Tx7By4ShWUC7Vhm7picDiU5igGjuivRhxPvbpflbh/bsiE5tx5cuOJE # JSG+uWcqByR7TC4cGvuavHSjk1iRXT/QjaOEeJoOnfesbOdvJrJdbm+leYLRI67N # 3cd8B/suU21tRdgwOnTk2hOuZKs/kLwaX6NsAbUy9pKsDmTyoWnGmyTWBPiTb2rp # 5ogo8Y8hMU1YQs7rHR5hqilEq88jF+9H8Kccb/1ismJTGnBnRMv68Ud2l5LFhOZ4 # nRtl4lHri+N1L8EBg7aE8EvPe8Ca9gz8sh2F4COTYd1PHce1ugLvvWW1+aOSpd8N # nwEid4zgD79ZQxisJqyO4lMWMzAgEeFhUm40FshtzXudAsX5LoCil4rLbHfwYtGO # pw9DVX3jXAV90tG9iRbcqjtt3vhW9T+L3fAZlMeraWfh7eUmPltMU8lEQOMelo/1 # ehkIGO7YZOHxUqeKpmF9QaW8LXTT090AHZ4k6g+tdpZFfCMotyG+E4XqN6ZWtKEB # QiE3xL27BDCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZI # hvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw # DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x # MjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAy # MDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMC # VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV # BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp # bWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC # AQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25Phdg # M/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPF # dvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6 # GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBp # Dco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50Zu # yjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3E # XzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0 # lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1q # GFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ # +QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PA # PBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkw # EgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxG # NSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARV # MFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWlj # cm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAK # BggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC # AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX # zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v # cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI # KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG # 9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0x # M7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmC # VgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449 # xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wM # nosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDS # PeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2d # Y3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxn # GSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+Crvs # QWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokL # jzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL # 6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNN # MIICNQIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp # bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw # b3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEn # MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjg2MDMtMDVFMC1EOTQ3MSUwIwYDVQQD # ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQD7 # n7Bk4gsM2tbU/i+M3BtRnLj096CBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w # IFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA6W/LXTAiGA8yMDI0MDIwODIxNTEy # NVoYDzIwMjQwMjA5MjE1MTI1WjB0MDoGCisGAQQBhFkKBAExLDAqMAoCBQDpb8td # AgEAMAcCAQACAhiGMAcCAQACAhM2MAoCBQDpcRzdAgEAMDYGCisGAQQBhFkKBAIx # KDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZI # hvcNAQELBQADggEBAGAvV6zb60A4zX+nZZKy49UjMnuEFe1BqyW3rOoU4oHUVYP7 # URZZTUF0j8rCsZPaJPv4sSPAG0DhPoxvq3+DmMz+THdUU8PZVU8KXXwSDmxzz+lu # Lx3KUOszvvcwCi1mmpW0sxK/ihlkWmEDyLfYAPcMuPH60sUe6Rt1c5pJYAHM4BAW # TIovxxMiIaNNS+IqA9JPdEQa93UtoVv8ZJBL5WjVL3m/6D/mtAv5OEG9cFvt7tU4 # /5S8umVjCaulc4loaLJe/1CPupgxcEQTChMVtO6Ymc2rEMFI8o42yVOPS2e3Qiq7 # 139D8btWDCPAT8vX5YQustqFompdf3UZmqAXkF4xggQNMIIECQIBATCBkzB8MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNy # b3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAfGzRfUn6MAW1gABAAAB8TAN # BglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8G # CSqGSIb3DQEJBDEiBCBNxFo+On23PyhRFWvPQEBJCUxhPfFzvkfXWUYSfG8SczCB # +gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EINV3/T5hS7ijwao466RosB7wwEib # t0a1P5EqIwEj9hF4MIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh # c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD # b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw # MTACEzMAAAHxs0X1J+jAFtYAAQAAAfEwIgQgoCCJooVIzMUHX5yLRYRDnfaBDGXd # 9LH2bX5fyXcn6fwwDQYJKoZIhvcNAQELBQAEggIApNBW0RtnQFmk1oISdouIDHZ2 # GLULfBIuh3GgAophfGhjlt2w7oa2lPkTbhClUyV5jqw+DJV8JHVQt6cNIhNarpag # P3BBB/qnezui0Y2hgiVhlL8PWYEq+tLu/6uvfyMG/Bsa1uZoich6qvFhXsbjq+Xn # +JxLUmDPej1yDMNsB9fOka99Zt5wMrAN8oE4Y5an6PofToW4qsPmuGDC0SeDRgby # ZCOOW4NWWnu4QJSHCnnkzIrMLrnJgTAbC2SNwSv6U51nEs56WBcjlNpv7UgbHJ2h # tl6I74DEdhlncWC+iqmSP4SqTUbGODjSP60A0O6/fktWrAaN99fhDK5Q7mE51mwT # e9KBHCymQMaOa4jvBrlnVNWX/uHEHk2+OxqBbRP21TFU3VEhZacVcZjBVWeETnrm # fa9RNTjTJFDQv71UcpEM1xu0ZLdZ127xaK9b3eyLMkkssXQ0CcGqvwzZz2PrIoLJ # Bvvf9oH7zVjvR1JmkzMYkek3U1tJdCkZfUxTc5dmEZ8eTSlWGvRQ0Ot1WLGQl3md # CKZYPIQz05L0qyLJau7uj1fZunacLxep/f0fm0zIt1ydYj27r0up2ME2mJRRYBoN # KKH6h1LQCmKpEQOgyx96yxuX943/z9RgLaqexDK4OjCHDF6MYdo4LzHniEHpBpT0 # gPMFBW+arketWPqw/3M= # SIG # End signature block |