ReportCommands/ComplianceReport.ps1
|
#requires -Version 5.1 function Get-KeeperComplianceSnapshot { [CmdletBinding()] param( [Parameter()][switch]$Rebuild, [Parameter()][switch]$NoRebuild, [Parameter()][long[]]$OwnerUserIds, [Parameter()][switch]$SharedOnly ) if ($Rebuild -and $NoRebuild) { Write-Error "-Rebuild and -NoRebuild cannot be used together." -ErrorAction Stop } Assert-KeeperComplianceReportAccess if (-not $script:ComplianceReportCache) { $script:ComplianceReportCache = @{ Entries = @{} } } if (-not $script:ComplianceReportCache.Entries) { $script:ComplianceReportCache.Entries = @{} } $cacheTtl = [TimeSpan]::FromDays(1) $scopeKey = if ($null -eq $OwnerUserIds) { 'all' } else { 'users:' + ((@($OwnerUserIds | Sort-Object) | ForEach-Object { [string]$_ }) -join ',') } $cacheKey = if ($SharedOnly) { "shared-only:$scopeKey" } else { $scopeKey } $allCacheEntry = $script:ComplianceReportCache.Entries['all'] $cacheEntry = $script:ComplianceReportCache.Entries[$cacheKey] if ($cacheEntry) { $loadedAt = [datetime]$cacheEntry.LoadedAt $cacheIsFresh = ((Get-Date) - $loadedAt) -lt $cacheTtl if ($NoRebuild -or (-not $Rebuild -and $cacheIsFresh)) { Write-KeeperComplianceStatus "Using in-session cache '$cacheKey' loaded at $($loadedAt.ToString('u'))." Set-KeeperComplianceLastSnapshotStatus -FromCache $true -Incomplete $false -BuiltAt $loadedAt return $cacheEntry.Snapshot } } if ($null -eq $OwnerUserIds -and $SharedOnly -and $allCacheEntry) { $allLoadedAt = [datetime]$allCacheEntry.LoadedAt $allCacheIsFresh = ((Get-Date) - $allLoadedAt) -lt $cacheTtl if ($NoRebuild -or (-not $Rebuild -and $allCacheIsFresh)) { Write-KeeperComplianceStatus "Using in-session cache 'all' for compatible shared-only request '$cacheKey', loaded at $($allLoadedAt.ToString('u'))." Set-KeeperComplianceLastSnapshotStatus -FromCache $true -Incomplete $false -BuiltAt $allLoadedAt return $allCacheEntry.Snapshot } } $enterprise = getEnterprise if (-not $enterprise -or -not $enterprise.loader) { Write-Error "Enterprise data is required to build the compliance report." -ErrorAction Stop } $auth = $enterprise.loader.Auth if (-not $auth) { Write-Warning "Cannot fetch compliance aging data: enterprise authentication is not available." return } if (-not $Rebuild) { try { if ($enterprise.loader.Auth) { $sqliteLoaded = Import-KeeperComplianceSnapshotFromSqlite -CacheKey $cacheKey -CacheTtl $cacheTtl ` -Enterprise $enterprise -Auth $enterprise.loader.Auth if ($sqliteLoaded) { $script:ComplianceReportCache.Entries[$cacheKey] = @{ Snapshot = $sqliteLoaded.Snapshot LoadedAt = $sqliteLoaded.LoadedAt } Write-KeeperComplianceStatus "Using SQLite cache '$cacheKey' loaded at $($sqliteLoaded.LoadedAt.ToString('u'))." Set-KeeperComplianceLastSnapshotStatus -FromCache $true -Incomplete $sqliteLoaded.Incomplete -BuiltAt $sqliteLoaded.LoadedAt return $sqliteLoaded.Snapshot } if ($null -eq $OwnerUserIds -and $SharedOnly) { $sqliteAll = Import-KeeperComplianceSnapshotFromSqlite -CacheKey 'all' -CacheTtl $cacheTtl ` -Enterprise $enterprise -Auth $enterprise.loader.Auth if ($sqliteAll) { $script:ComplianceReportCache.Entries['all'] = @{ Snapshot = $sqliteAll.Snapshot LoadedAt = $sqliteAll.LoadedAt } Write-KeeperComplianceStatus "Using SQLite cache 'all' for compatible shared-only request '$cacheKey', loaded at $($sqliteAll.LoadedAt.ToString('u'))." Set-KeeperComplianceLastSnapshotStatus -FromCache $true -Incomplete $sqliteAll.Incomplete -BuiltAt $sqliteAll.LoadedAt return $sqliteAll.Snapshot } } } } catch { Write-Verbose -Message "[compliance] SQLite cache read skipped: $($_.Exception.Message)" } } if ($null -eq $OwnerUserIds -and $NoRebuild) { Write-Warning "No local compliance cache is available for this request. Building it now." } $auth = $enterprise.loader.Auth $ecPrivateKey = $null if ($enterprise.loader.EcPrivateKey) { $ecPrivateKey = [KeeperSecurity.Utils.CryptoUtils]::LoadEcPrivateKey($enterprise.loader.EcPrivateKey) } $snapshot = [PSCustomObject]@{ Users = @{} Records = @{} SharedFolders = @{} Teams = @{} OwnedRecordsByUser = @{} EcPrivateKey = $ecPrivateKey Diagnostics = [PSCustomObject]@{ RecordDataFailures = 0 } } $ownerIdLookup = $null if ($null -ne $OwnerUserIds) { $ownerIdLookup = [System.Collections.Generic.HashSet[long]]::new() foreach ($ownerUserId in $OwnerUserIds) { $ownerIdLookup.Add([long]$ownerUserId) | Out-Null } } $enterpriseUserIds = [System.Collections.Generic.List[long]]::new() foreach ($enterpriseUser in $enterprise.enterpriseData.Users) { $userUid = [long]$enterpriseUser.Id $userEmail = [string]$enterpriseUser.Email $snapshot.Users[$userUid] = [PSCustomObject]@{ UserUid = $userUid Email = $userEmail FullName = [string]$enterpriseUser.DisplayName JobTitle = '' NodeId = [long]$enterpriseUser.ParentNodeId } if ($null -eq $ownerIdLookup -or $ownerIdLookup.Contains($userUid)) { $snapshot.OwnedRecordsByUser[$userUid] = [System.Collections.Generic.HashSet[string]]::new() $enterpriseUserIds.Add($userUid) | Out-Null } } if ($null -ne $OwnerUserIds -and $enterpriseUserIds.Count -eq 0) { Write-KeeperComplianceStatus "Owner pre-filter resolved to zero enterprise users." Set-KeeperComplianceLastSnapshotStatus -FromCache $false -Incomplete $false $snapshot.PSObject.Properties.Remove('EcPrivateKey') $snapshot.PSObject.Properties.Remove('Diagnostics') return $snapshot } Write-KeeperComplianceStatus "Building compliance snapshot for $($enterpriseUserIds.Count) owner user(s). Cache key: $cacheKey. SharedOnly=$SharedOnly." $prelimPageLimit = 10000 $prelimFixedChunkSize = 5 $problemUserIds = [System.Collections.Generic.HashSet[long]]::new() $prelimSingleUserIds = [System.Collections.Generic.HashSet[long]]::new() $userQueue = [System.Collections.Generic.Queue[long]]::new() foreach ($uid in $enterpriseUserIds) { $userQueue.Enqueue($uid) } $prelimBatchNumber = 0 while ($userQueue.Count -gt 0) { $prelimChunkSize = [Math]::Min($prelimFixedChunkSize, [Math]::Max(1, $userQueue.Count)) if ($userQueue.Count -gt 0 -and $prelimSingleUserIds.Contains([long]$userQueue.Peek())) { $prelimChunkSize = 1 } $prelimBatchNumber++ $takeCount = [Math]::Min($prelimChunkSize, $userQueue.Count) $userChunkList = [System.Collections.Generic.List[long]]::new() for ($qi = 0; $qi -lt $takeCount; $qi++) { $userChunkList.Add($userQueue.Dequeue()) | Out-Null } $userChunk = @($userChunkList) if ($userChunk.Count -eq 0) { continue } Write-KeeperComplianceStatus "Preliminary batch ${prelimBatchNumber}: requesting $($userChunk.Count) user(s); queue remaining=$($userQueue.Count); chunk size=$prelimChunkSize." $prelimRequest = [Enterprise.PreliminaryComplianceDataRequest]::new() foreach ($userUid in $userChunk) { $prelimRequest.EnterpriseUserIds.Add([long]$userUid) | Out-Null } $prelimRequest.IncludeNonShared = (-not $SharedOnly) $prelimRequest.IncludeTotalMatchingRecordsInFirstResponse = $true $prelimRequest.ContinuationToken = [Google.Protobuf.ByteString]::Empty $hasMore = $true $chunkCompleted = $true $chunkTotal = 0 $currentBatchLoaded = 0 $seenUserIds = [System.Collections.Generic.List[long]]::new() $prelimPageNumber = 0 while ($hasMore) { $prelimPageNumber++ Write-KeeperComplianceStatus "Preliminary batch ${prelimBatchNumber} page ${prelimPageNumber}: calling enterprise/get_preliminary_compliance_data." try { $prelimResponse = [Enterprise.PreliminaryComplianceDataResponse]( Get-KeeperComplianceRestResponse -Auth $auth -Endpoint 'enterprise/get_preliminary_compliance_data' ` -Request $prelimRequest -ResponseType ([Enterprise.PreliminaryComplianceDataResponse]) ) } catch { $message = [string]$_.Exception.Message $exceptionText = [string]$_ $isTimeout = ( $message.IndexOf('GatewayTimeout', [System.StringComparison]::OrdinalIgnoreCase) -ge 0 -or $message.IndexOf('gateway_timeout', [System.StringComparison]::OrdinalIgnoreCase) -ge 0 -or $message.IndexOf('HttpClient.Timeout', [System.StringComparison]::OrdinalIgnoreCase) -ge 0 -or $message.IndexOf('The request was canceled', [System.StringComparison]::OrdinalIgnoreCase) -ge 0 -or $exceptionText.IndexOf('TaskCanceledException', [System.StringComparison]::OrdinalIgnoreCase) -ge 0 -or $exceptionText.IndexOf('OperationCanceledException', [System.StringComparison]::OrdinalIgnoreCase) -ge 0 ) if ($isTimeout) { $requeueIds = Get-KeeperCompliancePrelimRequeueUserIds -UserChunk $userChunk -SeenUserIds $seenUserIds if ($requeueIds.Count -gt 1 -or $prelimChunkSize -gt 1) { if ($prelimChunkSize -gt 1) { foreach ($requeueUserId in $requeueIds) { $prelimSingleUserIds.Add([long]$requeueUserId) | Out-Null } Write-Warning "Preliminary compliance request timed out for $($requeueIds.Count) user(s). Retrying the affected users one-by-one." } else { Write-Warning "Preliminary compliance request timed out for user $($requeueIds[0]). Skipping after single-user retry." } $userQueue = Add-KeeperComplianceUserQueueFront -Queue $userQueue -FrontIds $requeueIds } else { Write-Warning "Preliminary compliance request timed out for user $($requeueIds[0]). Skipping after single-user retry." foreach ($problemUserId in $requeueIds) { $prelimSingleUserIds.Remove([long]$problemUserId) | Out-Null $problemUserIds.Add([long]$problemUserId) | Out-Null } } $chunkCompleted = $false break } throw } if ($prelimResponse.PSObject.Properties['TotalMatchingRecords'] -and $prelimResponse.TotalMatchingRecords) { $currentBatchLoaded = 0 $chunkTotal = [int]$prelimResponse.TotalMatchingRecords } Write-KeeperComplianceStatus "Preliminary batch ${prelimBatchNumber} page ${prelimPageNumber}: received $(@($prelimResponse.AuditUserData).Count) user result(s); total matching records=$chunkTotal." foreach ($auditUserData in $prelimResponse.AuditUserData) { $ownerUid = [long]$auditUserData.EnterpriseUserId if (-not $snapshot.OwnedRecordsByUser.ContainsKey($ownerUid)) { $snapshot.OwnedRecordsByUser[$ownerUid] = [System.Collections.Generic.HashSet[string]]::new() } if (-not $seenUserIds.Contains($ownerUid)) { $seenUserIds.Add($ownerUid) | Out-Null } foreach ($auditUserRecord in $auditUserData.AuditUserRecords) { $recordUid = ConvertTo-KeeperComplianceUid -ByteString $auditUserRecord.RecordUid if (-not $recordUid) { continue } $recordData = Get-KeeperComplianceRecordData -EncryptedData $auditUserRecord.EncryptedData -EcPrivateKey $ecPrivateKey ` -Diagnostics $snapshot.Diagnostics -RecordUid $recordUid -Source 'preliminary' if (-not $snapshot.Records.ContainsKey($recordUid)) { $snapshot.Records[$recordUid] = [PSCustomObject]@{ Uid = $recordUid Title = [string]$recordData.Title RecordType = [string]$recordData.RecordType Url = [string]$recordData.Url Shared = [bool]$auditUserRecord.Shared InTrash = $false UserPermissions = @{} SharedFolderUids = [System.Collections.Generic.HashSet[string]]::new() } } else { if (-not $snapshot.Records[$recordUid].Shared -and $auditUserRecord.Shared) { $snapshot.Records[$recordUid].Shared = $true } Merge-KeeperComplianceRecordFields -RecordEntry $snapshot.Records[$recordUid] -RecordData $recordData } $snapshot.OwnedRecordsByUser[$ownerUid].Add($recordUid) | Out-Null $currentBatchLoaded++ } } $hasMore = [bool]$prelimResponse.HasMore if ($chunkTotal -gt $prelimPageLimit -and $userChunk.Count -gt 1 -and $hasMore) { foreach ($requeueUserId in $userChunk) { $prelimSingleUserIds.Add([long]$requeueUserId) | Out-Null } Write-Warning "Preliminary compliance response reported $chunkTotal matching records for $($userChunk.Count) user(s). Retrying the affected users one-by-one." $requeueIds = Get-KeeperCompliancePrelimRequeueUserIds -UserChunk $userChunk -SeenUserIds $seenUserIds $userQueue = Add-KeeperComplianceUserQueueFront -Queue $userQueue -FrontIds $requeueIds $chunkCompleted = $false break } if ($hasMore) { $prelimRequest.ContinuationToken = $prelimResponse.ContinuationToken } } if ($chunkCompleted) { foreach ($completedUserId in $userChunk) { $prelimSingleUserIds.Remove([long]$completedUserId) | Out-Null } Write-KeeperComplianceStatus "Preliminary batch $prelimBatchNumber completed: users seen=$($seenUserIds.Count); records loaded=$currentBatchLoaded; next chunk size=$prelimFixedChunkSize." } } if ($problemUserIds.Count -gt 0) { $problemEmails = @() foreach ($problemUserId in $problemUserIds) { $problemUser = $null if ($enterprise.enterpriseData.TryGetUserById([long]$problemUserId, [ref]$problemUser) -and $problemUser) { $problemEmails += [string]$problemUser.Email } else { $problemEmails += [string]$problemUserId } } Write-Warning "Preliminary compliance data could not be fetched for: $($problemEmails -join ', ')" } Write-KeeperComplianceStatus "Preliminary compliance phase complete. Owners with records tracked=$($snapshot.OwnedRecordsByUser.Count); records tracked=$($snapshot.Records.Count)." $rootNodeId = [long]$enterprise.enterpriseData.RootNode.Id $anonymousSeed = 0L $maxUsersPerRequest = 5000 $maxRecordsPerRequest = 1000 $userIdList = @( $snapshot.OwnedRecordsByUser.GetEnumerator() | Where-Object { $_.Value -and $_.Value.Count -gt 0 } | ForEach-Object { [long]$_.Key } | Sort-Object ) $fullComplianceFailures = 0 $stopFullCompliance = $false for ($userIndex = 0; $userIndex -lt $userIdList.Count; $userIndex += $maxUsersPerRequest) { if ($stopFullCompliance) { break } $userChunk = @($userIdList | Select-Object -Skip $userIndex -First $maxUsersPerRequest) if ($userChunk.Count -eq 0) { continue } $chunkRecordSet = [System.Collections.Generic.HashSet[string]]::new() foreach ($userUid in $userChunk) { foreach ($recordUid in $snapshot.OwnedRecordsByUser[[long]$userUid]) { $chunkRecordSet.Add($recordUid) | Out-Null } } $chunkRecordList = @($chunkRecordSet) for ($recordIndex = 0; $recordIndex -lt $chunkRecordList.Count; $recordIndex += $maxRecordsPerRequest) { if ($stopFullCompliance) { break } $recordChunk = @($chunkRecordList | Select-Object -Skip $recordIndex -First $maxRecordsPerRequest) if ($recordChunk.Count -eq 0) { continue } $request = [Enterprise.ComplianceReportRequest]::new() $request.ReportName = "Compliance Report on $(Get-Date -Format o)" $request.SaveReport = $false $request.ComplianceReportRun = [Enterprise.ComplianceReportRun]::new() $request.ComplianceReportRun.ReportCriteriaAndFilter = [Enterprise.ComplianceReportCriteriaAndFilter]::new() $request.ComplianceReportRun.ReportCriteriaAndFilter.Criteria = [Enterprise.ComplianceReportCriteria]::new() $request.ComplianceReportRun.ReportCriteriaAndFilter.NodeId = $rootNodeId $request.ComplianceReportRun.ReportCriteriaAndFilter.Criteria.IncludeNonShared = (-not $SharedOnly) foreach ($userUid in $userChunk) { $request.ComplianceReportRun.Users.Add([long]$userUid) | Out-Null } foreach ($recordUid in $recordChunk) { $request.ComplianceReportRun.Records.Add( [Google.Protobuf.ByteString]::CopyFrom([KeeperSecurity.Utils.CryptoUtils]::Base64UrlDecode([string]$recordUid)) ) | Out-Null } try { $response = [Enterprise.ComplianceReportResponse]( Get-KeeperComplianceRestResponse -Auth $auth -Endpoint 'enterprise/run_compliance_report' ` -Request $request -ResponseType ([Enterprise.ComplianceReportResponse]) ) } catch { $fullComplianceFailures++ $message = [string]$_.Exception.Message if ($message.IndexOf('required privilege', [System.StringComparison]::OrdinalIgnoreCase) -ge 0 -or $message.IndexOf('access_denied', [System.StringComparison]::OrdinalIgnoreCase) -ge 0) { Write-Warning "Full compliance request was denied by Keeper. Continuing with the preliminary snapshot only." $stopFullCompliance = $true break } Write-Warning "Full compliance request failed for users=$($userChunk.Count), records=$($recordChunk.Count): $message" continue } $anonymousSeed = Update-KeeperComplianceAnonymousUsers -Response $response -AnonymousSeed $anonymousSeed Merge-KeeperComplianceResponse -Snapshot $snapshot -Response $response } } if ($snapshot.Diagnostics.RecordDataFailures -gt 0) { Write-Warning "Failed to decrypt or parse compliance metadata for $($snapshot.Diagnostics.RecordDataFailures) record payload(s). Some title/type/url fields may be blank." } if ($fullComplianceFailures -gt 0) { Write-Warning "$fullComplianceFailures full compliance request batch(es) failed. Results may be incomplete." } $recordDecryptFails = [int]$snapshot.Diagnostics.RecordDataFailures $incomplete = ($problemUserIds.Count -gt 0) -or ($fullComplianceFailures -gt 0) -or $stopFullCompliance -or ($recordDecryptFails -gt 0) Set-KeeperComplianceLastSnapshotStatus -FromCache $false -Incomplete $incomplete ` -PreliminaryUsersSkipped $problemUserIds.Count -FullComplianceFailures $fullComplianceFailures ` -PrivilegeDeniedStoppedFullFetch $stopFullCompliance -RecordMetadataDecryptFailures $recordDecryptFails $snapshot.PSObject.Properties.Remove('EcPrivateKey') $snapshot.PSObject.Properties.Remove('Diagnostics') $script:ComplianceReportCache.Entries[$cacheKey] = @{ Snapshot = $snapshot LoadedAt = Get-Date } Write-KeeperComplianceStatus "Compliance snapshot cached under '$cacheKey'. Final records=$($snapshot.Records.Count); shared folders=$($snapshot.SharedFolders.Count); teams=$($snapshot.Teams.Count)." Save-KeeperComplianceSnapshotToSqlite -CacheKey $cacheKey -Snapshot $snapshot -Incomplete $incomplete ` -SharedOnly ([bool]$SharedOnly) -Enterprise $enterprise -Auth $auth return $snapshot } function Get-KeeperComplianceAuditEventValue { param( [Parameter()]$Event, [Parameter(Mandatory = $true)][string]$Key ) if ($null -eq $Event) { return $null } $property = $Event.PSObject.Properties[$Key] if ($property -and $null -ne $property.Value) { return $property.Value } $keysProperty = $Event.PSObject.Properties['Keys'] if ($keysProperty -and $null -ne $keysProperty.Value) { try { if (@($keysProperty.Value) -contains $Key) { $value = $Event[$Key] if ($null -ne $value) { return $value } } } catch { } } $containsKeyMethod = $Event.PSObject.Methods['ContainsKey'] if ($containsKeyMethod) { try { if ($Event.ContainsKey($Key)) { $value = $Event[$Key] if ($null -ne $value) { return $value } } } catch { } } try { foreach ($p in $Event.PSObject.Properties) { if ($p.Name -ieq $Key -and $null -ne $p.Value) { return $p.Value } } } catch { } if ($Event -is [System.Collections.IDictionary]) { foreach ($k in @($Event.Keys)) { if ($null -eq $k) { continue } if ([string]$k -ieq $Key) { $v = $Event[$k] if ($null -ne $v) { return $v } } } } return $null } function ConvertTo-KeeperComplianceDateTime { param( [Parameter()]$EpochValue ) if ($null -eq $EpochValue) { return $null } $epoch = 0L if ([long]::TryParse($EpochValue.ToString(), [ref]$epoch)) { try { return [DateTimeOffset]::FromUnixTimeSeconds($epoch).LocalDateTime } catch { } } return $null } function Get-KeeperComplianceAgingData { param( [Parameter(Mandatory = $true)][string[]]$RecordUids, [Parameter()]$Snapshot, [Parameter()][long[]]$OwnerUserIdsForAging ) $recordIds = @($RecordUids | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Sort-Object -Unique) if ($recordIds.Count -eq 0) { return @{} } $enterprise = $null $auth = $null try { $enterprise = getEnterprise if ($enterprise -and $enterprise.loader -and $enterprise.loader.Auth) { $auth = $enterprise.loader.Auth Import-KeeperComplianceAgingCacheFromSqlite -Enterprise $enterprise -Auth $auth } } catch { Write-Verbose -Message "[compliance] Aging SQLite preload skipped: $($_.Exception.Message)" } if (-not $script:ComplianceAgingCache) { $script:ComplianceAgingCache = @{ Entries = @{} } } if (-not $script:ComplianceAgingCache.Entries) { $script:ComplianceAgingCache.Entries = @{} } $cacheTtl = [TimeSpan]::FromDays(1) $now = Get-Date $agingData = @{} $staleRecordIds = [System.Collections.Generic.List[string]]::new() foreach ($recordUid in $recordIds) { $cachedEntry = $script:ComplianceAgingCache.Entries[$recordUid] if ($cachedEntry -and (((Get-Date) - [datetime]$cachedEntry.LastCached) -lt $cacheTtl)) { $agingData[$recordUid] = @{ created = $cachedEntry.Created last_pw_change = $cachedEntry.LastPwChange last_modified = $cachedEntry.LastModified last_rotation = $cachedEntry.LastRotation } } else { $agingData[$recordUid] = @{ created = $null last_pw_change = $null last_modified = $null last_rotation = $null } $staleRecordIds.Add($recordUid) | Out-Null } } if ($null -ne $Snapshot -and $OwnerUserIdsForAging -and $OwnerUserIdsForAging.Count -gt 0 -and $enterprise -and $auth) { $refreshMap = Import-KeeperComplianceAgingUserRefreshFromSqlite -Enterprise $enterprise -Auth $auth $minTs = [DateTimeOffset]::UtcNow.AddDays(-1).ToUnixTimeSeconds() $skipIds = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) foreach ($ou in $OwnerUserIdsForAging) { $last = $refreshMap[[string]$ou] if ($null -eq $last -or [long]$last -lt $minTs) { continue } if ($Snapshot.OwnedRecordsByUser.ContainsKey([long]$ou)) { foreach ($r in $Snapshot.OwnedRecordsByUser[[long]$ou]) { $rs = [string]$r if ($recordIds -contains $rs) { $skipIds.Add($rs) | Out-Null } } } } if ($skipIds.Count -gt 0) { $newStale = [System.Collections.Generic.List[string]]::new() foreach ($x in $staleRecordIds) { if (-not $skipIds.Contains($x)) { $newStale.Add($x) | Out-Null } } $staleRecordIds = $newStale } } if ($staleRecordIds.Count -eq 0) { Write-KeeperComplianceStatus "Aging phase: all $($recordIds.Count) record(s) satisfied from cache." if ($null -ne $OwnerUserIdsForAging -and $OwnerUserIdsForAging.Count -gt 0) { try { if ($enterprise -and $auth) { Save-KeeperComplianceAgingUserRefreshToSqlite -Enterprise $enterprise -Auth $auth -UserIds $OwnerUserIdsForAging } } catch { Write-Verbose -Message "[compliance] Aging user refresh save skipped: $($_.Exception.Message)" } } return $agingData } Write-KeeperComplianceStatus "Aging phase: fetching audit events for $($staleRecordIds.Count) stale record(s); cache hits=$($recordIds.Count - $staleRecordIds.Count)." $typesByAgingEvent = [ordered]@{ created = @() last_modified = @('record_update') last_rotation = @('record_rotation_scheduled_ok', 'record_rotation_on_demand_ok') last_pw_change = @('record_password_change') } $requestChunkSize = 2000 function Invoke-KeeperComplianceAgingRequests { param( [Parameter(Mandatory = $true)][string[]]$RequestRecordUids, [Parameter()][string[]]$EventTypes, [Parameter(Mandatory = $true)][string]$Aggregate, [Parameter(Mandatory = $true)][string]$Order ) $responses = [System.Collections.Generic.List[object]]::new() for ($index = 0; $index -lt $RequestRecordUids.Count; $index += $requestChunkSize) { $chunk = @($RequestRecordUids | Select-Object -Skip $index -First $requestChunkSize) if ($chunk.Count -eq 0) { continue } Write-KeeperComplianceStatus "Aging request: aggregate=$Aggregate order=$Order events=$(@($EventTypes) -join ',') records=$($chunk.Count) offset=$index." $filter = New-Object KeeperSecurity.Enterprise.AuditLogCommands.ReportFilter $filter.RecordUid = $chunk if ($EventTypes -and $EventTypes.Count -gt 0) { $filter.EventTypes = $EventTypes } $request = New-Object KeeperSecurity.Enterprise.AuditLogCommands.GetAuditEventReportsCommand $request.Filter = $filter $request.ReportType = 'span' $request.Aggregate = @($Aggregate) $request.Columns = @('record_uid') $request.Order = $Order $request.Limit = 2000 $response = $auth.ExecuteAuthCommand( $request, [KeeperSecurity.Enterprise.AuditLogCommands.GetAuditEventReportsResponse], $true ).GetAwaiter().GetResult() if ($response -and $response.Events) { foreach ($auditRow in $response.Events) { $responses.Add($auditRow) | Out-Null } Write-KeeperComplianceStatus "Aging response: aggregate=$Aggregate returned $($response.Events.Count) event row(s)." } else { Write-KeeperComplianceStatus "Aging response: aggregate=$Aggregate returned 0 event row(s)." } } return @($responses) } $recordEventsByStat = @{} foreach ($stat in $typesByAgingEvent.Keys) { $aggregate = if ($stat -eq 'created') { 'first_created' } else { 'last_created' } $order = if ($stat -eq 'created') { 'ascending' } else { 'descending' } $events = Invoke-KeeperComplianceAgingRequests -RequestRecordUids @($staleRecordIds) ` -EventTypes $typesByAgingEvent[$stat] -Aggregate $aggregate -Order $order $recordEventsByStat[$stat] = @{} foreach ($auditRow in $events) { $recordUid = Get-KeeperComplianceAuditEventValue -Event $auditRow -Key 'record_uid' if (-not $recordUid) { continue } $recordEventsByStat[$stat][[string]$recordUid] = ConvertTo-KeeperComplianceDateTime ` -EpochValue (Get-KeeperComplianceAuditEventValue -Event $auditRow -Key $aggregate) } } $pwCountEvents = Invoke-KeeperComplianceAgingRequests -RequestRecordUids @($staleRecordIds) ` -EventTypes @('record_password_change') -Aggregate 'occurrences' -Order 'descending' $pwOccurrences = @{} foreach ($auditRow in $pwCountEvents) { $recordUid = Get-KeeperComplianceAuditEventValue -Event $auditRow -Key 'record_uid' if (-not $recordUid) { continue } $occurrences = 0 $occurrenceValue = Get-KeeperComplianceAuditEventValue -Event $auditRow -Key 'occurrences' if ($null -ne $occurrenceValue) { [void][int]::TryParse($occurrenceValue.ToString(), [ref]$occurrences) } $pwOccurrences[[string]$recordUid] = $occurrences } foreach ($stat in $recordEventsByStat.Keys) { foreach ($recordUid in $recordEventsByStat[$stat].Keys) { $agingData[$recordUid][$stat] = $recordEventsByStat[$stat][$recordUid] if ($stat -eq 'created' -and -not $agingData[$recordUid]['last_modified']) { $agingData[$recordUid]['last_modified'] = $recordEventsByStat[$stat][$recordUid] } } } foreach ($recordUid in $pwOccurrences.Keys) { if ($pwOccurrences[$recordUid] -le 1 -and $agingData[$recordUid]['last_pw_change']) { $agingData[$recordUid]['last_pw_change'] = $null } } foreach ($recordUid in $staleRecordIds) { $pwChange = $agingData[$recordUid]['last_pw_change'] $rotation = $agingData[$recordUid]['last_rotation'] if ($rotation -and (-not $pwChange -or $rotation -gt $pwChange)) { $agingData[$recordUid]['last_pw_change'] = $rotation } $script:ComplianceAgingCache.Entries[$recordUid] = @{ Created = $agingData[$recordUid]['created'] LastPwChange = $agingData[$recordUid]['last_pw_change'] LastModified = $agingData[$recordUid]['last_modified'] LastRotation = $agingData[$recordUid]['last_rotation'] LastCached = $now } } try { if ($enterprise -and $auth) { Save-KeeperComplianceAgingCacheToSqlite -Enterprise $enterprise -Auth $auth if ($null -ne $OwnerUserIdsForAging -and $OwnerUserIdsForAging.Count -gt 0) { Save-KeeperComplianceAgingUserRefreshToSqlite -Enterprise $enterprise -Auth $auth -UserIds $OwnerUserIdsForAging } } } catch { Write-Verbose -Message "[compliance] Aging SQLite save skipped: $($_.Exception.Message)" } return $agingData } function Get-KeeperComplianceOwners { param( [Parameter(Mandatory = $true)]$Snapshot, [Parameter()][string[]]$Username, [Parameter()][string[]]$Team, [Parameter()][string[]]$JobTitle, [Parameter()]$Node ) $enterprise = getEnterprise $enterpriseData = $enterprise.enterpriseData $candidateUserIds = [System.Collections.Generic.HashSet[long]]::new() $hasUsernameFilter = Test-KeeperComplianceHasNonEmptyStringList -Strings $Username $hasTeamFilter = Test-KeeperComplianceHasNonEmptyStringList -Strings $Team if ($hasUsernameFilter) { $lookup = @{} foreach ($value in $Username) { if (-not [string]::IsNullOrWhiteSpace([string]$value)) { $lookup[$value.ToLowerInvariant()] = $true } } foreach ($user in $Snapshot.Users.Values) { if ($user.Email -and $lookup.ContainsKey($user.Email.ToLowerInvariant())) { $candidateUserIds.Add([long]$user.UserUid) | Out-Null } } } if ($hasTeamFilter) { foreach ($teamRef in $Team) { if ([string]::IsNullOrWhiteSpace([string]$teamRef)) { continue } $resolvedTeam = Get-KeeperTeamByNameOrUid -EnterpriseData $enterpriseData -TeamInput $teamRef if (-not $resolvedTeam) { Write-Warning "No enterprise team matched '$teamRef' for compliance report owners." continue } foreach ($userUid in $enterpriseData.GetUsersForTeam($resolvedTeam.Uid)) { $candidateUserIds.Add([long]$userUid) | Out-Null } } } if ($candidateUserIds.Count -eq 0 -and -not $hasUsernameFilter -and -not $hasTeamFilter) { foreach ($userUid in $Snapshot.OwnedRecordsByUser.Keys) { $candidateUserIds.Add([long]$userUid) | Out-Null } } $owners = @() foreach ($userUid in $candidateUserIds) { if ($Snapshot.Users.ContainsKey([long]$userUid)) { $owners += $Snapshot.Users[[long]$userUid] } } if ($JobTitle) { $jobTitleLookup = @{} foreach ($title in $JobTitle) { if ($title) { $jobTitleLookup[$title.ToLowerInvariant()] = $true } } $owners = @($owners | Where-Object { $_.JobTitle -and $jobTitleLookup.ContainsKey(([string]$_.JobTitle).ToLowerInvariant()) }) } if (Test-KeeperComplianceHasNodeFilter -Node $Node) { $resolvedNode = Resolve-KeeperComplianceNode -Node $Node.Trim() -Context 'compliance report node filter' $targetNodeId = [long]$resolvedNode.Id $rootNodeId = [long]$enterpriseData.RootNode.Id if ($targetNodeId -ne $rootNodeId) { $owners = @($owners | Where-Object { $nid = [long]$_.NodeId if ($nid -le 0) { $nid = $rootNodeId } return ($nid -eq $targetNodeId) }) } } return @($owners) } function Test-KeeperComplianceRecordMatch { param( [Parameter(Mandatory = $true)]$Record, [Parameter()][string[]]$RecordFilter, [Parameter()][string[]]$Url, [Parameter()][switch]$Shared, [Parameter()][switch]$DeletedItems, [Parameter()][switch]$ActiveItems ) if ($Shared -and -not $Record.Shared) { return $false } if ($DeletedItems -and -not $Record.InTrash) { return $false } if ($ActiveItems -and $Record.InTrash) { return $false } if ($Url) { $matchedUrl = $false foreach ($urlValue in $Url) { if ($Record.Url -and $Record.Url.IndexOf($urlValue, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) { $matchedUrl = $true break } } if (-not $matchedUrl) { return $false } } if ($RecordFilter) { $matchedRecord = $false foreach ($recordRef in $RecordFilter) { if ($Record.Uid -eq $recordRef) { $matchedRecord = $true break } if ($Record.Title -and $Record.Title -like $recordRef) { $matchedRecord = $true break } } if (-not $matchedRecord) { return $false } } return $true } function Get-KeeperComplianceReportRows { param( [Parameter(Mandatory = $true)]$Snapshot, [Parameter()][string[]]$Username, [Parameter()][string[]]$Team, [Parameter()][string[]]$JobTitle, [Parameter()]$Node, [Parameter()][string[]]$Record, [Parameter()][string[]]$Url, [Parameter()][switch]$Shared, [Parameter()][switch]$DeletedItems, [Parameter()][switch]$ActiveItems, [Parameter()][switch]$Aging, [Parameter()][long[]]$OwnerUserIdsForAging ) if ($DeletedItems -and $ActiveItems) { Write-Error "-DeletedItems and -ActiveItems cannot be used together." -ErrorAction Stop } $owners = Get-KeeperComplianceOwners -Snapshot $Snapshot -Username $Username -Team $Team -JobTitle $JobTitle -Node $Node $rows = [System.Collections.Generic.List[PSCustomObject]]::new() $agingData = @{} Write-KeeperComplianceStatus "Building report rows for $($owners.Count) owner(s)." foreach ($owner in $owners) { if (-not $Snapshot.OwnedRecordsByUser.ContainsKey([long]$owner.UserUid)) { continue } foreach ($recordUid in $Snapshot.OwnedRecordsByUser[[long]$owner.UserUid]) { if (-not $Snapshot.Records.ContainsKey($recordUid)) { continue } $recordEntry = $Snapshot.Records[$recordUid] if (-not (Test-KeeperComplianceRecordMatch -Record $recordEntry -RecordFilter $Record -Url $Url ` -Shared:$Shared -DeletedItems:$DeletedItems -ActiveItems:$ActiveItems)) { continue } $permissionsLookup = @{} foreach ($userUid in $recordEntry.UserPermissions.Keys) { Add-KeeperCompliancePermissionByUserUid -Snapshot $Snapshot -PermissionLookup $permissionsLookup ` -TargetUid ([long]$userUid) -PermissionBits ([int]$recordEntry.UserPermissions[$userUid]) } foreach ($sharedFolderUid in $recordEntry.SharedFolderUids) { if (-not $Snapshot.SharedFolders.ContainsKey([string]$sharedFolderUid)) { continue } $folderEntry = $Snapshot.SharedFolders[[string]$sharedFolderUid] if (-not $folderEntry.RecordPermissions.ContainsKey($recordUid)) { continue } $folderBits = [int]$folderEntry.RecordPermissions[$recordUid] foreach ($folderUserUid in $folderEntry.Users) { Add-KeeperCompliancePermissionByUserUid -Snapshot $Snapshot -PermissionLookup $permissionsLookup ` -TargetUid ([long]$folderUserUid) -PermissionBits $folderBits } foreach ($teamUid in $folderEntry.Teams) { if (-not $Snapshot.Teams.ContainsKey([string]$teamUid)) { continue } foreach ($teamUserUid in $Snapshot.Teams[[string]$teamUid].Users) { Add-KeeperCompliancePermissionByUserUid -Snapshot $Snapshot -PermissionLookup $permissionsLookup ` -TargetUid ([long]$teamUserUid) -PermissionBits $folderBits } } } if ($permissionsLookup.Count -eq 0 -and $owner.Email) { Add-KeeperCompliancePermissionByEmail -PermissionLookup $permissionsLookup -Email ([string]$owner.Email) -PermissionBits 1 } $sharedFolderIds = @($recordEntry.SharedFolderUids | Sort-Object) foreach ($email in ($permissionsLookup.Keys | Sort-Object)) { $bits = [int]$permissionsLookup[$email] $row = [ordered]@{ record_uid = $recordEntry.Uid title = [string]$recordEntry.Title type = [string]$recordEntry.RecordType username = [string]$email permissions = Get-KeeperCompliancePermissionText -PermissionBits $bits permission_bits = $bits url = if ($recordEntry.Url) { ([string]$recordEntry.Url).TrimEnd('/') } else { '' } in_trash = [bool]$recordEntry.InTrash shared_folder_uid = $sharedFolderIds } $rows.Add([PSCustomObject]$row) | Out-Null } } } $rows = @( $rows | Sort-Object record_uid, @{ Expression = { $_.permission_bits -band 1 }; Descending = $true }, @{ Expression = { $_.permission_bits }; Descending = $true } ) if ($Aging -and $rows.Count -gt 0) { Write-KeeperComplianceStatus "Applying aging data to $($rows.Count) row(s)." $agingData = Get-KeeperComplianceAgingData -RecordUids @($rows | Select-Object -ExpandProperty record_uid -Unique) ` -Snapshot $Snapshot -OwnerUserIdsForAging $OwnerUserIdsForAging foreach ($row in $rows) { $rowAging = $agingData[$row.record_uid] Add-Member -InputObject $row -NotePropertyName created -NotePropertyValue $rowAging['created'] -Force Add-Member -InputObject $row -NotePropertyName last_pw_change -NotePropertyValue $rowAging['last_pw_change'] -Force Add-Member -InputObject $row -NotePropertyName last_modified -NotePropertyValue $rowAging['last_modified'] -Force Add-Member -InputObject $row -NotePropertyName last_rotation -NotePropertyValue $rowAging['last_rotation'] -Force } } Write-KeeperComplianceStatus "Report row generation complete. Rows=$($rows.Count)." return @($rows) } function ConvertTo-KeeperComplianceDisplayRows { param( [Parameter(Mandatory = $true)]$Rows ) return @( $Rows | ForEach-Object { $row = [ordered]@{} foreach ($property in $_.PSObject.Properties) { if ($property.Name -eq 'permission_bits') { continue } if ($property.Name -eq 'shared_folder_uid') { $row[$property.Name] = @($property.Value) -join ', ' } else { $row[$property.Name] = $property.Value } } [PSCustomObject]$row } ) } <# .Synopsis Clear compliance report cache #> function Clear-KeeperComplianceCache { $script:ComplianceReportCache = @{ Entries = @{} } $script:ComplianceAgingCache = @{ Entries = @{} } $script:ComplianceReportLastSnapshotStatus = $null try { $entClear = getEnterprise if ($entClear -and $entClear.loader -and $entClear.loader.Auth) { Remove-KeeperComplianceSqliteCache -Enterprise $entClear -Auth $entClear.loader.Auth } } catch { Write-Verbose -Message "[compliance] SQLite cache clear skipped: $($_.Exception.Message)" } } function Invoke-KeeperComplianceReportSession { param( [Parameter(Mandatory = $true)][scriptblock]$ScriptBlock, [Parameter()][switch]$NoCache, [Parameter()][object[]]$ArgumentList ) try { if ($null -ne $ArgumentList -and $ArgumentList.Length -gt 0) { return & $ScriptBlock @ArgumentList } return & $ScriptBlock } finally { if ($NoCache) { Clear-KeeperComplianceCache } } } function Get-KeeperComplianceReport { <# .Synopsis Run enterprise compliance report .Parameter Format table (default), json, or csv .Parameter Output File path for json/csv output .Parameter Username Filter by enterprise username(s) .Parameter Node Filter by node name or ID .Parameter Pattern Filter rows by pattern (supports regex:, exact:, not: prefixes) .Parameter Regex Treat patterns as regular expressions .Parameter PatternMatchAll Require all patterns to match (AND); default is OR .Parameter JobTitle Filter by job title .Parameter Record Filter by record UID or title .Parameter Team Filter by team name or UID .Parameter Url Filter by URL substring .Parameter Shared Show only shared records .Parameter DeletedItems Show only deleted (trashed) records .Parameter ActiveItems Show only active records .Parameter Rebuild Force rebuild compliance cache .Parameter NoRebuild Use existing cache if available .Parameter NoCache Discard cache after the report .Parameter Aging Include password aging columns #> [CmdletBinding()] param( [Parameter()][ValidateSet('table', 'json', 'csv')][string]$Format = 'table', [Parameter()][string]$Output, [Parameter()][string[]]$Username, [Parameter()][string]$Node, [Parameter()][string[]]$JobTitle, [Parameter()][string[]]$Record, [Parameter()][string[]]$Team, [Parameter()][string[]]$Url, [Parameter()][string[]]$Pattern, [Parameter()][switch]$Regex, [Parameter()][switch]$PatternMatchAll, [Parameter()][switch]$Shared, [Parameter()][switch]$DeletedItems, [Parameter()][switch]$ActiveItems, [Parameter()][switch]$Rebuild, [Parameter()][switch]$NoRebuild, [Parameter()][switch]$NoCache, [Parameter()][switch]$Aging ) $reportRows = Invoke-KeeperComplianceReportSession -NoCache:$NoCache -ScriptBlock { Write-KeeperComplianceStatus "Starting compliance-report. Format=$Format Rebuild=$Rebuild NoRebuild=$NoRebuild NoCache=$NoCache Aging=$Aging." $fetchOwnerIds = Resolve-KeeperComplianceFetchOwnerIds -Username $Username -Team $Team -Node $Node if ($null -ne $fetchOwnerIds -and $fetchOwnerIds.Count -eq 0) { Write-Warning "No enterprise users matched the provided owner filters." } elseif ($null -eq $fetchOwnerIds) { Write-KeeperComplianceStatus "Owner pre-filter: all enterprise users." } else { Write-KeeperComplianceStatus "Owner pre-filter matched $($fetchOwnerIds.Count) user(s)." } $snapshot = Get-KeeperComplianceSnapshot -Rebuild:$Rebuild -NoRebuild:$NoRebuild -OwnerUserIds $fetchOwnerIds $ownerIdsForAging = if ($null -ne $fetchOwnerIds) { $fetchOwnerIds } else { $null } $reportRows = Get-KeeperComplianceReportRows -Snapshot $snapshot -Username $Username -Team $Team ` -JobTitle $JobTitle -Node $Node -Record $Record -Url $Url -Shared:$Shared ` -DeletedItems:$DeletedItems -ActiveItems:$ActiveItems -Aging:$Aging ` -OwnerUserIdsForAging $ownerIdsForAging if ($Pattern -and $Pattern.Count -gt 0) { $reportRows = Invoke-KeeperCompliancePatternFilterRows -Rows $reportRows -Patterns $Pattern ` -UseRegex:$Regex -MatchAll:$PatternMatchAll } return ,@($reportRows) } if ($reportRows.Count -eq 0) { Write-KeeperComplianceStatus "No compliance report rows matched the current filters." Write-Host "No compliance report rows found." return } $displayRows = ConvertTo-KeeperComplianceDisplayRows -Rows $reportRows if ($Format -eq 'table' -and -not ($Pattern -and $Pattern.Count -gt 0)) { $lastRecUid = '' $displayRows = @($displayRows | ForEach-Object { $curUid = [string]$_.record_uid $dr = [ordered]@{} foreach ($p in $_.PSObject.Properties) { if ($p.Name -eq 'record_uid' -and $curUid -and $curUid -eq $lastRecUid) { $dr[$p.Name] = '' } else { $dr[$p.Name] = $p.Value } } if ($curUid) { $lastRecUid = $curUid } [PSCustomObject]$dr }) } Write-KeeperComplianceStatus "Rendering $($reportRows.Count) row(s) as $Format." Write-KeeperReportOutput -Rows $reportRows -DisplayRows $displayRows -Format $Format -Output $Output -JsonDepth 6 } New-Alias -Name compliance-report -Value Get-KeeperComplianceReport function ConvertTo-KeeperUnixSecondsOptional { param( [Parameter()]$DateTimeValue ) if ($null -eq $DateTimeValue) { return $null } try { $dt = [datetime]$DateTimeValue return [int64][DateTimeOffset]::new($dt).ToUnixTimeSeconds() } catch { return $null } } function Resolve-KeeperAgingCutoffDateTime { param( [Parameter()][string]$Period, [Parameter()][string]$CutoffDate ) if ($Period -and $CutoffDate) { Write-Error "-Period and -CutoffDate cannot be used together." -ErrorAction Stop } if ($CutoffDate) { $fmts = @( 'yyyy-MM-dd', 'yyyy.MM.dd', 'yyyy/MM/dd', 'MM-dd-yyyy', 'MM.dd.yyyy', 'MM/dd/yyyy' ) $trimmed = $CutoffDate.Trim() foreach ($fmt in $fmts) { try { return [datetime]::ParseExact($trimmed, $fmt, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::None) } catch [System.FormatException] { continue } } try { return [datetime]::Parse($trimmed, [System.Globalization.CultureInfo]::CurrentCulture, [System.Globalization.DateTimeStyles]::None) } catch { } Write-Error "Unrecognized -CutoffDate format. Use yyyy-MM-dd, yyyy.MM.dd, yyyy/MM/dd, MM-dd-yyyy, MM.dd.yyyy, or MM/dd/yyyy." -ErrorAction Stop } $duration = $Period if ([string]::IsNullOrWhiteSpace($duration)) { Write-Host "" Write-Host "The default password aging period is 3 months." Write-Host "To change this value pass -Period (e.g. 10d for 10 days; 3m for 3 months; 1y for 1 year)." Write-Host "" $duration = '3m' } $co = $duration.Substring($duration.Length - 1).ToLowerInvariant() $numPart = $duration.Substring(0, [Math]::Max(0, $duration.Length - 1)) $va = 0 if (-not [int]::TryParse($numPart, [ref]$va)) { Write-Error "Invalid -Period value: $duration" -ErrorAction Stop } $va = [Math]::Abs($va) $days = $va if ($co -eq 'd') { } elseif ($co -eq 'm') { $days = $va * 30 } elseif ($co -eq 'y') { $days = $va * 365 } else { Write-Error "Invalid -Period suffix: use d, m, or y (e.g. 3m)." -ErrorAction Stop } return (Get-Date).AddDays(-$days) } # SIG # Begin signature block # MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDHlukH52EKCaXI # 5x7gNJ8cBTXWDwhAU+sjdH2aXDnSGKCCITswggWNMIIEdaADAgECAhAOmxiO+dAt # 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa # Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD # ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC # ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E # MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy # unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF # xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1 # 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB # MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR # WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6 # nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB # YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S # UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x # q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB # NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP # TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC # AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp # Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0 # aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB # LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc # Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov # Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy # oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW # juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF # mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z # twGpn1eqXijiuZQwggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0GCSqG # SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx # GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy # dXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTlaMGkx # CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4 # RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEzODQg # MjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C0Cit # eLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce2vnS # 1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0daE6ZM # swEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6TSXBC # Mo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoAFdE3 # /hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7OhD26j # q22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM1bL5 # OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z8ujo # 7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05huzU # tw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNYmtwm # KwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP/2NP # TLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0TAQH/ # BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYDVR0j # BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud # JQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0 # cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0 # cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E # PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz # dGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATANBgkq # hkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95RysQDK # r2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HLIvda # qpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5BtfQ/g+ # lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnhOE7a # brs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIhdXNS # y0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV9zeK # iwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/jwVYb # KyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYHKi8Q # xAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmCXBVm # zGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l/aCn # HwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZWeE4w # gga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0GCSqGSIb3DQEBCwUAMGIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH # NDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTlaMGkxCzAJBgNVBAYTAlVT # MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1 # c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTEwggIi # MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHTCphBcr48RsAcrHXbo0Zo # dLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPhof6pvF4uGjwjqNjfEvUi # 6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mAxAHeHYNnQxqXmRinvuNg # xVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBvMgEdgkFiDNYiOTx4OtiF # cMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps0wjUjsZvkgFkriK9tUKJ # m/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF83bRVFLeGkuAhHiGPMvS # GmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXiUOeSLsJygoLPp66bkDX1 # ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOMCZIVNSaz7BX8VtYGqLt9 # MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydPpOjL6s36czwzsucuoKs7 # Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrUG2ZdSoQbU2rMkpLiQ6bG # RinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+sdFUeEY0qVjPKOWug/G6 # X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAd # BgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYDVR0jBBgwFoAU7NfjgtJx # XWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUF # BwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGln # aWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5j # b20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJo # dHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNy # bDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQEL # BQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WIGjB/T8ObXAZz8OjuhUxj # aaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+IQhQE7jU/kXjjytJgnn0 # hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8Myb9rEVKChHyfpzee5kH0 # F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2th9y1IsA0QF8dTXqvcnT # mpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjajV/gxdEkMx1NKU4uHQcKf # ZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2Lr3ty9qIijanrUR3anzE # wlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFzeGxcytL5TTLL4ZaoBdqbh # OhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG7uEBYTptMSbhdhGQDpOX # gpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+NJpud/v4+7RWsWCiKi9EO # LLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckTetiSuEtQvLsNz3Qbp7wG # WqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszWkPZPubdcMIIG7TCCBNWg # AwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQG # EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0 # IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUgQ0Ex # MB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVowYzELMAkGA1UEBhMCVVMx # FzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBTSEEy # NTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIwMjUgMTCCAiIwDQYJKoZI # hvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5gVrMrV7pvUf+GcAoB38o3 # zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN+vo8dE2/pPvOx/Vj8Tch # TySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qome7MrxVyfQO9sMx6ZAWj # FDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ//nBZZREr4h/GI6Dxb2Uo # yrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouTMYFOnHoRh6+86Ltc5zjP # KHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8DD+nigNJFmt6LAHvH3KS # uNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnpJeItK/DhKbPxTTuGoX7w # JNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP51ho1zBp+xUIZkpSFA8vW # doUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49kPmk8iyyizNDIXj//cOg # rY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5PWPsWeupWs7NpChUk555K # 096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7YufAkprxMiXAJQ1XCmnCf # gPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTkO/zy # Me39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK6eQGfHrK4pBW9i/USezL # TjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwgZUGCCsG # AQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j # b20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp # Q2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZTSEEyNTYyMDI1Q0ExLmNy # dDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGln # aUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2U0hBMjU2MjAyNUNBMS5j # cmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEB # CwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP2zWLpQq1b4URGnwWBdEZ # D9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O6Lgjg8K8elC4+oWCqnU/ # ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskgiC3QYIUP2S3HQvHG1FDu # +WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMUBaB5bdrPbF6MRYs03h4o # bEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDFkxUGtMTaiLR9wjxUxu2h # ECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+zJNEsuEB7O7/cuvTQasn # M9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/lwd6UArb+BOVAkg2oOvol # /DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxlRcGG0LIhp6GvReQGgMgY # xQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2zRWVFjF7mcr4C34Mj3oc # CVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJgbaP5t2nGj/ULLi49xTcB # ZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOCIUjsarfNZzCCB0kwggUx # oAMCAQICEAHdzU+FVN9jCMv0HhHagNUwDQYJKoZIhvcNAQELBQAwaTELMAkGA1UE # BhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2Vy # dCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENB # MTAeFw0yNjA2MDUwMDAwMDBaFw0yNzA2MDQyMzU5NTlaMIHRMRMwEQYLKwYBBAGC # NzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMR0wGwYDVQQPDBRQ # cml2YXRlIE9yZ2FuaXphdGlvbjEQMA4GA1UEBRMHMzQwNzk4NTELMAkGA1UEBhMC # VVMxETAPBgNVBAgTCElsbGlub2lzMRAwDgYDVQQHEwdDaGljYWdvMR0wGwYDVQQK # ExRLZWVwZXIgU2VjdXJpdHkgSW5jLjEdMBsGA1UEAxMUS2VlcGVyIFNlY3VyaXR5 # IEluYy4wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCb4DRTV0sNQsa1 # 0YRh+bliabmLOVYr6S0+BSVvRJAN3SHP6x52i1Dkpki5xVDIH06ZnnsToVrgvTv+ # QxGwsn9SAPHEZ/PIJRFxbMR4ShDaptYyL4f0u4k/3HwRzIleWE4mTUonYH8BdgLw # /F53B7wa7VTDHtxXltYTibEOwJxYCOi4Zr2FYQhjw14/CHcqS3FSMs6YYU2T56+g # w819hQM3K0YlwTNOFoIm1v7/ZZZiJGH8uGDsvy1makh1Xyyo/wN8EbQ1nbslmePT # roPm9w7WqiP/yiq+CZHiuTk9JK5bEgkWG3ns+v25cI251WidJx3SU7IZnX0OTd6/ # ZdKhprD5Gcfy5GBbJdcYw2WycQRW0PT5BEt55xRE0heufkpDaTUN6RdOuJdXbkl0 # hV91IZIuhueEMCk3h5mDTlU5gImxqj0R/TbAxjSSGTKCeuYFkQIRqytSabdrZZ48 # kW5hOIZMVDY1f4kpPJa8UeEvDZXT3vrtj36aSJrwez2uh4FMNlkCAwEAAaOCAgIw # ggH+MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBT1 # SmCYU/7Yrz1fX66Ur5nSzlSYOzA9BgNVHSAENjA0MDIGBWeBDAEDMCkwJwYIKwYB # BQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMC # B4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0 # cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25p # bmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI # QTM4NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JT # QTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUA # A4ICAQBcavcUHNFEg872HDRq2+hRlnvaghCXv7X/6h9HSzjAQP3rt95BZty3ASqi # 2MYyGQLGdDl4DToe/WhajtEOBOYa83agW6tBvrfcKRrDrwJOMPTbwNYvn+GuiL4T # CKzXaytWiJJbrc5odc7Ecat2ZvJylpPmNainr4Q0LzzH23Gea/Mm/hIJTN4IGgrH # hrXiTIIW/ZUzrY6g8b3RZB4BA497n43wNdSqP+C3ntFw6NiGB4Z25SW4YntIxYPv # Kf37OVhF0xqxLC1sK/XxgK0EGQ6iaj8Ncpr2C5vSNZqfW2MndxOA1W67pgDpg83k # UWG+/YJeGhqOTF82/0kIzQXeI/lIqbnL/IJAJqSm/ROSpsGUKVbzk03cpTD55ZQX # WjM0fLirypBqY05T8gnh1L0fSwxr/SwJZ8OddivgyK1YOMn02nnsEG5kxBt9cMX4 # JCYABhypmAVDRvyYifEVdoFWv2gAXXW+PPRvlNa6E4aMCZrVcoKHiyeMAXOi1IC9 # mHvC2+foTSMFueq3AdnYfeKnZnAiKXKRhXcdHbQYcR2A7AIzIcqahPYr4FNEgb/E # /y/kypAkf0rMHlYl1kNqLs2Nv1UnMEHYT5YmDVLO63+1Trcw4zTZ70zuqIqeID/d # nbOlgtyG6DSRCL7f0E7kP18f4RoX5i1PkfeO4VJHsAuCeNG1qjGCBdkwggXVAgEB # MH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYD # VQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNI # QTM4NCAyMDIxIENBMQIQAd3NT4VU32MIy/QeEdqA1TANBglghkgBZQMEAgEFAKCB # hDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEE # AYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJ # BDEiBCCZRlKcj6KbFeXBcLkeC2Kd/AUTTVVkc4XP9D2IQHhDBDANBgkqhkiG9w0B # AQEFAASCAYBzcE8KtoaZEodniZn00R9L1GaifG7pT0WAcOY7DW3jHE3CFihURMPi # Mk1EF5bcIf9xL7Qj/8ss6I5nCJzR2VPU+RERJR53rRdnV/xEy/PSuLTZFZi1uBaE # 4DdSx+x3QcykvsPaVVDCiDXBbVbfX7OJLHmsNLHtCPvHWqpXmNUtoZjsGfxsZ+3R # g6SjHvpzUvLFlHJGuoq/FOuxj+uy7WHCCOn7jbeBXyOXSuKqDHBk7jUn0y1YyU7k # T1SSkB3iCagvRMr8Mr/abcKSyQp9BN7ek/BLfXHjCjSWVnL/xc86xtzOHP+AddB1 # ZgU8K9x7D5Ag4ZOxvGz8sm0EdPckBq9Yf+e69haAdZ40G7XBBqzI5FqG565dhOb3 # Kva4ulU9wf9xEM1Hbd0L2XoWNXJe500dfPZKjBDvQqtMx55F5q8tc/y8hOQfLkxz # GoI7I63HA1DD9I6bkHlyzU/zPTf8/QD8LxOCA+42W9sicT86aNyDEy9fr8ptYj4x # 4svcWseHaB+hggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD # VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD # ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg # Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN # AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwNjI0MTgwNDQwWjAv # BgkqhkiG9w0BCQQxIgQg/iU82gkCfwuMgnQJ5/eHcqh37tbW0gx0Y5CB3ofuPmAw # DQYJKoZIhvcNAQEBBQAEggIAL9eYdjaaZfz+pIbCxQOleoqsDUXWZP/mEJzLJiI5 # Ov02bWe0AIyB03QPYfgbDw3YcnAPYorTOhocpA6YOq2HT/ztitHzeiZWo0kFNad6 # YTtj58ulvIEIatILImcCnHyyT/HCaDyimVXjp9Bn4jxkURSnzNvRFbzv5DxXxK80 # 68S4MyjXOT5LbTFyfFA5iO/9nvTVZ5euZTOAo9T1GVQV408pJmHNiYqUUmQTrdI+ # wlcxY3WnocoSrxXuPWj1DvQlAX4DXCAOU4eoUIaQaOyiiA4BDNEXH83xWiv0vlbA # qsx/xmiLFHrGbwAGsw5LKar97dfGbZPg7VWUf0nYVTLRhBZZJZWy7LrBpN+8HJ1L # dWqND+RAG0FAQZpiT6Y7+9p9EufSFrwDnLuwtoGnq70ySNt2qAC2/oZwiSe2eH2P # vzPPYsOB4SlhFffdBRVVxf9gdOYsmGb8BMY43QLb+JpeWw00WdptfVjGJev77JVZ # sAispXcTYKXuBMC7X7VjziQY6rlOFJhNBT++rBUR0KgAvDkXVkixd8tW8zxz+kpC # NmkLQDNTrYJaY73YCSO8J7IOh0pnAucTVFxtlcIoSXD5bMIkGJQVwAxHqV2EHeeM # yyjchOGAvb9vhj0M9mqkr2aipyK2fUbMnts8EozriGaRCRUNU5F20AmgrhIknJcd # LGA= # SIG # End signature block |