SecurityAuditReport.ps1
|
#requires -Version 5.1 function Script:Get-KeeperJsonPropertyValue { param( [Parameter()] $Object, [Parameter(Mandatory = $true)] [string] $Name, [Parameter()] $DefaultValue = $null ) if ($null -eq $Object) { return $DefaultValue } $property = $Object.PSObject.Properties[$Name] if ($null -ne $property) { return $property.Value } return $DefaultValue } function Script:ConvertTo-KeeperInt { param( [Parameter()] $Value, [Parameter()] [int] $DefaultValue = 0 ) if ($null -eq $Value) { return $DefaultValue } $parsed = 0 if ([int]::TryParse($Value.ToString(), [ref]$parsed)) { return $parsed } return $DefaultValue } function Script:Show-KeeperSecurityAuditSyntaxHelp { Write-Host @" Security Audit Report Command Syntax Description: Column Name Description email e-mail address name user display name sync_pending whether security data sync is pending weak number of records whose password strength is in the weak category fair number of records whose password strength is in the fair category medium number of records whose password strength is in the medium category strong number of records whose password strength is in the strong category reused number of reused passwords unique number of unique passwords securityScore security score (0-100) twoFactorChannel 2FA - On/Off node enterprise node path BreachWatch Columns (with -BreachWatch): at_risk number of at-risk records passed number of passed records ignored number of ignored records Switches: -Format <table|json|csv> format of the report -Output <FILENAME> output to the given filename -SyntaxHelp display description of each column in the report -Node <name|UID> name(s) or UID(s) of node(s) to filter results by -BreachWatch display a BreachWatch security report -ShowUpdated calculate current security audit scores locally -Save push updated scores to Keeper -ScoreType <type> strong_passwords or default -AttemptFix reset invalid security-data for affected users -Force skip confirmation prompts "@ } function Script:Expand-KeeperSecurityAuditData { param( [Parameter(Mandatory = $true)] [string] $Json, [Parameter(Mandatory = $true)] [int] $NumberOfReusedPasswords ) $parsed = $Json | ConvertFrom-Json $secStats = Get-KeeperJsonPropertyValue $parsed 'securityAuditStats' $bwStats = Get-KeeperJsonPropertyValue $parsed 'bwStats' $hasSecStats = $null -ne $secStats function getIntValue { param( $TopLevel, $SecLevel, $BwLevel = $null ) $topValue = ConvertTo-KeeperInt (Get-KeeperJsonPropertyValue $parsed $TopLevel) if ($topValue -ne 0) { return $topValue } if ($null -ne $secStats) { $secValue = ConvertTo-KeeperInt (Get-KeeperJsonPropertyValue $secStats $SecLevel) if ($secValue -ne 0) { return $secValue } } if ($null -ne $bwStats -and -not [string]::IsNullOrEmpty($BwLevel)) { return (ConvertTo-KeeperInt (Get-KeeperJsonPropertyValue $bwStats $BwLevel)) } return 0 } $weak = getIntValue 'weak_record_passwords' 'weak_record_passwords' $fair = getIntValue 'fair_record_passwords' 'fair_record_passwords' $medium = getIntValue 'medium_record_passwords' 'medium_record_passwords' $strong = getIntValue 'strong_record_passwords' 'strong_record_passwords' $total = getIntValue 'total_record_passwords' 'total_record_passwords' $unique = [Math]::Max(0, $total - $NumberOfReusedPasswords) $passed = ConvertTo-KeeperInt (Get-KeeperJsonPropertyValue $bwStats 'passed_records') $atRisk = ConvertTo-KeeperInt (Get-KeeperJsonPropertyValue $bwStats 'at_risk_records') $ignored = ConvertTo-KeeperInt (Get-KeeperJsonPropertyValue $bwStats 'ignored_records') if (-not $hasSecStats) { $medium = $total - $weak - $strong } return @{ weak_record_passwords = $weak fair_record_passwords = $fair medium_record_passwords = $medium strong_record_passwords = $strong total_record_passwords = $total unique_record_passwords = $unique passed_records = $passed at_risk_records = $atRisk ignored_records = $ignored } } function Script:Get-KeeperSecurityStrengthCategory { # Maps Keeper password-strength scores to bucket names. # Score mapping: 0-1 = weak, 2 = fair, 3 = medium, 4+ = strong param([int] $Score) if ($Score -ge 4) { return 'strong' } if ($Score -eq 2) { return 'fair' } if ($Score -le 1) { return 'weak' } return 'medium' } function Script:Get-KeeperSecurityScoreDeltas { param( [Parameter()] $RecordSecurityData, [Parameter(Mandatory = $true)] [int] $Delta ) $deltas = @{ weak_record_passwords = 0 fair_record_passwords = 0 medium_record_passwords = 0 strong_record_passwords = 0 total_record_passwords = 0 unique_record_passwords = 0 passed_records = 0 at_risk_records = 0 ignored_records = 0 } $passwordStrength = ConvertTo-KeeperInt (Get-KeeperJsonPropertyValue $RecordSecurityData 'strength') $strengthKey = '{0}_record_passwords' -f (Get-KeeperSecurityStrengthCategory $passwordStrength) if ($deltas.ContainsKey($strengthKey)) { $deltas[$strengthKey] = $Delta } $deltas.total_record_passwords = $Delta # bw_result from the API: 2 = at-risk (breached), 1 = passed (clean), 0/other = ignored $breachWatchResult = ConvertTo-KeeperInt (Get-KeeperJsonPropertyValue $RecordSecurityData 'bw_result') if ($breachWatchResult -eq 2) { $deltas.at_risk_records = $Delta } elseif ($breachWatchResult -eq 1) { $deltas.passed_records = $Delta } else { $deltas.ignored_records = $Delta } return $deltas } function Script:Update-KeeperSecurityScoreDeltas { param( [Parameter(Mandatory = $true)] [hashtable] $Data, [Parameter(Mandatory = $true)] [hashtable] $Deltas ) foreach ($entry in $Deltas.GetEnumerator()) { if ($Data.ContainsKey($entry.Key)) { $Data[$entry.Key] += $entry.Value } else { $Data[$entry.Key] = $entry.Value } } } function Script:Decrypt-KeeperSecurityData { param( [Parameter()] $SecurityData, [Parameter(Mandatory = $true)] [Enterprise.EncryptedKeyType] $KeyType, [Parameter()] $RsaKey, [Parameter()] $EcKey ) if ($null -eq $SecurityData -or $SecurityData.IsEmpty) { return $null } $dataBytes = $SecurityData.ToByteArray() try { if ($KeyType -eq [Enterprise.EncryptedKeyType]::KtEncryptedByPublicKeyEcc) { if ($null -eq $EcKey) { return $null } $decryptedBytes = [KeeperSecurity.Utils.CryptoUtils]::DecryptEc($dataBytes, $EcKey) } else { if ($null -eq $RsaKey) { return $null } $decryptedBytes = [KeeperSecurity.Utils.CryptoUtils]::DecryptRsa($dataBytes, $RsaKey) } } catch { return $null } try { $json = [System.Text.Encoding]::UTF8.GetString($decryptedBytes) return ($json | ConvertFrom-Json) } catch { return $null } } function Script:Update-KeeperSecurityIncrementalData { param( [Parameter(Mandatory = $true)] [hashtable] $Data, [Parameter(Mandatory = $true)] [Authentication.SecurityReport] $SecurityReport, [Parameter()] $RsaKey, [Parameter()] $EcKey, [Parameter(Mandatory = $true)] [ref] $HasErrors ) $updatedData = @{} foreach ($key in $Data.Keys) { $updatedData[$key] = $Data[$key] } foreach ($incrementalData in $SecurityReport.SecurityReportIncrementalData) { $oldData = Decrypt-KeeperSecurityData $incrementalData.OldSecurityData $incrementalData.OldDataEncryptionType $RsaKey $EcKey $currentData = Decrypt-KeeperSecurityData $incrementalData.CurrentSecurityData $incrementalData.CurrentDataEncryptionType $RsaKey $EcKey if (($null -ne $oldData -and $null -eq (Get-KeeperJsonPropertyValue $oldData 'strength')) -or ($null -ne $currentData -and $null -eq (Get-KeeperJsonPropertyValue $currentData 'strength'))) { $HasErrors.Value = $true break } if ($null -ne $oldData) { $deltas = Get-KeeperSecurityScoreDeltas $oldData -1 Update-KeeperSecurityScoreDeltas $updatedData $deltas } if ($null -ne $currentData) { $deltas = Get-KeeperSecurityScoreDeltas $currentData 1 Update-KeeperSecurityScoreDeltas $updatedData $deltas } } if ($HasErrors.Value) { return $Data } return $updatedData } function Script:Format-KeeperSecurityReportData { param( [Parameter(Mandatory = $true)] [hashtable] $Data ) $report = [ordered]@{ securityAuditStats = [ordered]@{ weak_record_passwords = ConvertTo-KeeperInt $Data.weak_record_passwords fair_record_passwords = ConvertTo-KeeperInt $Data.fair_record_passwords medium_record_passwords = ConvertTo-KeeperInt $Data.medium_record_passwords strong_record_passwords = ConvertTo-KeeperInt $Data.strong_record_passwords total_record_passwords = ConvertTo-KeeperInt $Data.total_record_passwords unique_record_passwords = ConvertTo-KeeperInt $Data.unique_record_passwords } bwStats = [ordered]@{ passed_records = ConvertTo-KeeperInt $Data.passed_records at_risk_records = ConvertTo-KeeperInt $Data.at_risk_records ignored_records = ConvertTo-KeeperInt $Data.ignored_records } } return ($report | ConvertTo-Json -Depth 5 -Compress) } function Script:Get-KeeperStrongByTotal { param( [Parameter(Mandatory = $true)] [int] $Total, [Parameter(Mandatory = $true)] [int] $Strong ) if ($Total -eq 0) { return 0.0 } return ([double]$Strong / [double]$Total) } function Script:Get-KeeperSecurityScore { param( [Parameter(Mandatory = $true)] [int] $Total, [Parameter(Mandatory = $true)] [int] $Strong, [Parameter(Mandatory = $true)] [int] $Unique, [Parameter(Mandatory = $true)] [bool] $TwoFactorOn ) $strongByTotal = Get-KeeperStrongByTotal $Total $Strong $uniqueByTotal = if ($Total -eq 0) { 0.0 } else { [double]$Unique / [double]$Total } $twoFactorValue = if ($TwoFactorOn) { 1.0 } else { 0.0 } return ($strongByTotal + $uniqueByTotal + 1.0 + $twoFactorValue) / 4.0 } function Script:Confirm-KeeperSecurityAuditAction { param( [Parameter(Mandatory = $true)] [string] $Prompt, [Parameter()] [switch] $Force ) if ($Force) { return $true } $answer = Read-Host $Prompt return ($answer -match '^(?i)y(es)?$') } function Script:Clear-KeeperSecurityDataForUsers { param( [Parameter(Mandatory = $true)] [Enterprise] $Enterprise, [Parameter(Mandatory = $true)] [System.Collections.Generic.List[long]] $UserIds ) $chunkSize = 999 for ($offset = 0; $offset -lt $UserIds.Count; $offset += $chunkSize) { $request = New-Object Enterprise.ClearSecurityDataRequest $request.Type = [Enterprise.ClearSecurityDataType]::ForceClientResendSecurityData $endIndex = [Math]::Min($offset + $chunkSize - 1, $UserIds.Count - 1) for ($index = $offset; $index -le $endIndex; $index++) { $request.EnterpriseUserId.Add($UserIds[$index]) | Out-Null } $Enterprise.loader.Auth.ExecuteAuthRest("enterprise/clear_security_data", $request).GetAwaiter().GetResult() | Out-Null } Write-Host "Security data cleared for $($UserIds.Count) user(s)." } function Script:Test-KeeperRunReportsPrivilege { param( [Parameter(Mandatory = $true)] [Enterprise] $Enterprise ) if ($Script:Context.ManagedCompanyId -gt 0) { return $true } $enterpriseData = $Enterprise.enterpriseData $roleData = $Enterprise.roleData if ($null -eq $enterpriseData -or $null -eq $roleData) { return $false } $currentUser = $null if (-not $enterpriseData.TryGetUserByEmail($Enterprise.loader.Auth.Username, [ref]$currentUser)) { return $false } $userRoleIds = [System.Collections.Generic.HashSet[long]]::new() foreach ($roleId in $roleData.GetRolesForUser($currentUser.Id)) { [void]$userRoleIds.Add($roleId) } foreach ($managedNode in @($roleData.GetManagedNodes())) { if (-not $userRoleIds.Contains($managedNode.RoleId)) { continue } foreach ($privilege in @($roleData.GetPrivilegesForRoleAndNode($managedNode.RoleId, $managedNode.ManagedNodeId))) { if ([string]::Equals($privilege.PrivilegeType, 'RUN_REPORTS', [System.StringComparison]::OrdinalIgnoreCase)) { return $true } } } return $false } function Script:Test-KeeperEnterpriseAddonEnabled { param( [Parameter(Mandatory = $true)] [Enterprise] $Enterprise, [Parameter(Mandatory = $true)] [string] $AddonName ) $license = $Enterprise.enterpriseData.EnterpriseLicense if ($null -eq $license) { return $false } if ([string]::Equals($license.LicenseStatus, 'business_trial', [System.StringComparison]::OrdinalIgnoreCase)) { return $true } foreach ($addon in @($license.AddOns)) { if (-not [string]::Equals($addon.Name, $AddonName, [System.StringComparison]::OrdinalIgnoreCase)) { continue } if ($addon.Enabled -or $addon.IncludedInProduct) { return $true } } return $false } function Script:Write-KeeperSecurityAuditOutput { param( [Parameter(Mandatory = $true)] [object[]] $Rows, [Parameter(Mandatory = $true)] [bool] $ShowBreachWatch, [Parameter(Mandatory = $true)] [ValidateSet('table', 'json', 'csv')] [string] $Format, [Parameter()] [string] $Output ) $title = if ($ShowBreachWatch) { 'Security Audit Report (BreachWatch)' } else { 'Security Audit Report' } $internalFields = if ($ShowBreachWatch) { @('email', 'name', 'sync_pending', 'at_risk', 'passed', 'ignored') } else { @('email', 'name', 'sync_pending', 'weak', 'fair', 'medium', 'strong', 'reused', 'unique', 'securityScore', 'twoFactorChannel', 'node') } $displayRows = if ($ShowBreachWatch) { $Rows | Select-Object @{Name='Email';Expression={$_.email}}, @{Name='Name';Expression={$_.name}}, @{Name='Sync Pending';Expression={$_.sync_pending}}, @{Name='At Risk';Expression={$_.at_risk}}, @{Name='Passed';Expression={$_.passed}}, @{Name='Ignored';Expression={$_.ignored}} } else { $Rows | Select-Object @{Name='Email';Expression={$_.email}}, @{Name='Name';Expression={$_.name}}, @{Name='Sync Pending';Expression={$_.sync_pending}}, @{Name='Weak';Expression={$_.weak}}, @{Name='Fair';Expression={$_.fair}}, @{Name='Medium';Expression={$_.medium}}, @{Name='Strong';Expression={$_.strong}}, @{Name='Reused';Expression={$_.reused}}, @{Name='Unique';Expression={$_.unique}}, @{Name='Security Score';Expression={$_.securityScore}}, @{Name='2FA';Expression={$_.twoFactorChannel}}, @{Name='Node';Expression={$_.node}} } switch ($Format) { 'json' { $jsonRows = foreach ($row in $Rows) { $jsonRow = [ordered]@{} foreach ($field in $internalFields) { $jsonRow[$field] = $row.$field } [PSCustomObject]$jsonRow } $jsonText = $jsonRows | ConvertTo-Json -Depth 5 if ($Output) { Set-Content -Path $Output -Value $jsonText -Encoding utf8 Write-Host "Output written to $Output" return } return $jsonText } 'csv' { $csvText = ($displayRows | ConvertTo-Csv -NoTypeInformation) if ($Output) { Set-Content -Path $Output -Value $csvText -Encoding utf8 Write-Host "Output written to $Output" return } return $csvText } default { if ($Output) { $tableText = @($displayRows | Format-Table -Property * -AutoSize | Out-String -Width 8192) Set-Content -Path $Output -Value @($title, '', $tableText) -Encoding utf8 Write-Host "Output written to $Output" return } Write-Host "" Write-Host $title $displayRows | Format-Table -Property * -AutoSize | Out-String -Width 8192 } } } function Get-KeeperSecurityAuditReport { <# .SYNOPSIS Run a security audit report. .DESCRIPTION Retrieves enterprise security audit data from Keeper, decrypts each user's report payload, optionally applies incremental security data updates, and returns the results in table, JSON, or CSV form. #> [CmdletBinding()] param( [Parameter()] [ValidateSet('table', 'json', 'csv')] [string] $Format = 'table', [Parameter()] [string] $Output, [Parameter()] [switch] $SyntaxHelp, [Parameter()] [string[]] $Node, [Parameter()] [switch] $BreachWatch, [Parameter()] [switch] $ShowUpdated, [Parameter()] [switch] $Save, [Parameter()] [ValidateSet('strong_passwords', 'default')] [string] $ScoreType = 'default', [Parameter()] [switch] $AttemptFix, [Parameter()] [switch] $Force ) if ($SyntaxHelp) { Show-KeeperSecurityAuditSyntaxHelp return } [Enterprise]$enterprise = getEnterprise if ($null -eq $enterprise.enterpriseData) { Write-Warning "Enterprise data is not available." return } $treeKey = $enterprise.loader.TreeKey if ($null -eq $treeKey -or $treeKey.Length -eq 0) { Write-Warning "Enterprise tree key is not available. Ensure enterprise data is loaded." return } $nodeFilter = Resolve-EnterpriseNodeFilter -Enterprise $enterprise -Nodes $Node $showUpdatedData = $ShowUpdated.IsPresent -or $Save.IsPresent $saveReport = $Save.IsPresent $useStrongPasswordScoring = [string]::Equals($ScoreType, 'strong_passwords', [System.StringComparison]::OrdinalIgnoreCase) $rsaKey = $null if ($enterprise.loader.RsaPrivateKey -and $enterprise.loader.RsaPrivateKey.Length -gt 0) { try { $rsaKey = [KeeperSecurity.Utils.CryptoUtils]::LoadRsaPrivateKey($enterprise.loader.RsaPrivateKey) } catch { Write-Verbose "Failed to load RSA private key from enterprise loader: $_" } } $ecKey = $null if ($enterprise.loader.EcPrivateKey -and $enterprise.loader.EcPrivateKey.Length -gt 0) { try { $ecKey = [KeeperSecurity.Utils.CryptoUtils]::LoadEcPrivateKey($enterprise.loader.EcPrivateKey) } catch { Write-Verbose "Failed to load EC private key from enterprise loader: $_" } } $rows = New-Object System.Collections.Generic.List[object] $invalidUsers = New-Object 'System.Collections.Generic.List[long]' $updatedSecurityReports = New-Object 'System.Collections.Generic.List[Authentication.SecurityReport]' $saveBuildFailures = 0 $fromPage = 0L $complete = $false $asOfRevision = 0L $hasErrors = $false while (-not $complete) { $request = New-Object Authentication.SecurityReportRequest $request.FromPage = $fromPage $response = $enterprise.loader.Auth.ExecuteAuthRest( "enterprise/get_security_report_data", $request, [Authentication.SecurityReportResponse] ).GetAwaiter().GetResult() $asOfRevision = $response.AsOfRevision try { if ($null -eq $rsaKey -and $response.EnterprisePrivateKey -and -not $response.EnterprisePrivateKey.IsEmpty) { $keyData = [KeeperSecurity.Utils.CryptoUtils]::DecryptAesV2($response.EnterprisePrivateKey.ToByteArray(), $treeKey) $rsaKey = [KeeperSecurity.Utils.CryptoUtils]::LoadRsaPrivateKey($keyData) } if ($null -eq $ecKey -and $response.EnterpriseEccPrivateKey -and -not $response.EnterpriseEccPrivateKey.IsEmpty) { $keyData = [KeeperSecurity.Utils.CryptoUtils]::DecryptAesV2($response.EnterpriseEccPrivateKey.ToByteArray(), $treeKey) $ecKey = [KeeperSecurity.Utils.CryptoUtils]::LoadEcPrivateKey($keyData) } } catch { Write-Verbose "Failed to load enterprise private keys from response: $_" } foreach ($securityReport in $response.SecurityReport) { $user = $null if (-not $enterprise.enterpriseData.TryGetUserById($securityReport.EnterpriseUserId, [ref]$user)) { continue } if ($nodeFilter -and -not $nodeFilter.Contains($user.ParentNodeId)) { continue } $email = if ($user.Email) { $user.Email } else { $securityReport.EnterpriseUserId.ToString() } $name = if ($user.DisplayName) { $user.DisplayName } else { $email } $nodePath = Get-KeeperNodePath -NodeId $user.ParentNodeId $twoFactorOn = ($securityReport.TwoFactor -ne 'two_factor_disabled' -and -not [string]::IsNullOrEmpty($securityReport.TwoFactor)) $row = [ordered]@{ email = $email name = $name sync_pending = '' node = $nodePath reused = [int]$securityReport.NumberOfReusedPassword twoFactorChannel = if ($twoFactorOn) { 'On' } else { 'Off' } } if ($securityReport.EncryptedReportData -and $securityReport.EncryptedReportData.Length -gt 0) { try { $decryptedBytes = [KeeperSecurity.Utils.CryptoUtils]::DecryptAesV2($securityReport.EncryptedReportData.ToByteArray(), $treeKey) $json = [System.Text.Encoding]::UTF8.GetString($decryptedBytes) $data = Expand-KeeperSecurityAuditData -Json $json -NumberOfReusedPasswords $securityReport.NumberOfReusedPassword } catch { $invalidUsers.Add($securityReport.EnterpriseUserId) | Out-Null continue } } else { $data = @{ weak_record_passwords = 0 fair_record_passwords = 0 medium_record_passwords = 0 strong_record_passwords = 0 total_record_passwords = 0 unique_record_passwords = 0 passed_records = 0 at_risk_records = 0 ignored_records = 0 } } $rowIncrementalFailed = $false if ($showUpdatedData -and $securityReport.SecurityReportIncrementalData.Count -gt 0) { $data = Update-KeeperSecurityIncrementalData -Data $data -SecurityReport $securityReport -RsaKey $rsaKey -EcKey $ecKey -HasErrors ([ref]$rowIncrementalFailed) if ($rowIncrementalFailed) { $hasErrors = $true } else { $data.unique_record_passwords = [Math]::Max(0, (ConvertTo-KeeperInt $data.total_record_passwords) - [int]$securityReport.NumberOfReusedPassword) } } $row.weak = ConvertTo-KeeperInt $data.weak_record_passwords $row.fair = ConvertTo-KeeperInt $data.fair_record_passwords $row.medium = ConvertTo-KeeperInt $data.medium_record_passwords $row.strong = ConvertTo-KeeperInt $data.strong_record_passwords $row.total = ConvertTo-KeeperInt $data.total_record_passwords $row.unique = ConvertTo-KeeperInt $data.unique_record_passwords $row.passed = ConvertTo-KeeperInt $data.passed_records $row.at_risk = ConvertTo-KeeperInt $data.at_risk_records $row.ignored = ConvertTo-KeeperInt $data.ignored_records if ($row.unique -lt 0 -and $row.total -gt 0 -and $AttemptFix.IsPresent) { $invalidUsers.Add($securityReport.EnterpriseUserId) | Out-Null continue } if ($rowIncrementalFailed) { $row.sync_pending = 'Error' } elseif ($row.total -eq 0 -and $row.reused -ne 0) { $row.sync_pending = 'Yes' } if ($useStrongPasswordScoring) { $score = Get-KeeperStrongByTotal -Total $row.total -Strong $row.strong $displayScore = [int](100 * $score) } else { $score = Get-KeeperSecurityScore -Total $row.total -Strong $row.strong -Unique $row.unique -TwoFactorOn $twoFactorOn $displayScore = [int](100 * [Math]::Round($score, 2)) } $row.securityScore = $displayScore $rows.Add([PSCustomObject]$row) | Out-Null if ($saveReport -and -not $hasErrors) { try { $updatedSecurityReport = New-Object Authentication.SecurityReport $updatedSecurityReport.Revision = $asOfRevision $updatedSecurityReport.EnterpriseUserId = $securityReport.EnterpriseUserId $reportJson = Format-KeeperSecurityReportData -Data $data $jsonBytes = [System.Text.Encoding]::UTF8.GetBytes($reportJson) $updatedSecurityReport.EncryptedReportData = [Google.Protobuf.ByteString]::CopyFrom( [KeeperSecurity.Utils.CryptoUtils]::EncryptAesV2($jsonBytes, $treeKey) ) $updatedSecurityReports.Add($updatedSecurityReport) | Out-Null } catch { $saveBuildFailures++ } } } $complete = $response.Complete $fromPage = $response.ToPage + 1 } if ($invalidUsers.Count -gt 0) { Write-Warning "Decryption failed for $($invalidUsers.Count) user(s). Successfully decrypted: $($rows.Count)." } else { Write-Verbose "All $($rows.Count) user record(s) decrypted successfully." } if ($AttemptFix.IsPresent -and $invalidUsers.Count -gt 0) { if (Confirm-KeeperSecurityAuditAction -Prompt "Do you want to reset their security data? (y/n)" -Force:$Force.IsPresent) { Clear-KeeperSecurityDataForUsers -Enterprise $enterprise -UserIds $invalidUsers } else { Write-Host "Skipping security data reset." } } if ($saveReport -and $saveBuildFailures -gt 0) { Write-Warning "Unable to prepare $saveBuildFailures updated security report(s). Save skipped." } elseif ($saveReport -and $hasErrors) { Write-Warning "Updated security scores were not saved because some incremental security data could not be processed." } elseif ($saveReport -and $updatedSecurityReports.Count -gt 0) { if (Confirm-KeeperSecurityAuditAction -Prompt "Push updated security scores to Keeper? (y/n)" -Force:$Force.IsPresent) { try { $saveRequest = New-Object Authentication.SecurityReportSaveRequest $saveRequest.SecurityReport.AddRange($updatedSecurityReports) $enterprise.loader.Auth.ExecuteAuthRest("enterprise/save_summary_security_report", $saveRequest).GetAwaiter().GetResult() | Out-Null Write-Host "Security scores pushed to Keeper." } catch { Write-Warning "Error saving security reports: $($_.Exception.Message)" } } else { Write-Host "Save cancelled." } } $sortedRows = @($rows | Sort-Object email) Write-KeeperSecurityAuditOutput -Rows $sortedRows -ShowBreachWatch:$BreachWatch.IsPresent -Format $Format -Output $Output } function Get-KeeperBreachWatchReport { <# .SYNOPSIS Run a BreachWatch security report for all users in your enterprise. .DESCRIPTION Validates BreachWatch reporting access, then executes the security audit report pipeline in BreachWatch mode with updated report data saved back to Keeper. Note: this command pushes updated summary scores to Keeper automatically. #> [CmdletBinding()] param( [Parameter()] [ValidateSet('table', 'json', 'csv')] [string] $Format = 'table', [Parameter()] [string] $Output ) [Enterprise]$enterprise = getEnterprise if (-not (Test-KeeperRunReportsPrivilege -Enterprise $enterprise)) { throw "You do not have the required privilege to run a BreachWatch report" } if (-not (Test-KeeperEnterpriseAddonEnabled -Enterprise $enterprise -AddonName 'enterprise_breach_watch')) { throw "BreachWatch is not enabled for this enterprise." } Get-KeeperSecurityAuditReport -Format $Format -Output $Output -BreachWatch -Save -Force } Set-Alias -Name bw-report -Value Get-KeeperBreachWatchReport # SIG # Begin signature block # MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC/aYDGwNG2lW7L # lqeN6BxJmprsKp4+5MmcZXQ3fi4UAKCCITswggWNMIIEdaADAgECAhAOmxiO+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 # oAMCAQICEAe0P3SLJmcoVNrErUyxTt0wDQYJKoZIhvcNAQELBQAwaTELMAkGA1UE # BhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2Vy # dCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENB # MTAeFw0yNTEyMzEwMDAwMDBaFw0yOTAxMDIyMzU5NTlaMIHRMRMwEQYLKwYBBAGC # NzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMR0wGwYDVQQPDBRQ # cml2YXRlIE9yZ2FuaXphdGlvbjEQMA4GA1UEBRMHMzQwNzk4NTELMAkGA1UEBhMC # VVMxETAPBgNVBAgTCElsbGlub2lzMRAwDgYDVQQHEwdDaGljYWdvMR0wGwYDVQQK # ExRLZWVwZXIgU2VjdXJpdHkgSW5jLjEdMBsGA1UEAxMUS2VlcGVyIFNlY3VyaXR5 # IEluYy4wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCUcNMoSVmxAi0a # vG+StFJMNFFTUIOo3HdBZ+0gqA1XpNgUx11vB1vCZrvFsD9m5oA58tdp4gZN3LmQ # aMvCl2ANUT7MilI02Hf1RWlygBzon6iE0GpU3lgRrwrk1dhtLpGsR6dbMKUUHprc # vKpXk90/VN+vhzY1uik1tCTxkDCPu/AYJg7m9+tR2KqvMuYMaMLhii66eWUAGsBC # h/uZxjkGoJF6qZ0DgFd7rW7VYljbfYSNPeZNGTDgB0J/wOsKl0mn612DTseIvAKt # 4vra/FLFukyEyStnfQ8lWYDcLLCMCjNVrzGipmT5E2iyx7Y1RZCIpNwVogp3Ixbk # Gbq5A/41YNOLLd4cFewyB2F037RevBCRsUODZEt1qBf7Jbu3DiYo1G+zTj9E0R1s # FzyijcfdsTm6X5ble+yCJeGkX5XgsyPnZpyz/FX9Fr0N9pMPGWwW2PKyHEnSytXm # 0Dxdq2P4mA4CBUxq7YoV26L2PF6QEh9BQdXTPcnLysUv7SI/a0ECAwEAAaOCAgIw # ggH+MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBRG # 4H6CH8pvNX632bsdnrda4MtJLDA9BgNVHSAENjA0MDIGBWeBDAEDMCkwJwYIKwYB # BQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMC # B4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0 # cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25p # bmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI # QTM4NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JT # QTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUA # A4ICAQA1Wlq0WzJa3N6DgjgBU7nagIJBab1prPARXZreX1MOv9VjnS5o0CrfQLr6 # z3bmWHw7xT8dt6bcSwRixqvPJtv4q8Rvo80O3eUMvMxQzqmi7z1zf+HG+/3G4F+2 # IYegvPc8Ui151XCV9rjA8tvFWRLRMX0ZRxY1zfT027HMw0iYL20z44+Cky//FAnL # iRwoNDGiRkZiHbB9YOftPAYNMG3gm1z3zOW5RdfKPrqvMuijE+dfyLIAA6Immpzu # FMH+Wgn8NnSlot9b4YKycaqqdjd7wXDjPub/oQ7VShuCSBWj+UNOTVh0vcZGackc # H1DLVgwp2dcKlxJiQKtkHT/T6LloY6LTe6+8wkVkr8EAv1W+q/+M1a4Ao+ykFbIA # 2LBEmA9qdgoLtenAYIiEg+48SjMPgyBbVPE3bhL1vIqjEIxYCfdmi6wx33oYX7HB # +bJ7zitHw4GgtpfPV8y8QRZImKmeDOKyXjQPDmQM/Eglm/Ns0GzBkVXM8h6UI34b # WZrHz9sbLSE20m5Svmxftvw5zju+I3WsmS/stNfWlOkwU0niUgwPHaz21kjXEA5A # g+aqv26wodqZcnGOlChoWDvSJ8KKgdOFbeAYKAMp1NY7iWV315zpGH19RipCR1NH # 0ND8iIubk3WGNf2rzEfqlOi3h2ywqVkU6AKXHdO5JV4otSKKEDGCBdkwggXVAgEB # MH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYD # VQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNI # QTM4NCAyMDIxIENBMQIQB7Q/dIsmZyhU2sStTLFO3TANBglghkgBZQMEAgEFAKCB # hDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEE # AYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJ # BDEiBCA77RouiJzhJTCktDFNkzcsauI57kMeBqniVTkGDbbf5DANBgkqhkiG9w0B # AQEFAASCAYBvxRJaiXPid8HcH2YkZ8Lca+CMTPaUcdrzNT8QH25p0kc4ktcGVp6w # mbzSPW2wbBHReJ4SFrZF0qCJC5A9YG++c6HDqVvm7hpP9AKXewXJbqISOwhg5ehO # 8xCYqGcwZjcWudj/4CJP2dno8tKJNPiWApisR7dLUHvTw6uWaF2xUSOmdz+sjIeN # 3wMDj7a24torNz0Eoi6wZwa3lHVK0eChSL7KYM9pO8+q9ORqOSV8pZJmbXYFnCu/ # r7h+1uPtXYMrQ9Q8g2Fu7mKnWs4mLDxliWq8kKYdz0oEeZYKwMUko13aFGF0sdYF # 7qW9aPnT4wpXxZ7De7UkIe76rPRu2diPvZ+ab8haC13XfA1Fp9aBVw88Ai926dCe # a9K8S5noIHwlY1fl/dFjwT/2pD33KP/dlr5Y6bGrYLra0QD2OD6I8RfzUnS0PaNS # Jaf/xMaq2MN5dAq8K1dsSCe1yPuzdrUSEy3Bt7WBvFgDv8MPfWDl7vDQoSkolZZc # YfGIssVt5QShggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD # VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD # ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg # Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN # AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwNTA1MTgxNzA4WjAv # BgkqhkiG9w0BCQQxIgQgy9hfOSdLm4+tTpphMR4WvYNdVVpSLYZKcPcvh2+YI2gw # DQYJKoZIhvcNAQEBBQAEggIAhJ52T7kKbihFAQCzlUhamK1VEqLv0ZiZv8jDiaD/ # 0eSLGY5GFeWmNsPx9GSS5jbF7T3OOGY4Nm1tgQuMD8HQelpdlhIq41a+/UqJwmdH # hHOqpmXtMFTvt18XYTXy7AWE6KRuFWr4nHjSIvg22lREQ8m+zM12SBhw8hTZUNfo # 5VEjeUljO2TORMuxTYyNnnncREoZojh3nrcV0Rx1QXecJsTM3Amet0nzkLzvwkLY # 9aZLQLoggAY4ms4qfX/cPFjZxZgAogTZOXbDdB0VjY0l7zwTpsA/QhdvAMquLLFl # YEwGnlZ3BgFhnEcqdM2FBR7h+P4SaPeBo4FJysX27clslo/OWUfS5Cc1Ti8jcoDU # /RLN56T0oAzXKNBMSrEHwbIm+8O3LvfyuTLG5t3JnqxYe3ZOe3dBhr3l3/6BAsYt # 8KKOJ4WOLQT48tOxs45sE3oMObb2m95AM0Ov/2aP3w6IN+dJgS/ErxNn05z+OpmZ # +t8KHsmqzAhs+/ztXg5bSPf0LAisf7ioFDPd5U0emfXqJ30adpwpUvq+LGyu055w # M63CO/Mi83QsJnckwyhGeuG2C2125sTzTUl4+Pb8EE3fBHlvKfBt8dftCG2K1djq # 6V4CuAWCvauoVsPw5OgwuE+xY7Jpwv69ro5+Nni5J90slLogiJWj2c+52igPP9iL # EJk= # SIG # End signature block |