Framework/Managers/PartialScanManager.ps1
Set-StrictMode -Version Latest class PartialScanManager : EventBase { hidden [string] $OrgName = $null; hidden [string] $ProjectName = $null; hidden [PSObject] $ScanPendingForResources = $null; hidden [string] $ResourceScanTrackerFileName=$null; hidden [PartialScanResourceMap] $ResourceScanTrackerObj = $null [PSObject] $ControlSettings; hidden [ActiveStatus] $ActiveStatus = [ActiveStatus]::NotStarted; hidden [string] $CAScanProgressSnapshotsContainerName = [Constants]::CAScanProgressSnapshotsContainerName hidden [string] $AzSKTempStatePath = (Join-Path $([Constants]::AzSKAppFolderPath) "TempState" | Join-Path -ChildPath "PartialScanData"); hidden [bool] $StoreResTrackerLocally = $false; hidden [string] $ScanSource = $null; hidden [bool] $IsRTFAlreadyAvailable = $false; hidden [bool] $IsDurableStorageFound = $false; hidden [string] $MasterFilePath; $StorageContext = $null; $ControlStateBlob = $null; hidden static $IsCsvUpdatedAtCheckpoint = $false; hidden static $CollatedSummaryCount = @(); # Matrix of counts for severity and control status hidden static $CollatedBugSummaryCount = @(); # Matrix of counts for severity and Bug status hidden static $ControlResultsWithBugSummary = @(); hidden static $ControlResultsWithSARIFSummary= @(); hidden static $ControlResultsWithClosedBugSummary= @(); hidden static $duplicateClosedBugCount=0; hidden [string] $SummaryMarkerText = "------"; hidden [string] $BackupControlStatePath = (Join-Path $([Constants]::AzSKAppFolderPath) "TempState" | Join-Path -ChildPath "BackupControlState"); hidden [string] $BackupControlStateFilePath; hidden [PSObject] $StateOfControlsToBeFixed = $null; hidden [bool] $IsControlStateBackupFetched = $false; hidden static [PartialScanManager] $Instance = $null; static [PartialScanManager] GetInstance([PSObject] $StorageAccount, [string] $OrganizationName) { if ( $null -eq [PartialScanManager]::Instance) { [PartialScanManager]::Instance = [PartialScanManager]::new($OrganizationName); } [PartialScanManager]::Instance.OrgName = $OrganizationName; return [PartialScanManager]::Instance } static [PartialScanManager] GetInstance() { if ( $null -eq [PartialScanManager]::Instance) { [PartialScanManager]::Instance = [PartialScanManager]::new(); } return [PartialScanManager]::Instance } static [void] ClearInstance() { [PartialScanManager]::Instance = $null [PartialScanManager]::IsCsvUpdatedAtCheckpoint = $false } PartialScanManager([string] $OrganizationName) { $this.ControlSettings = [ConfigurationManager]::LoadServerConfigFile("ControlSettings.json"); $this.OrgName = $OrganizationName; if ([string]::isnullorwhitespace($this.ResourceScanTrackerFileName)) { if([ConfigurationManager]::GetAzSKSettings().IsCentralScanModeOn) { $this.ResourceScanTrackerFileName = Join-Path $OrganizationName $([Constants]::ResourceScanTrackerCMBlobName) } else { $this.ResourceScanTrackerFileName = Join-Path $OrganizationName $([Constants]::ResourceScanTrackerBlobName) } } $this.GetResourceScanTrackerObject(); } PartialScanManager() { $this.ControlSettings = [ConfigurationManager]::LoadServerConfigFile("ControlSettings.json"); if ([string]::isnullorwhitespace($this.ResourceScanTrackerFileName)) { $this.ResourceScanTrackerFileName = [Constants]::ResourceScanTrackerBlobName } $this.GetResourceScanTrackerObject(); } hidden [void] GetResourceTrackerFile($orgName, $isControlFixCmd) { $this.ScanSource = [AzSKSettings]::GetInstance().GetScanSource(); $this.OrgName = $orgName #Validating the configuration of storing resource tracker file if($null -ne $this.ControlSettings.PartialScan) { $this.StoreResTrackerLocally = [Bool]::Parse($this.ControlSettings.PartialScan.StoreResourceTrackerLocally); } #Use local Resource Tracker files for partial scanning if ($this.StoreResTrackerLocally -and ($this.ScanSource -ne "CA" -and $this.ScanSource -ne "CICD") ) { if($null -eq $this.ScanPendingForResources) { if($isControlFixCmd) { $this.ResourceScanTrackerFileName = "ControlFix"+ $this.ResourceScanTrackerFileName } if(![string]::isnullorwhitespace($this.OrgName)){ if(Test-Path (Join-Path (Join-Path $this.AzSKTempStatePath $this.OrgName) $this.ResourceScanTrackerFileName)) { $this.ScanPendingForResources = Get-Content (Join-Path (Join-Path $this.AzSKTempStatePath $this.OrgName) $this.ResourceScanTrackerFileName) -Raw } $this.MasterFilePath = (Join-Path (Join-Path $this.AzSKTempStatePath $this.OrgName) $this.ResourceScanTrackerFileName) } else { $this.MasterFilePath = (Join-Path $this.AzSKTempStatePath $this.ResourceScanTrackerFileName) } } } if ($this.ScanSource -eq "CA") # use storage in ADOScannerRG in case of CA scan { $this.MasterFilePath = (Join-Path (Join-Path $this.AzSKTempStatePath $this.OrgName) $this.ResourceScanTrackerFileName) 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 $containerObject = Get-AzStorageContainer -Context $this.StorageContext -Name $this.CAScanProgressSnapshotsContainerName -ErrorAction SilentlyContinue #If checkpoint container is found then get ResourceTracker.json (if exists) if($null -ne $containerObject) { $this.ControlStateBlob = Get-AzStorageBlob -Container $this.CAScanProgressSnapshotsContainerName -Context $this.StorageContext -Blob (Join-Path $this.OrgName.ToLower() $this.ResourceScanTrackerFileName) -ErrorAction SilentlyContinue #If controlStateBlob is null then it will get created when we first write the resource tracker file to storage #If its not null this means Resource tracker file has been found in storage and will be used to continue pending scan if ($null -ne $this.ControlStateBlob) { if ($null -ne $this.MasterFilePath) { if (-not (Test-Path $this.MasterFilePath)) { $filePath = $this.MasterFilePath.Replace($this.ResourceScanTrackerFileName, "") New-Item -ItemType Directory -Path $filePath New-Item -Path $filePath -Name $this.ResourceScanTrackerFileName -ItemType "file" } #Copy existing RTF locally to handle any non ascii characters as ICloudBlob.DownloadText() was inserting non ascii charcaters Get-AzStorageBlobContent -CloudBlob $this.ControlStateBlob.ICloudBlob -Context $this.StorageContext -Destination $this.MasterFilePath -Force $this.ScanPendingForResources = Get-ChildItem -Path $this.MasterFilePath -Force | Get-Content | ConvertFrom-Json #Delete the local RTF file Remove-Item -Path (Join-Path (Join-Path $this.AzSKTempStatePath $this.OrgName) $this.ResourceScanTrackerFileName) } $this.IsRTFAlreadyAvailable = $true } else { $this.IsRTFAlreadyAvailable = $false } $this.IsDurableStorageFound = $true } #If checkpoint container is not found then create new else { $containerObject = New-AzStorageContainer -Name $this.CAScanProgressSnapshotsContainerName -Context $this.StorageContext -ErrorAction SilentlyContinue if ($null -ne $containerObject ) { $this.IsDurableStorageFound = $true } else { $this.PublishCustomMessage("Could not find/create partial scan container in storage.", [MessageType]::Warning) } } } catch { $this.PublishCustomMessage("Exception when trying to find/create partial scan container: $_.", [MessageType]::Warning) #Eat exception } } elseif ($this.ScanSource -eq "CICD") # use extension storage in case of CICD partial scan { if(![string]::isnullorwhitespace($this.OrgName)) { $rmContext = [ContextHelper]::GetCurrentContext(); $user = ""; $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user,$rmContext.AccessToken))) $uri= ""; if (Test-Path env:partialScanURI) { #Uri is created in cicd task based on jobid $uri = $env:partialScanURI } else { $uri = [Constants]::StorageUri -f $this.OrgName, $this.OrgName, "ResourceTrackerFile" } try { $webRequestResult = Invoke-RestMethod -Uri $uri -Method Get -ContentType "application/json" -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)} $this.ScanPendingForResources = $webRequestResult.value | ConvertFrom-Json $this.IsRTFAlreadyAvailable = $true; } catch { $this.ScanPendingForResources = $null $this.IsRTFAlreadyAvailable = $false; } } } } #Update resource status in ResourceMapTable object [void] UpdateResourceStatus([string] $resourceId, [ScanState] $state) { $resourceValues = @(); #$this.GetResourceScanTrackerObject(); if($this.IsListAvailableAndActive()) { $resourceValue = $this.ResourceScanTrackerObj.ResourceMapTable | Where-Object { $_.Id -eq $resourceId}; if($null -ne $resourceValue) { $resourceValue.ModifiedDate = [DateTime]::UtcNow; $resourceValue.State = $state; } else { $resourceValue = [PartialScanResource]@{ Id = $resourceId; State = $state; ScanRetryCount = 1; CreatedDate = [DateTime]::UtcNow; ModifiedDate = [DateTime]::UtcNow; } $this.ResourceScanTrackerObj.ResourceMapTable +=$resourceValue; } } } [void] UpdateResourceScanRetryCount([string] $resourceId) { $resourceValues = @(); if($this.IsListAvailableAndActive()) { $resourceValue = $this.ResourceScanTrackerObj.ResourceMapTable | Where-Object { $_.Id -eq $resourceId}; if($null -ne $resourceValue) { $resourceValue.ModifiedDate = [DateTime]::UtcNow; $resourceValue.ScanRetryCount = $resourceValue.ScanRetryCount + 1; if($resourceValue.ScanRetryCount -ge [Constants]::PartialScanMaxRetryCount) { $resourceValue.State = [ScanState]::ERR } } else { #do nothing } } } # Method to remove obsolete Resource Tracker file [void] RemovePartialScanData() { if ($this.ScanSource -eq "CICD") { if($null -ne $this.ResourceScanTrackerObj) { $rmContext = [ContextHelper]::GetCurrentContext(); $user = ""; $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user,$rmContext.AccessToken))) $uri =""; if (Test-Path env:partialScanURI) { #Uri is created by cicd task based on jobid $uri = $env:partialScanURI } else { $uri = [Constants]::StorageUri -f $this.OrgName, $this.OrgName, "ResourceTrackerFile" } try { if ($this.ResourceScanTrackerObj.ResourceMapTable -ne $null){ $webRequestResult = Invoke-WebRequest -Uri $uri -Method Delete -ContentType "application/json" -Headers @{Authorization = ("Basic {0}" -f $base64AuthInfo) } $this.ResourceScanTrackerObj = $null } } catch { #do nothing } } } elseif ($this.ScanSource -eq "CA" -and $this.IsDurableStorageFound) { #Move resource tracker file to archive folder if($null -ne $this.ControlStateBlob) { $archiveName = "Checkpoint_" +(Get-Date).ToUniversalTime().ToString("yyyyMMddHHmmss") + ".json"; #Store final RTF file locally and then upload to archive folder [JsonHelper]::ConvertToJsonCustom($this.ResourceScanTrackerObj) | Out-File $this.MasterFilePath -Force Set-AzStorageBlobContent -File $this.MasterFilePath -Container $this.CAScanProgressSnapshotsContainerName -Blob (Join-Path $this.OrgName.ToLower() (Join-Path "Archive" $archiveName)) -BlobType Block -Context $this.StorageContext -Force Remove-AzStorageBlob -CloudBlob $this.ControlStateBlob.ICloudBlob -Force -Context $this.StorageContext #Delete local RTF file if (Test-Path (Join-Path $this.AzSKTempStatePath $this.OrgName)) { Remove-Item -Path (Join-Path $this.AzSKTempStatePath $this.OrgName) -Recurse } } } #Use local Resource Tracker files for partial scanning elseif ($this.StoreResTrackerLocally) { if($null -ne $this.ResourceScanTrackerObj) { if(![string]::isnullorwhitespace($this.OrgName)){ if(Test-Path (Join-Path $this.AzSKTempStatePath $this.OrgName)) { Remove-Item -Path (Join-Path (Join-Path $this.AzSKTempStatePath $this.OrgName) $this.ResourceScanTrackerFileName) <#Create archive folder if not exists if(-not (Test-Path (Join-Path (Join-Path $this.AzSKTempStatePath $this.OrgName) "archive"))) { New-Item -ItemType Directory -Path (Join-Path (Join-Path $this.AzSKTempStatePath $this.OrgName) "archive") } $timestamp =(Get-Date -format "yyMMddHHmmss") Move-Item -Path (Join-Path (Join-Path $this.AzSKTempStatePath $this.OrgName) $this.ResourceScanTrackerFileName) -Destination (Join-Path (Join-Path (Join-Path $this.AzSKTempStatePath $this.OrgName) "archive")"Checkpoint_$($timestamp)") #> } } $this.ResourceScanTrackerObj = $null } } } #Method to fetch all applicable resources as per input command (including those with "COMP" status in ResourceTracker file) [void] CreateResourceMasterList([PSObject] $resourceIds) { if(($resourceIds | Measure-Object).Count -gt 0) { [System.Collections.Generic.List[PartialScanResource]] $resourceIdMap = @(); $progressCount=1 $resourceIds | ForEach-Object { $resourceValue = [PartialScanResource]@{ Id = $_.ResourceId; State = [ScanState]::INIT; ScanRetryCount = 0; CreatedDate = [DateTime]::UtcNow; ModifiedDate = [DateTime]::UtcNow; Name=$_.ResourceName; ProjectName = $_.ResourceGroupName #ResourceDetails=$_.ResourceDetails } #We dont need to store project name if -dnrr not given or the resource is not release/agentpool if($PSCmdlet.MyInvocation.BoundParameters['DoNotRefetchResources']){ if($_.ResourceType -ne "ADO.Release" -and $_.ResourceType -ne "ADO.AgentPool"){ $resourceValue = $resourceValue | Select-Object -Property * -ExcludeProperty ProjectName } } else { $resourceValue = $resourceValue | Select-Object -Property * -ExcludeProperty ProjectName } #$resourceIdMap.Add($hashId,$resourceValue); $resourceIdMap.Add([PartialScanResource] $resourceValue) if ($progressCount%100 -eq 0) { Write-Progress -Activity "Tracking $($progressCount) of $($resourceIds.Count) untracked resources " -Status "Progress: " -PercentComplete ($progressCount / $resourceIds.Count * 100) } $progressCount++; } Write-Progress -Activity "Tracked all resources" -Status "Ready" -Completed $masterControlBlob = [PartialScanResourceMap]@{ Id = [DateTime]::UtcNow.ToString("yyyyMMdd_HHmmss"); CreatedDate = [DateTime]::UtcNow; ResourceMapTable = $resourceIdMap; } if ($this.ScanPendingForResources -ne $null -and $this.ScanSource -eq "CICD"){ if([Helpers]::CheckMember($this.ScanPendingForResources.ResourceMapTable,"value")) { $this.ResourceScanTrackerObj = [PartialScanResourceMap]@{ Id = $this.ScanPendingForResources.Id; CreatedDate = $this.ScanPendingForResources.CreatedDate; ResourceMapTable = $this.ScanPendingForResources.ResourceMapTable.value; } } else{ $this.ResourceScanTrackerObj = [PartialScanResourceMap]@{ Id = $this.ScanPendingForResources.Id; CreatedDate = $this.ScanPendingForResources.CreatedDate; ResourceMapTable = $this.ScanPendingForResources.ResourceMapTable; } } } else{ $this.ResourceScanTrackerObj = $masterControlBlob; } if ($this.ScanSource -eq "CICD" -or $this.ScanSource -eq "CA") { $this.WriteToDurableStorage(); } else { $this.WriteToResourceTrackerFile(); } $this.ActiveStatus = [ActiveStatus]::Yes; } } [void] WriteToResourceTrackerFile() { if ($this.StoreResTrackerLocally) { if($null -ne $this.ResourceScanTrackerObj) { if(![string]::isnullorwhitespace($this.OrgName)){ if(-not (Test-Path (Join-Path $this.AzSKTempStatePath $this.OrgName))) { New-Item -ItemType Directory -Path (Join-Path $this.AzSKTempStatePath $this.OrgName) -ErrorAction Stop | Out-Null } } else{ if(-not (Test-Path "$this.AzSKTempStatePath")) { New-Item -ItemType Directory -Path "$this.AzSKTempStatePath" -ErrorAction Stop | Out-Null } } $this.PublishCustomMessage("Updating resource tracker file", [MessageType]::Warning) [JsonHelper]::ConvertToJsonCustom($this.ResourceScanTrackerObj) | Out-File $this.MasterFilePath -Force $this.PublishCustomMessage("Resource tracker file updated", [MessageType]::Warning) } } } [void] WriteToDurableStorage() { if ($this.ScanSource -eq "CICD") { if($null -ne $this.ResourceScanTrackerObj) { if(![string]::isnullorwhitespace($this.OrgName)) { $rmContext = [ContextHelper]::GetCurrentContext(); $user = ""; $uri = ""; $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user,$rmContext.AccessToken))) $scanObject = $this.ResourceScanTrackerObj | ConvertTo-Json $body = ""; if (Test-Path env:partialScanURI) { $uri = $env:partialScanURI $JobId =""; $JobId = $uri.Replace('?','/').Split('/')[$JobId.Length -2] if ($this.IsRTFAlreadyAvailable -eq $true){ $body = @{"id" = $Jobid; "__etag"=-1; "value"= $scanObject;} | ConvertTo-Json } else{ $body = @{"id" = $Jobid; "value"= $scanObject;} | ConvertTo-Json } } else { $uri = [Constants]::StorageUri -f $this.OrgName, $this.OrgName, "ResourceTrackerFile" if ($this.IsRTFAlreadyAvailable -eq $true){ $body = @{"id" = "ResourceTrackerFile";"__etag"=-1; "value"= $scanObject;} | ConvertTo-Json } else{ $body = @{"id" = "ResourceTrackerFile"; "value"= $scanObject;} | ConvertTo-Json } } try { $webRequestResult = Invoke-WebRequest -Uri $uri -Method Put -ContentType "application/json" -Headers @{Authorization = ("Basic {0}" -f $base64AuthInfo) } -Body $body $this.IsRTFAlreadyAvailable = $true; } catch { $this.PublishCustomMessage("Could not update resource tracker file.", [MessageType]::Warning); } } } } elseif ($this.ScanSource -eq "CA" -and $this.IsDurableStorageFound) { if ($this.IsRTFAlreadyAvailable) # Copy RTF from memory { $this.ControlStateBlob.ICloudBlob.UploadText([JsonHelper]::ConvertToJsonCustom($this.ResourceScanTrackerObj) ) } else { # If file is not available in storage then upload it from local for the first instance if ($null -ne $this.MasterFilePath -and -not (Test-Path $this.MasterFilePath)) { # Create directory and resource tracker file $filePath = $this.MasterFilePath.Replace($this.ResourceScanTrackerFileName, "") if (-not (Test-Path $filePath)) { New-Item -ItemType Directory -Path $filePath } New-Item -Path $filePath -Name $this.ResourceScanTrackerFileName -ItemType "file" } [JsonHelper]::ConvertToJsonCustom($this.ResourceScanTrackerObj) | Out-File $this.MasterFilePath -Force Set-AzStorageBlobContent -File $this.MasterFilePath -Container $this.CAScanProgressSnapshotsContainerName -Blob (Join-Path $this.OrgName.ToLower() $this.ResourceScanTrackerFileName) -BlobType Block -Context $this.StorageContext -Force $this.ControlStateBlob = Get-AzStorageBlob -Container $this.CAScanProgressSnapshotsContainerName -Context $this.StorageContext -Blob (Join-Path $this.OrgName.ToLower() $this.ResourceScanTrackerFileName) -ErrorAction SilentlyContinue $this.IsRTFAlreadyAvailable = $true } } } #Method to fetch ResourceTrackerFile as an object hidden [void] GetResourceScanTrackerObject() { try { if($null -eq $this.ScanPendingForResources) { return; } if ($this.ScanSource -eq "CICD") # use extension storage in case of CICD partial scan { if(![string]::isnullorwhitespace($this.ScanPendingForResources)) { if([Helpers]::CheckMember($this.ScanPendingForResources.ResourceMapTable,"value")) { $this.ResourceScanTrackerObj = [PartialScanResourceMap]@{ Id = $this.ScanPendingForResources.Id; CreatedDate = $this.ScanPendingForResources.CreatedDate; ResourceMapTable = $this.ScanPendingForResources.ResourceMapTable.value; } } else{ $this.ResourceScanTrackerObj = [PartialScanResourceMap]@{ Id = $this.ScanPendingForResources.Id; CreatedDate = $this.ScanPendingForResources.CreatedDate; ResourceMapTable = $this.ScanPendingForResources.ResourceMapTable; } } } } elseif ($this.ScanSource -eq "CA") { if(![string]::isnullorwhitespace($this.ScanPendingForResources)) { $this.ResourceScanTrackerObj = $this.ScanPendingForResources } } elseif ($this.StoreResTrackerLocally) { if(![string]::isnullorwhitespace($this.OrgName)){ if(-not (Test-Path (Join-Path $this.AzSKTempStatePath $this.OrgName))) { New-Item -ItemType Directory -Path (Join-Path $this.AzSKTempStatePath $this.OrgName) -ErrorAction Stop | Out-Null } } else{ if(-not (Test-Path "$this.AzSKTempStatePath")) { New-Item -ItemType Directory -Path "$this.AzSKTempStatePath" -ErrorAction Stop | Out-Null } } $this.ResourceScanTrackerObj = Get-content $this.MasterFilePath | ConvertFrom-Json } } catch{ $this.ResourceScanTrackerObj = $null $this.ScanPendingForResources = $null $this.PublishCustomMessage("RTF not found", [MessageType]::Warning); } } #Sending $isControlFixCmd as true in case set-azskadosecuritystatus command is used in order to store RTF in separate folder, so that it does not interfere with GADS command [ActiveStatus] IsPartialScanInProgress($orgName, $isControlFixCmd) { $this.GetResourceTrackerFile($orgName, $isControlFixCmd); if($null -ne $this.ControlSettings.PartialScan) { $resourceTrackerFileValidforDays = [Int32]::Parse($this.ControlSettings.PartialScan.ResourceTrackerValidforDays); $this.GetResourceScanTrackerObject(); if($null -eq $this.ResourceScanTrackerObj) { return $this.ActiveStatus = [ActiveStatus]::No; } $shouldStopScanning = ($this.ResourceScanTrackerObj.ResourceMapTable | Where-Object {$_.State -notin ([ScanState]::COMP,[ScanState]::ERR)} | Measure-Object).Count -eq 0 if($this.ResourceScanTrackerObj.CreatedDate.AddDays($resourceTrackerFileValidforDays) -lt [DateTime]::UtcNow -or $shouldStopScanning) { $this.RemovePartialScanData(); $this.ScanPendingForResources = $null; return $this.ActiveStatus = [ActiveStatus]::No; } return $this.ActiveStatus = [ActiveStatus]::Yes } else { $this.ScanPendingForResources = $null; return $this.ActiveStatus = [ActiveStatus]::No; } } [PSObject] GetNonScannedResources() { #[System.Collections.Generic.List[PartialScanResource]] $nonScannedResources = @(); $nonScannedResources = @() $this.GetResourceScanTrackerObject(); if($this.IsListAvailableAndActive()) { $nonScannedResources +=[PartialScanResource[]] $this.ResourceScanTrackerObj.ResourceMapTable | Where-Object {$_.State -eq [ScanState]::INIT} return [PartialScanResource[]] $nonScannedResources; } return $null; } [PSObject] GetAllListedResources() { #[System.Collections.Generic.List[PartialScanResource]] $nonScannedResources = @(); $nonScannedResources = @() $this.GetResourceScanTrackerObject(); if($this.IsListAvailableAndActive()) { $nonScannedResources +=[PartialScanResource[]] $this.ResourceScanTrackerObj.ResourceMapTable return [PartialScanResource[]] $nonScannedResources; } return $null; } [Bool] IsListAvailableAndActive() { if($null -ne $this.ResourceScanTrackerObj -and $this.ActiveStatus -eq [ActiveStatus]::Yes -and $null -ne $this.ResourceScanTrackerObj.ResourceMapTable) { return $true } else { return $false } } # Collect control results summary data and append to it at every checkpoint. Any changes in this method should be synced with WritePSConsole.ps1 PrintSummaryData method [void] CollateSummaryData($event) { $summary = @($event | select-object @{Name="VerificationResult"; Expression = {$_.ControlResults.VerificationResult}},@{Name="ControlSeverity"; Expression = {$_.ControlItem.ControlSeverity}}) if(($summary | Measure-Object).Count -ne 0) { $severities = @(); $severities += $summary | Select-Object -Property ControlSeverity | Select-Object -ExpandProperty ControlSeverity -Unique; $verificationResults = @(); $verificationResults += $summary | Select-Object -Property VerificationResult | Select-Object -ExpandProperty VerificationResult -Unique; if($severities.Count -ne 0) { # Create summary matrix $totalText = "Total"; $MarkerText = "MarkerText"; $rows = @(); $rows += $severities; $rows += $MarkerText; $rows += $totalText; $rows += $MarkerText; #Execute below block only once (when first resource is scanned) if([PartialScanManager]::CollatedSummaryCount.Count -eq 0) { $rows | ForEach-Object { $result = [PSObject]::new(); Add-Member -InputObject $result -Name "Summary" -MemberType NoteProperty -Value $_.ToString() Add-Member -InputObject $result -Name $totalText -MemberType NoteProperty -Value 0 #Get all possible verificationResults initially [Enum]::GetNames([VerificationResult]) | ForEach-Object { Add-Member -InputObject $result -Name $_.ToString() -MemberType NoteProperty -Value 0 }; [PartialScanManager]::CollatedSummaryCount += $result; }; } $totalRow = [PartialScanManager]::CollatedSummaryCount | Where-Object { $_.Summary -eq $totalText } | Select-Object -First 1; $summary | Group-Object -Property ControlSeverity | ForEach-Object { $item = $_; $summaryItem = [PartialScanManager]::CollatedSummaryCount | Where-Object { $_.Summary -eq $item.Name } | Select-Object -First 1; if($summaryItem) { $summaryItem.Total += $_.Count; if($totalRow) { $totalRow.Total += $_.Count } $item.Group | Group-Object -Property VerificationResult | ForEach-Object { $propName = $_.Name; $summaryItem.$propName += $_.Count; if($totalRow) { $totalRow.$propName += $_.Count } }; } }; $markerRows = [PartialScanManager]::CollatedSummaryCount | Where-Object { $_.Summary -eq $MarkerText } $markerRows | ForEach-Object { $markerRow = $_ Get-Member -InputObject $markerRow -MemberType NoteProperty | ForEach-Object { $propName = $_.Name; $markerRow.$propName = $this.SummaryMarkerText; } }; } } } # Collect Bug summary data and append to it at every checkpoint. Any changes in this method should be synced with WritePSConsole.ps1 PrintBugSummaryData method [void] CollateBugSummaryData($event){ #gather all control results that have failed/verify as their control result #obtain their control severities $event | ForEach-Object { $item = $_ if ($item -and $item.ControlResults -and ($item.ControlResults[0].VerificationResult -eq "Failed" -or $item.ControlResults[0].VerificationResult -eq "Verify")) { $item $item.ControlResults[0].Messages | ForEach-Object{ if($_.Message -eq "New Bug" -or $_.Message -eq "Active Bug" -or $_.Message -eq "Resolved Bug"){ [PartialScanManager]::CollatedBugSummaryCount += [PSCustomObject]@{ BugStatus=$_.Message ControlSeverity = $item.ControlItem.ControlSeverity; }; } }; #Collecting control results where bug has been found (new/active/resolved). This is used to generate BugSummary at the end of scan [PartialScanManager]::ControlResultsWithBugSummary += $item } }; } # Collect Closed Bugs summary data and append to it at every checkpoint. Any changes in this method should be synced with WritePSConsole.ps1 PrintBugSummaryData method [void] CollateClosedBugSummaryData($event){ #gather all control results that have passed as their control result #obtain their control severities $TotalWorkItemCount=0; $TotalControlsClosedCount=0; $event | ForEach-Object { $item = $_ if ($item -and $item.ControlResults) { $TotalControlsClosedCount+=1; # If two bugs are logged against same resource and control in different project, message will contain closed bug twice with different urls $item.ControlResults[0].Messages | ForEach-Object{ if($_.Message -eq "Closed Bug"){ # CollatedBugSummaryCount is used for PS Console summary printing [PartialScanManager]::CollatedBugSummaryCount += [PSCustomObject]@{ BugStatus=$_.Message ControlSeverity = $item.ControlItem.ControlSeverity; }; $TotalWorkItemCount+=1 } }; #Collecting control results where closed bug has been found. This is used to generate BugSummary at the end of scan [PartialScanManager]::ControlResultsWithClosedBugSummary += $item } }; [PartialScanManager]::duplicateClosedBugCount+=($TotalWorkItemCount-$TotalControlsClosedCount) } # Write to csv and append to it at every checkpoint. Any changes in this method should be synced with WriteSummaryFile.ps1 WriteToCSV method [void] WriteToCSV([SVTEventContext[]] $arguments, $FilePath) { if ([string]::IsNullOrEmpty($FilePath)) { return; } [CsvOutputItem[]] $csvItems = @(); $anyAttestedControls = $null -ne ($arguments | Where-Object { $null -ne ($_.ControlResults | Where-Object { $_.AttestationStatus -ne [AttestationStatus]::None } | Select-Object -First 1) } | Select-Object -First 1); $arguments | ForEach-Object { $item = $_ if ($item -and $item.ControlResults) { $item.ControlResults | ForEach-Object{ $csvItem = [CsvOutputItem]@{ ControlID = $item.ControlItem.ControlID; ControlSeverity = $item.ControlItem.ControlSeverity; Description = $item.ControlItem.Description; FeatureName = $item.FeatureName; Recommendation = $item.ControlItem.Recommendation; Rationale = $item.ControlItem.Rationale; AdditionalInfo = $_.AdditionalInfoInCSV }; if($_.VerificationResult -ne [VerificationResult]::NotScanned) { $csvItem.Status = $_.VerificationResult.ToString(); } if($item.ControlItem.IsBaselineControl) { $csvItem.IsBaselineControl = "Yes"; } else { $csvItem.IsBaselineControl = "No"; } if($anyAttestedControls) { $csvItem.ActualStatus = $_.ActualVerificationResult.ToString(); } if($item.IsResource()) { $csvItem.ResourceName = $item.ResourceContext.ResourceName; $csvItem.ResourceGroupName = $item.ResourceContext.ResourceGroupName; try { if($item.ResourceContext.ResourceDetails -ne $null -and ([Helpers]::CheckMember($item.ResourceContext.ResourceDetails,"ResourceLink"))) { $csvItem.ResourceLink = $item.ResourceContext.ResourceDetails.ResourceLink; } } catch { $_ } $csvItem.ResourceId = $item.ResourceContext.ResourceId; $csvItem.DetailedLogFile = "/$([Helpers]::SanitizeFolderName($item.ResourceContext.ResourceGroupName))/$($item.FeatureName).LOG"; } else { $csvItem.ResourceId = $item.OrganizationContext.scope; $csvItem.DetailedLogFile = "/$([Helpers]::SanitizeFolderName($item.OrganizationContext.OrganizationName))/$($item.FeatureName).LOG" } if($_.AttestationStatus -ne [AttestationStatus]::None) { $csvItem.AttestedSubStatus = $_.AttestationStatus.ToString(); if($null -ne $_.StateManagement -and $null -ne $_.StateManagement.AttestedStateData) { $csvItem.AttesterJustification = $_.StateManagement.AttestedStateData.Justification $csvItem.AttestedBy = $_.StateManagement.AttestedStateData.AttestedBy if(![string]::IsNullOrWhiteSpace($_.StateManagement.AttestedStateData.ExpiryDate)) { $csvItem.AttestationExpiryDate = $_.StateManagement.AttestedStateData.ExpiryDate } if(![string]::IsNullOrWhiteSpace($_.StateManagement.AttestedStateData.AttestedDate)) { $csvItem.AttestedOn= $_.StateManagement.AttestedStateData.AttestedDate } } } <#if($_.IsControlInGrace -eq $true) { $csvItem.IsControlInGrace = "Yes" } else { $csvItem.IsControlInGrace = "No" }#> $csvItems += $csvItem; } } } if ($csvItems.Count -gt 0) { # Remove Null properties $nonNullProps = @(); $nonNullProps = [CsvOutputItem].GetMembers() | Where-Object { $_.MemberType -eq [System.Reflection.MemberTypes]::Property }| Select-object -Property Name ($csvItems | Select-Object -Property $nonNullProps.Name -ExcludeProperty SupportsAutoFix,ChildResourceName,IsPreviewBaselineControl,UserComments ) | Group-Object -Property FeatureName | Foreach-Object {$_.Group | Export-Csv -Path $FilePath -append -NoTypeInformation} [PartialScanManager]::IsCsvUpdatedAtCheckpoint = $true } } [void] CollateSARIFData($event) { $event | ForEach-Object { $item = $_ if ($item -and $item.ControlResults -and ($item.ControlResults[0].VerificationResult -eq "Failed" -or $item.ControlResults[0].VerificationResult -eq "Verify")) { #Collecting Failed and verify controls [PartialScanManager]::ControlResultsWithSARIFSummary += $item } }; } [void] FetchControlStateBackup($InternalId) { $this.BackupControlStateFilePath = (Join-Path $this.BackupControlStatePath $this.OrgName) if($InternalId -match "Organization") { if(-not (Test-Path $this.BackupControlStateFilePath)) { New-Item -ItemType Directory -Path $this.BackupControlStateFilePath -ErrorAction Stop | Out-Null } else { $this.StateOfControlsToBeFixed += Get-Content (Join-Path $this.BackupControlStateFilePath "$InternalId + '.Json'") -Raw | ConvertFrom-Json } } else { # validate org level folder exists if(-not (Test-Path $this.BackupControlStateFilePath)) { New-Item -ItemType Directory -Path $this.BackupControlStateFilePath -ErrorAction Stop | Out-Null } $this.BackupControlStateFilePath = (Join-Path $this.BackupControlStateFilePath $this.ProjectName) if(-not (Test-Path $this.BackupControlStateFilePath)) { New-Item -ItemType Directory -Path $this.BackupControlStateFilePath -ErrorAction Stop | Out-Null } else { $this.StateOfControlsToBeFixed += Get-Content (Join-Path $this.BackupControlStateFilePath "$($InternalId + '.Json')") -Raw | ConvertFrom-Json } } $this.IsControlStateBackupFetched = $true } [void] WriteControlFixDataObject($results) { if ($this.ScanSource -eq "SDL" -or $this.ScanSource -eq "") { $scannedby = [ContextHelper]::GetCurrentSessionUser(); $date = [DateTime]::UtcNow; $applicableControls = @() $controlsDataObject = @(); if (($results | measure-object).Count -gt 0) { if ($results[0].FeatureName -eq "Project") { $controlsDataObject = @($results | Where-Object {$_.ControlItem.Tags -contains 'AutomatedFix' -and ($_.ControlResults.VerificationResult -eq 'Failed' -or $_.ControlResults.VerificationResult -eq 'Verify') -and $null -ne $_.ControlResults.BackupControlState} ` | Select-Object @{Name="ProjectName"; Expression={$_.ResourceContext.ResourceName}}, @{Name="ResourceName"; Expression={$_.ResourceContext.ResourceName}}, @{Name="ResourceId"; Expression={$_.ResourceContext.ResourceId}}, @{Name="InternalId"; Expression={$_.ControlItem.id}}, @{Name="DataObject"; Expression={$_.ControlResults.BackupControlState}}); } else { $controlsDataObject = @($results | Where-Object {$_.ControlItem.Tags -contains 'AutomatedFix' -and ($_.ControlResults.VerificationResult -eq 'Failed' -or $_.ControlResults.VerificationResult -eq 'Verify') -and $null -ne $_.ControlResults.BackupControlState} ` | Select-Object @{Name="ProjectName"; Expression={$_.ResourceContext.ResourceGroupName}}, @{Name="ResourceName"; Expression={$_.ResourceContext.ResourceName}}, @{Name="ResourceId"; Expression={$_.ResourceContext.ResourceId}}, @{Name="InternalId"; Expression={$_.ControlItem.id}}, @{Name="DataObject"; Expression={$_.ControlResults.BackupControlState}}); } } if($null -ne $controlsDataObject -and $controlsDataObject.Count -gt 0) { $controlsDataObject | Add-Member -NotePropertyName ScannedBy -NotePropertyValue $scannedBy $controlsDataObject | Add-Member -NotePropertyName Date -NotePropertyValue $date if(-not $this.IsControlStateBackupFetched) { $this.ProjectName = ($controlsDataObject | Select-Object -Property ProjectName -Unique).ProjectName $this.ProjectName = $this.ProjectName.Trim() $this.FetchControlStateBackup($controlsDataObject[0].InternalId); } $controlsDataObject = @($controlsDataObject) if($controlsDataObject.Count -gt 0) { $fileName = $controlsDataObject[0].InternalId + ".json" if($null -ne $this.StateOfControlsToBeFixed) { $existingDataObj = $this.StateOfControlsToBeFixed | where-Object {$_.ResourceId -in $controlsDataObject.ResourceId} if (($existingDataObj | Measure-Object).Count -gt 0) { $this.StateOfControlsToBeFixed = @($this.StateOfControlsToBeFixed | where-Object {$_ -notin $existingDataObj}) } } $applicableControls += $controlsDataObject | select-object -property Date,ResourceId,ResourceName,DataObject,ScannedBy $this.StateOfControlsToBeFixed += $applicableControls [JsonHelper]::ConvertToJsonCustom($this.StateOfControlsToBeFixed) | Out-File (Join-Path $this.BackupControlStateFilePath $fileName) -Force } } } } } # SIG # Begin signature block # MIIoKgYJKoZIhvcNAQcCoIIoGzCCKBcCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAy3lzkth8SP/7L # 8pN6F9nfHdsP27vJIAHV8gLpaUoTMKCCDXYwggX0MIID3KADAgECAhMzAAADrzBA # 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 # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIGT1zsyPq62vQ7V5shwchnD2 # yWbO7qhSD7CnQ1Dml0FjMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A # cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB # BQAEggEApGd55pVToFRTJDomAuSqDaBLEzkpIrPIIejHXhkHRW6Y7jGD3wiMoToP # GONn0yoj7gLPBf5IEfCCgSrocEcKwi+dGu+XAvZXoCTqdsvjkGZ/SOFMbZ2e1Wtj # 37S2ZiJ+sHcNICGvKSlrpNgRabznl12zShF98Y9d2BzvkjN0WOT6IJ8dSJEplllW # jP+KbcVePU5RkWemjU45uqxxCP/l6dM4fTOWEfN+LuCrYT7IEm2b64Ud2x5mD39v # Ef84Xw5QCBFlOO8sL33VqmF9AmB0whvNfHnmECvyYKlipstcamqrEzvvvC/nW3uD # j88DpSoPF+ZtmaDsZNUjRx1pf93r1aGCF5QwgheQBgorBgEEAYI3AwMBMYIXgDCC # F3wGCSqGSIb3DQEHAqCCF20wghdpAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsq # hkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl # AwQCAQUABCB0azXYjpCFG3qsOkZnsXJ+vaRaIk02xZoUalB8IKFCLAIGZeenjg+P # GBMyMDI0MDMwNzA3Mzc1MC42MTVaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l # cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046N0YwMC0w # NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wg # ghHqMIIHIDCCBQigAwIBAgITMwAAAfAqfB1ZO+YfrQABAAAB8DANBgkqhkiG9w0B # AQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD # VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yMzEyMDYxODQ1 # NTFaFw0yNTAzMDUxODQ1NTFaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz # aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv # cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z # MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046N0YwMC0wNUUwLUQ5NDcxJTAjBgNV # BAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEB # AQUAA4ICDwAwggIKAoICAQC1Hi1Tozh3O0czE8xfRnrymlJNCaGWommPy0eINf+4 # EJr7rf8tSzlgE8Il4Zj48T5fTTOAh6nITRf2lK7+upcnZ/xg0AKoDYpBQOWrL9Ob # FShylIHfr/DQ4PsRX8GRtInuJsMkwSg63bfB4Q2UikMEP/CtZHi8xW5XtAKp95cs # 3mvUCMvIAA83Jr/UyADACJXVU4maYisczUz7J111eD1KrG9mQ+ITgnRR/X2xTDMC # z+io8ZZFHGwEZg+c3vmPp87m4OqOKWyhcqMUupPveO/gQC9Rv4szLNGDaoePeK6I # U0JqcGjXqxbcEoS/s1hCgPd7Ux6YWeWrUXaxbb+JosgOazUgUGs1aqpnLjz0YKfU # qn8i5TbmR1dqElR4QA+OZfeVhpTonrM4sE/MlJ1JLpR2FwAIHUeMfotXNQiytYfR # BUOJHFeJYEflZgVk0Xx/4kZBdzgFQPOWfVd2NozXlC2epGtUjaluA2osOvQHZzGO # oKTvWUPX99MssGObO0xJHd0DygP/JAVp+bRGJqa2u7AqLm2+tAT26yI5veccDmNZ # sg3vDh1HcpCJa9QpRW/MD3a+AF2ygV1sRnGVUVG3VODX3BhGT8TMU/GiUy3h7ClX # OxmZ+weCuIOzCkTDbK5OlAS8qSPpgp+XGlOLEPaM31Mgf6YTppAaeP0ophx345oh # twIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFNCCsqdXRy/MmjZGVTAvx7YFWpslMB8G # A1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCG # Tmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUy # MFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4w # XAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2Vy # dHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwG # A1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQD # AgeAMA0GCSqGSIb3DQEBCwUAA4ICAQA4IvSbnr4jEPgo5W4xj3/+0dCGwsz863QG # Z2mB9Z4SwtGGLMvwfsRUs3NIlPD/LsWAxdVYHklAzwLTwQ5M+PRdy92DGftyEOGM # Hfut7Gq8L3RUcvrvr0AL/NNtfEpbAEkCFzseextY5s3hzj3rX2wvoBZm2ythwcLe # ZmMgHQCmjZp/20fHWJgrjPYjse6RDJtUTlvUsjr+878/t+vrQEIqlmebCeEi+VQV # xc7wF0LuMTw/gCWdcqHoqL52JotxKzY8jZSQ7ccNHhC4eHGFRpaKeiSQ0GXtlbGI # bP4kW1O3JzlKjfwG62NCSvfmM1iPD90XYiFm7/8mgR16AmqefDsfjBCWwf3qheIM # fgZzWqeEz8laFmM8DdkXjuOCQE/2L0TxhrjUtdMkATfXdZjYRlscBDyr8zGMlprF # C7LcxqCXlhxhtd2CM+mpcTc8RB2D3Eor0UdoP36Q9r4XWCVV/2Kn0AXtvWxvIfyO # Fm5aLl0eEzkhfv/XmUlBeOCElS7jdddWpBlQjJuHHUHjOVGXlrJT7X4hicF1o23x # 5U+j7qPKBceryP2/1oxfmHc6uBXlXBKukV/QCZBVAiBMYJhnktakWHpo9uIeSnYT # 6Qx7wf2RauYHIER8SLRmblMzPOs+JHQzrvh7xStx310LOp+0DaOXs8xjZvhpn+Wu # Zij5RmZijDCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZI # 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 # MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjdGMDAtMDVFMC1EOTQ3MSUwIwYDVQQD # ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQDC # KAZKKv5lsdC2yoMGKYiQy79p/6CBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w # IFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA6ZN3hjAiGA8yMDI0MDMwNjIzMTUx # OFoYDzIwMjQwMzA3MjMxNTE4WjB0MDoGCisGAQQBhFkKBAExLDAqMAoCBQDpk3eG # AgEAMAcCAQACAg07MAcCAQACAhM5MAoCBQDplMkGAgEAMDYGCisGAQQBhFkKBAIx # KDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZI # hvcNAQELBQADggEBABDwetuoovq/+DHcNbUgCPGpvM2fF2CgwSXMQBTdwSXtSx8a # en2nlTpljDWH4IU7SYIU+PZD8KOgdL4Uh3JLlqFfJtQ1wOqoCDChUWsEWPdBfAEo # 2ydUO0Y5mRgLjAPNTw4/ticpHx1DRZxFAzmTf16PhTX4fC/EsOPRn0Tcf7DF8vQm # ldp0A+icmb59oLpFaMpu/P6UTcKMFJ9rHMqQMdqpJlMOLFnbVl/4ejnOOiu9Wf7x # TEmr/oz5RNlBbnrEhRkBhWa7W7BgixGeDNaNzCktSHescaxETZgGNdbO/+4OotxB # GVq3EdCoD1oH/pUEy785t/kIcA5LUo1cDHswdWExggQNMIIECQIBATCBkzB8MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNy # b3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAfAqfB1ZO+YfrQABAAAB8DAN # BglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8G # CSqGSIb3DQEJBDEiBCDnXPCi7uhCDMHPz8xsbCO7HKyYNDebOAr6bWhBBeuVBDCB # +gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIFwBmqOlcv3kU7mAB5sWR74QFAiS # 6mb+CM6asnFAZUuLMIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh # c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD # b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw # MTACEzMAAAHwKnwdWTvmH60AAQAAAfAwIgQgk6FfKpcslH529YkphsbPuf6oH0ej # uRb/LQe71jZ8gVgwDQYJKoZIhvcNAQELBQAEggIAa82xEg16aIDmIE3wXDDK5aPf # zmeW19vKqAGtt1+fCc1PgoprIWhvwY3I479pAz5Dfsr4HFzDM00sUfcbqVGB5KlZ # 5C9lRa6xHjX3laUOx4vf51iz7rPeY8nSnE8DZ23SxDqCMCbf32RGJGgBbSZmgPvN # RyF5ODQzGMzALljdNHaZQX8TBv4iMZ/MUysLu0+Hx96dCmfWBkjifIRo2PUvuWkr # m4T7r88Ej9qpGnHB8PEdcx9s9hVdY4Yz8g1ii44/FSNBoWd4v7RXlta1zGAqhKnP # Zd6Bt1EdUQ2VIyTrl0j26pnD9bxSIPzR8lPzuJj91tO+YJNBnFngUFnwIf8sTdsb # WM7bDx9A3EA+Isvu/iDo0wN9zpIyFKEhb3PMZYmnggKLBY6fLp5ryGy78Xrf5x/I # /PBKSY/U3wJ7GvSVqHucZ/oiny32GYmo08laDb8IWa3FmQYqTbT8ndPAps6nP4u3 # aWy0fS41yB2a3N6LYow/IUtDLEg/0D0vuwV6W/ocAT04VUyw/p8HBXV3czP3P3Rm # IsToXAAXqDEQe+kkQbtVz2D5e0sbL5D+VT0OJE+bX5HHw/tAOU5Is4pvQ20W+Ntt # wP14FZ3rKfqgK1wnaQO1hX8X2Rv/l8cnn91/cevrLQUEw5zxXSx+xQSbbVDmA5lr # kOR4rDdJyUud8jLtyJ8= # SIG # End signature block |