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
# MIInvAYJKoZIhvcNAQcCoIInrTCCJ6kCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDu0RnMQpTI+qM0
# h0LTH/dgIIVOYsJ8iDSKXQeNVLNl26CCDYEwggX/MIID56ADAgECAhMzAAACUosz
# qviV8znbAAAAAAJSMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjEwOTAyMTgzMjU5WhcNMjIwOTAxMTgzMjU5WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDQ5M+Ps/X7BNuv5B/0I6uoDwj0NJOo1KrVQqO7ggRXccklyTrWL4xMShjIou2I
# sbYnF67wXzVAq5Om4oe+LfzSDOzjcb6ms00gBo0OQaqwQ1BijyJ7NvDf80I1fW9O
# L76Kt0Wpc2zrGhzcHdb7upPrvxvSNNUvxK3sgw7YTt31410vpEp8yfBEl/hd8ZzA
# v47DCgJ5j1zm295s1RVZHNp6MoiQFVOECm4AwK2l28i+YER1JO4IplTH44uvzX9o
# RnJHaMvWzZEpozPy4jNO2DDqbcNs4zh7AWMhE1PWFVA+CHI/En5nASvCvLmuR/t8
# q4bc8XR8QIZJQSp+2U6m2ldNAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUNZJaEUGL2Guwt7ZOAu4efEYXedEw
# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1
# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDY3NTk3MB8GA1UdIwQYMBaAFEhu
# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu
# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w
# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx
# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAFkk3
# uSxkTEBh1NtAl7BivIEsAWdgX1qZ+EdZMYbQKasY6IhSLXRMxF1B3OKdR9K/kccp
# kvNcGl8D7YyYS4mhCUMBR+VLrg3f8PUj38A9V5aiY2/Jok7WZFOAmjPRNNGnyeg7
# l0lTiThFqE+2aOs6+heegqAdelGgNJKRHLWRuhGKuLIw5lkgx9Ky+QvZrn/Ddi8u
# TIgWKp+MGG8xY6PBvvjgt9jQShlnPrZ3UY8Bvwy6rynhXBaV0V0TTL0gEx7eh/K1
# o8Miaru6s/7FyqOLeUS4vTHh9TgBL5DtxCYurXbSBVtL1Fj44+Od/6cmC9mmvrti
# yG709Y3Rd3YdJj2f3GJq7Y7KdWq0QYhatKhBeg4fxjhg0yut2g6aM1mxjNPrE48z
# 6HWCNGu9gMK5ZudldRw4a45Z06Aoktof0CqOyTErvq0YjoE4Xpa0+87T/PVUXNqf
# 7Y+qSU7+9LtLQuMYR4w3cSPjuNusvLf9gBnch5RqM7kaDtYWDgLyB42EfsxeMqwK
# WwA+TVi0HrWRqfSx2olbE56hJcEkMjOSKz3sRuupFCX3UroyYf52L+2iVTrda8XW
# esPG62Mnn3T8AuLfzeJFuAbfOSERx7IFZO92UPoXE1uEjL5skl1yTZB3MubgOA4F
# 8KoRNhviFAEST+nG8c8uIsbZeb08SeYQMqjVEmkwggd6MIIFYqADAgECAgphDpDS
# AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0
# ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla
# MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT
# H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG
# OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S
# 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz
# y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7
# 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u
# M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33
# X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl
# XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP
# 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB
# l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF
# RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM
# CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ
# BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud
# DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO
# 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0
# LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p
# Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB
# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw
# cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA
# XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY
# 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj
# 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd
# d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ
# Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf
# wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ
# aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j
# NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B
# xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96
# eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7
# r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I
# RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIZkTCCGY0CAQEwgZUwfjELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z
# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAlKLM6r4lfM52wAAAAACUjAN
# BglghkgBZQMEAgEFAKCBsDAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor
# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgcRsYk8YZ
# vGnaqn6Fby54jz91yVBgvbJfPuFu3WF0/kIwRAYKKwYBBAGCNwIBDDE2MDSgFIAS
# AE0AaQBjAHIAbwBzAG8AZgB0oRyAGmh0dHBzOi8vd3d3Lm1pY3Jvc29mdC5jb20g
# MA0GCSqGSIb3DQEBAQUABIIBAHrmJbb1oSwyU2vUoYx8TFCPyMQ0L5C4u5yL0f47
# rLueFwSafslXVCdtSa3lTC5hpvYyndEOek1xesSZ6UzlMMLYgtpxgjrqJsqFSav5
# OCp9cKqPQsKvo8AlVogP5swNKFLyEQYKVAW/9QPqTO8jqlFwuwSup1PZfSYFYmgo
# 9XrTZBa801Li86C5MvFbBsBdJt8FpR5WtgnVUGIm7kIbK2EiXQRU7ONCZ1J65cEs
# PZYq87Q8CiUW53Wcko36wQYN31pOB4RxETKzuT9ERub/LKQ9eeRxnLN+6qKC30ss
# KtjyC2D1j0YftyjjLdsI3AGNIrogtnyBXCxJUgCm19INP9KhghcZMIIXFQYKKwYB
# BAGCNwMDATGCFwUwghcBBgkqhkiG9w0BBwKgghbyMIIW7gIBAzEPMA0GCWCGSAFl
# AwQCAQUAMIIBWQYLKoZIhvcNAQkQAQSgggFIBIIBRDCCAUACAQEGCisGAQQBhFkK
# AwEwMTANBglghkgBZQMEAgEFAAQgISZNX92yFtwLhM/rzC8sUkaD9CFeCGsGuH3A
# PjV/GksCBmH9VmkTERgTMjAyMjAyMTUxMDIwNTkuNjU1WjAEgAIB9KCB2KSB1TCB
# 0jELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl
# ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMk
# TWljcm9zb2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1U
# aGFsZXMgVFNTIEVTTjpEMDgyLTRCRkQtRUVCQTElMCMGA1UEAxMcTWljcm9zb2Z0
# IFRpbWUtU3RhbXAgU2VydmljZaCCEWgwggcUMIIE/KADAgECAhMzAAABj/NRqOta
# ct3MAAEAAAGPMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv
# ZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBD
# QSAyMDEwMB4XDTIxMTAyODE5Mjc0NloXDTIzMDEyNjE5Mjc0NlowgdIxCzAJBgNV
# BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w
# HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29m
# dCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEmMCQGA1UECxMdVGhhbGVzIFRT
# UyBFU046RDA4Mi00QkZELUVFQkExJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0
# YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCZVz7+
# tc8XHoWTj4Kkuu5sOstNrdC4AdFVe7L7OzFfCSiCRPRr5da4FpvAfKqPxFGJlBC9
# 29s6rk1ETE54eJoK2RSxDTYRIB70LP6WgE22x8Krzjq7ei1YcImWqS8OtKvuYwGr
# BxFjtx+EAZ8u+WkxKiOgCeTtF6P6NwmdjEh43fgXeH0nAA1jfrSgZgIhLuks6ixZ
# X5vG6D26JNlgT9dyXJg0Xpd3Nn/MP/hTmnFPgxlCbMEa8Oz7xwN0D+y1l+P+vL6L
# RdRg0U+G6pz5QqTCb9c0cH4IOwZCX5lLQZxtRS6fhU9OEcmbqZEDAvnLzOm1YQih
# xtN5FJOZmdRraJgfYQ4FXt4KPHRJ1vqQtzXF0VFyQN5AZHgnXIXLJu5mxQ/zHR06
# wQSgtC46G4qUMtASDsPpnGZkmdLwHTd7CT9KlUuqxvrpTarIXgHAO3W5mSMRnt+K
# cihSBLPgHt9Ytgh47Y4JjEgTRe/CxWin0+9NdNm0Y/POYdTvncZqsrK4zqhr+ppP
# Ni+sB9RvspiG9VArEZQ+Qv354qIzsbSp6ckIWtfNk/BFahxwBHfc+E0S67PMpkUn
# gN5pMIuD/y4rRDhCMVF5/mfgf7YpAgSJtnvMh4FfeOCysgJvPNKbRBfdJFWZkf/8
# CqnxjGTBygjVYIGLO/zjP16rBEF1Dgdhw1tAwwIDAQABo4IBNjCCATIwHQYDVR0O
# BBYEFPMG5nRrrknO4qHOhZvbl/s3I3G8MB8GA1UdIwQYMBaAFJ+nFV0AXmJdg/Tl
# 0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCGTmh0dHA6Ly93d3cubWljcm9zb2Z0
# LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAy
# MDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4wXAYIKwYBBQUHMAKGUGh0dHA6Ly93
# d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVGltZS1T
# dGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwGA1UdEwEB/wQCMAAwEwYDVR0lBAww
# CgYIKwYBBQUHAwgwDQYJKoZIhvcNAQELBQADggIBAM1/06j3PKELmfWMLyJTs0lj
# f0WLOOHFnAlslj9i3CfremUyVNJoGl6tqfnrp+5GiMYlK/cTBmz5Gu45TZP9lEPH
# hUd6wse1yUTwaYwzWpMxpk8vwjYWtGZ/k6ingapzE100QIEKVVmafQrMV08ypFrn
# /RHoKaComHSa68iaKSAe5u+iGxq88TLIdBr3gcPj8s0p39ghoIoo/P1IDl8BrimF
# DgS/PZq5j1JSW4h3kwr0flyNZXAHEK9gAP7UJb3PsayEmU2OoG9a0o7onQB6Z+Dr
# PbyDupzsb+0K2uUfj/LbvL6y27BZc2/B2xJ3WW8HgzrcC4yX1inpq79cWScbMk8X
# qf+5ZHomFC/OHjQguB5OEuZiF/zP5oNvivY4EsbU/YHpoJNbZhCS3tOlSfMjRwoa
# vbXcJsq0aT844gdKwM7FqyZ4Yn4WJQkKJXXnCHdplP9VP8+Qv0TiEMEDAa3j0bzy
# BII7TH2N90NlZ1YZsQteVKYDcQ/h5NirtGuiVjTgbx8a0XSnO5m7jcDb3Noj2Uiv
# m6UpHPwShAdTpy7Q/FTDQH0fxwCS9DFoy6ZFn/h8Juo1vhNw+Q9xY4jbhBiW+lu1
# P2nfV+VgSWZznCMamUCTL+eQlxPQdkQ1d6fFa0++3iByiqml4k8DdL/UPnsovfrr
# t6kivTJXb3QTai1lsBbwMIIHcTCCBVmgAwIBAgITMwAAABXF52ueAptJmQAAAAAA
# FTANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0
# aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgyMjI1WhcNMzAwOTMwMTgzMjI1WjB8MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNy
# b3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCAiIwDQYJKoZIhvcNAQEBBQADggIP
# ADCCAgoCggIBAOThpkzntHIhC3miy9ckeb0O1YLT/e6cBwfSqWxOdcjKNVf2AX9s
# SuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+uDZnhUYjDLWNE893MsAQGOhgfWpSg0S3
# po5GawcU88V29YZQ3MFEyHFcUTE3oAo4bo3t1w/YJlN8OWECesSq/XJprx2rrPY2
# vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhiJdxqD89d9P6OU8/W7IVWTe/dvI2k45GP
# sjksUZzpcGkNyjYtcI4xyDUoveO0hyTD4MmPfrVUj9z6BVWYbWg7mka97aSueik3
# rMvrg0XnRm7KMtXAhjBcTyziYrLNueKNiOSWrAFKu75xqRdbZ2De+JKRHh09/SDP
# c31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXftnIv231fgLrbqn427DZM9ituqBJR6L8F
# A6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8P0zbr17C89XYcz1DTsEzOUyOArxCaC4Q
# 6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMYctenIPDC+hIK12NvDMk2ZItboKaDIV1f
# MHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9stQcxWv2XFJRXRLbJbqvUAV6bMURHXLv
# jflSxIUXk8A8FdsaN8cIFRg/eKtFtvUeh17aj54WcmnGrnu3tz5q4i6tAgMBAAGj
# ggHdMIIB2TASBgkrBgEEAYI3FQEEBQIDAQABMCMGCSsGAQQBgjcVAgQWBBQqp1L+
# ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4EFgQUn6cVXQBeYl2D9OXSZacbUzUZ6XIw
# XAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYzaHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMBMG
# A1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsG
# A1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2VsuP6KJc
# YmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwubWljcm9z
# b2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIz
# LmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93d3cubWlj
# cm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3J0
# MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38Kq3hLB9nATEkW+Geckv8qW/qXBS2Pk5H
# ZHixBpOXPTEztTnXwnE2P9pkbHzQdTltuw8x5MKP+2zRoZQYIu7pZmc6U03dmLq2
# HnjYNi6cqYJWAAOwBb6J6Gngugnue99qb74py27YP0h1AdkY3m2CDPVtI1TkeFN1
# JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQJL1AoL8ZthISEV09J+BAljis9/kpicO8
# F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1ijbCHcNhcy4sa3tuPywJeBTpkbKpW99J
# o3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP9pEB9s7GdP32THJvEKt1MMU0sHrYUP4K
# WN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkkvnNtyo4JvbMBV0lUZNlz138eW0QBjloZ
# kWsNn6Qo3GcZKCS6OEuabvshVGtqRRFHqfG3rsjoiV5PndLQTHa1V1QJsWkBRH58
# oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g75LcVv7TOPqUxUYS8vwLBgqJ7Fx0ViY1w
# /ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr4A245oyZ1uEi6vAnQj0llOZ0dFtq0Z4+
# 7X6gMTN9vMvpe784cETRkPHIqzqKOghif9lwY1NNje6CbaUFEMFxBmoQtB1VM1iz
# oXBm8qGCAtcwggJAAgEBMIIBAKGB2KSB1TCB0jELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxhbmQgT3Bl
# cmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjpEMDgyLTRC
# RkQtRUVCQTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIj
# CgEBMAcGBSsOAwIaAxUAPk0vggR250gHB0agJpXRYFtBmmqggYMwgYCkfjB8MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNy
# b3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIFAOW1rBAw
# IhgPMjAyMjAyMTUxMjM1MjhaGA8yMDIyMDIxNjEyMzUyOFowdzA9BgorBgEEAYRZ
# CgQBMS8wLTAKAgUA5bWsEAIBADAKAgEAAgIKHwIB/zAHAgEAAgIRSTAKAgUA5bb9
# kAIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6Eg
# oQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBABbsRXkKYNwDUD0MdQZeol6T
# hqQmBDzlXhQnNYUds/DT2BNiDmeaMs0xcEdPyQCvtytrmXnngKhJI4o+vANJXS/a
# 7rSHBaSQ6rROfBoRDBv6V8YMwMEZ5vYXZVHH9VJBQYqmZBoz3hE0sbR7UphNdzZV
# NseZvrwVpZV5MgxdiEdiMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzAR
# BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p
# Y3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3Rh
# bXAgUENBIDIwMTACEzMAAAGP81Go61py3cwAAQAAAY8wDQYJYIZIAWUDBAIBBQCg
# ggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQg
# BfsBB50dhl7hXgLLe7iFaxkrymFT4Uv+84XnNZn+GiQwgfoGCyqGSIb3DQEJEAIv
# MYHqMIHnMIHkMIG9BCCXcgVP4sbGC5WOIqbbYi2Y7p0UNZbydKG7o7qDzIXHHzCB
# mDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYD
# VQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAk
# BgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABj/NRqOta
# ct3MAAEAAAGPMCIEIEVovGoO3hMtwk/Fq4VXrEIc5RMn1YsNS4nhJVGg/PXSMA0G
# CSqGSIb3DQEBCwUABIICAA2eZb9CgMSGzZ6NLq7pNTgV6BOzwJpTbFdWE/vkYHcb
# LuLBHFNZxHeGpqAXDLSglLfODcJ0GewgEuK06Q9IIsjeeTbo/6d0pQe6Q1bHzFvc
# mYhi3r2QKEPGLQ3bDJeXZiSNPOA2Zns1TUHRAHV+gEBUv2ZrfKuBXmczbaSvLQ9U
# +3oioA9omu8Qw3GZ3U6jYTvt1GIMzmRRiI8jDAwm6InqAUPL60s0y8xLvle89Nle
# ohtUX9ikxg3E0vRCEpbiSzfTNURFj+yKV4SjhVLdGEARRQObcpdDOIgz2ym4xmWJ
# Clllw+N7PBLWPLzVRBtEbJcDCAWa3aLIarM5R/bgl7N+p1RPOw/LhrtWjyRDKSmX
# SehQZuiUau1Hp9RhWXcmpxPNC53FgspMgzodQaC1VC2dJwxf2hQfNPSLbiEijA6g
# 1QtBJaztRHCw9qIOfDOWE40eQuSv8nVeCzZwzzzUqRvV/Ejmdrc4X5kGO0m3rOwm
# olwOOMxVsZUsx2nf9I5t25PTL7E8/HvmK2YeUBJzcpxgy/tRfQ3rqzABptm/0Vrw
# f6xqUHTUtO7a4A81a9cPZkU6j/XDR6ketFOA/CbWrOC7YUWciDjgW4fettc4sMm9
# ofiZevIDgV6SnVpIPlagOvCbKR1s18dBq8C4pxc/uAdHAKxYC3C3/XQsgPxOtDoK
# SIG # End signature block