PAM/PamCommon.ps1

#requires -Version 5.1

# Shared PAM helpers

$script:PamRotationScriptVerbs = New-Object 'System.Collections.Generic.HashSet[string]'
[void]$script:PamRotationScriptVerbs.Add('list')
[void]$script:PamRotationScriptVerbs.Add('l')
[void]$script:PamRotationScriptVerbs.Add('add')
[void]$script:PamRotationScriptVerbs.Add('new')
[void]$script:PamRotationScriptVerbs.Add('n')
[void]$script:PamRotationScriptVerbs.Add('a')
[void]$script:PamRotationScriptVerbs.Add('edit')
[void]$script:PamRotationScriptVerbs.Add('e')
[void]$script:PamRotationScriptVerbs.Add('delete')
[void]$script:PamRotationScriptVerbs.Add('d')

function script:testPamRotationScriptVerb {
    Param ([string] $Value)
    if ([string]::IsNullOrWhiteSpace($Value)) {
        return $false
    }
    $trimmed = $Value.Trim()
    foreach ($verb in $script:PamRotationScriptVerbs) {
        if ([string]::Equals($verb, $trimmed, [System.StringComparison]::OrdinalIgnoreCase)) {
            return $true
        }
    }
    return $false
}

function script:encodePamByteString {
    Param (
        [Google.Protobuf.ByteString] $ByteString
    )

    if ($null -eq $ByteString -or $ByteString.IsEmpty) {
        return ''
    }

    return [KeeperSecurity.Utils.CryptoUtils]::Base64UrlEncode($ByteString.ToByteArray())
}

function script:resolvePamRotationRecord {
    Param (
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.VaultOnline] $Vault,
        [Parameter(Mandatory = $true)]
        [string] $Identifier,
        [Parameter(Mandatory = $true)]
        [System.Collections.Generic.HashSet[string]] $AllowedTypes
    )

    try {
        return [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord($Vault, $Identifier.Trim(), $AllowedTypes)
    }
    catch [System.InvalidOperationException] {
        Write-Output $_.Exception.Message
        throw
    }
}

function script:testPamRotationScriptOwnerException {
    Param (
        [Parameter(Mandatory = $true)]
        [System.Exception] $Exception
    )

    $ex = $Exception
    while ($null -ne $ex) {
        if ($ex -is [KeeperSecurity.Authentication.KeeperApiException]) {
            $code = [string]$ex.Code
            if ($code -eq 'only_owner_can_modify_scripts' -or $code -eq 'RS_ONLY_OWNER_CAN_MODIFY_SCRIPTS') {
                return $true
            }
        }
        $ex = $ex.InnerException
    }
    return $false
}

function script:updatePamRotationScriptRecord {
    Param (
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.VaultOnline] $Vault,
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.TypedRecord] $Record,
        [Parameter(Mandatory = $true)]
        [ValidateSet('add', 'edit', 'remove')]
        [string] $Action
    )

    try {
        $Vault.UpdateRecord($Record).GetAwaiter().GetResult() | Out-Null
        return $true
    }
    catch {
        if (testPamRotationScriptOwnerException -Exception $_.Exception) {
            switch ($Action) {
                'add' { Write-Output 'Only the record owner can attach post-rotation scripts.' }
                'edit' { Write-Output 'Only the record owner can edit post-rotation scripts.' }
                'remove' { Write-Output 'Only the record owner can remove post-rotation scripts.' }
            }
            return $false
        }
        throw
    }
}

function script:getPamRotationVault {
    $vault = getVault
    if (-not $vault) {
        Write-Error -Message 'Vault is not available.' -ErrorAction Stop
    }

    return $vault
}

function script:confirmPamYesNo {
    Param (
        [Parameter(Mandatory = $true)]
        [string] $Prompt,
        [bool] $DefaultYes = $true
    )

    $suffix = if ($DefaultYes) { ' [Y/n]: ' } else { ' [y/N]: ' }
    $answer = Read-Host ($Prompt.TrimEnd() + $suffix)
    if ([string]::IsNullOrWhiteSpace($answer)) {
        return $DefaultYes
    }

    return ($answer.Trim().StartsWith('y', [System.StringComparison]::OrdinalIgnoreCase))
}

function script:toPamUidBytes {
    Param ([string] $Uid)
    return [KeeperSecurity.Utils.CryptoUtils]::Base64UrlDecode($Uid)
}

function script:enumeratePamTypedFields {
    Param ([KeeperSecurity.Vault.TypedRecord] $Record)

    if ($null -eq $Record) {
        return @()
    }

    $fields = New-Object System.Collections.Generic.List[object]
    if ($null -ne $Record.Fields) {
        foreach ($f in $Record.Fields) { [void]$fields.Add($f) }
    }
    if ($null -ne $Record.Custom) {
        foreach ($f in $Record.Custom) { [void]$fields.Add($f) }
    }
    return , $fields.ToArray()
}

function script:getPamDefaultScheduleFromConfig {
    Param ([KeeperSecurity.Vault.TypedRecord] $Config)

    if ($null -eq $Config) {
        return $null
    }

    $scheduleField = $null
    foreach ($field in (enumeratePamTypedFields -Record $Config)) {
        if ($field.FieldName -ne 'schedule') {
            continue
        }
        if ($field.FieldLabel -eq 'defaultRotationSchedule') {
            $scheduleField = $field
            break
        }
        if ($null -eq $scheduleField) {
            $scheduleField = $field
        }
    }

    if ($null -eq $scheduleField -or $null -eq $scheduleField.Values -or $scheduleField.Values.Count -eq 0) {
        return $null
    }

    $value = $scheduleField.Values[0]
    if ($null -eq $value) {
        return $null
    }

    if ([string]::IsNullOrWhiteSpace($value.Type) -or
        [string]::Equals($value.Type, 'On-Demand', [System.StringComparison]::OrdinalIgnoreCase)) {
        return New-Object 'System.Collections.Generic.List[object]'
    }

    $dict = New-Object 'System.Collections.Generic.Dictionary[string,object]'
    $dict['type'] = $value.Type
    if (-not [string]::IsNullOrEmpty($value.Time)) {
        $dict['time'] = $value.Time
        $dict['utcTime'] = $value.Time
    }
    if (-not [string]::IsNullOrEmpty($value.Weekday)) { $dict['weekday'] = $value.Weekday }
    if (-not [string]::IsNullOrEmpty($value.Month)) { $dict['month'] = $value.Month }
    if (-not [string]::IsNullOrEmpty($value.MonthDay)) { $dict['monthDay'] = $value.MonthDay }
    if (-not [string]::IsNullOrEmpty($value.IntervalCount)) { $dict['intervalCount'] = $value.IntervalCount }
    if (-not [string]::IsNullOrEmpty($value.Cron)) { $dict['cron'] = $value.Cron }
    if (-not [string]::IsNullOrEmpty($value.TimeZone)) { $dict['tz'] = $value.TimeZone }

    $list = New-Object 'System.Collections.Generic.List[object]'
    [void]$list.Add($dict)
    return , $list
}

function script:toPamScheduleObjectList {
    Param ($ScheduleData)

    $list = New-Object 'System.Collections.Generic.List[object]'
    if ($null -eq $ScheduleData) {
        return , $list
    }

    if ($ScheduleData -is [System.Collections.Generic.List[object]]) {
        return , $ScheduleData
    }

    if ($ScheduleData -is [System.Collections.IDictionary]) {
        [void]$list.Add($ScheduleData)
        return , $list
    }

    if (($ScheduleData -is [System.Collections.IEnumerable]) -and -not ($ScheduleData -is [string])) {
        foreach ($item in $ScheduleData) {
            if ($null -ne $item) {
                [void]$list.Add($item)
            }
        }
        return , $list
    }

    [void]$list.Add($ScheduleData)
    return , $list
}

function script:getPamDefaultResourceUidFromConfig {
    Param ([KeeperSecurity.Vault.TypedRecord] $Config)

    if ($null -eq $Config -or $null -eq $Config.Fields) {
        return $null
    }

    foreach ($field in $Config.Fields) {
        if ($field.FieldName -ne 'pamResources') {
            continue
        }
        if ($null -eq $field.Values -or $field.Values.Count -eq 0) {
            return $null
        }
        $refs = $field.Values[0].ResourceRef
        if ($null -eq $refs -or $refs.Length -ne 1) {
            return $null
        }
        return [string]$refs[0]
    }
    return $null
}

function script:testPamNoopRecord {
    Param ([KeeperSecurity.Vault.TypedRecord] $Record)

    if ($null -eq $Record -or $null -eq $Record.Fields) {
        return $false
    }

    foreach ($field in $Record.Fields) {
        if ($field.FieldName -ne 'NOOP') {
            continue
        }
        if ($null -eq $field.Values -or $field.Values.Count -eq 0) {
            return $false
        }
        $value = [string]$field.Values[0]
        return (-not [string]::IsNullOrEmpty($value) -and
            [string]::Equals($value.Trim(), 'TRUE', [System.StringComparison]::OrdinalIgnoreCase))
    }
    return $false
}

function script:formatPamPasswordComplexityInfoDisplay {
    Param ([KeeperSecurity.Utils.PasswordGenerationOptions] $Rules)

    if ($null -eq $Rules) {
        return ''
    }

    $chars = $Rules.SpecialCharacters
    if ([string]::IsNullOrEmpty($chars)) {
        $chars = '!@#$%^&*()_+=-[];,.<>?'
    }

    return ("Length: {0}; Lowercase: {1}; Uppercase: {2}; Digits: {3}; Symbols: {4}; Special Characters: {5}" -f `
        $Rules.Length, $Rules.Lower, $Rules.Upper, $Rules.Digit, $Rules.Special, $chars)
}

function script:testPamUsesDefaultRotationSchedule {
    Param (
        [KeeperSecurity.Vault.VaultOnline] $Vault,
        [string] $RecordUid,
        [string] $ConfigUid
    )

    if ($null -eq $Vault -or [string]::IsNullOrWhiteSpace($RecordUid) -or [string]::IsNullOrWhiteSpace($ConfigUid)) {
        return $false
    }

    $config = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord(
        $Vault, $ConfigUid.Trim(), [KeeperSecurity.Plugins.PAM.PamRecordTypes]::Configuration)
    if ($null -eq $config) {
        return $false
    }

    $defaultSchedule = toPamScheduleObjectList (getPamDefaultScheduleFromConfig -Config $config)
    if ($defaultSchedule.Count -eq 0) {
        return $false
    }

    $cached = $Vault.GetRecordRotation($RecordUid.Trim())
    if ($null -eq $cached -or [string]::IsNullOrWhiteSpace($cached.Schedule)) {
        return $false
    }

    try {
        $recordSchedule = toPamScheduleObjectList (
            [KeeperSecurity.Plugins.PAM.RotationUtils]::ParseScheduleJsonString($cached.Schedule))
    }
    catch {
        return $false
    }

    if ($recordSchedule.Count -eq 0) {
        return $false
    }

    return [string]::Equals(
        [KeeperSecurity.Plugins.PAM.RotationUtils]::SerializeScheduleData($recordSchedule),
        [KeeperSecurity.Plugins.PAM.RotationUtils]::SerializeScheduleData($defaultSchedule),
        [System.StringComparison]::Ordinal)
}

function script:resolvePamRotationTargetRecords {
    Param (
        [KeeperSecurity.Vault.VaultOnline] $Vault,
        [string] $Record,
        [string] $Folder
    )

    $recordUids = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::Ordinal)
    $rotationTypes = [KeeperSecurity.Plugins.PAM.PamRecordTypes]::Rotation

    if (-not [string]::IsNullOrWhiteSpace($Record)) {
        $resolved = $null
        try {
            $resolved = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord($Vault, $Record.Trim(), $rotationTypes)
        }
        catch [System.InvalidOperationException] {
            Write-Error -Message $_.Exception.Message -ErrorAction Stop
        }

        if ($null -ne $resolved) {
            [void]$recordUids.Add($resolved.Uid)
        }
        else {
            Write-Output ("Record `"{0}`" not found." -f $Record.Trim())
        }
    }

    if (-not [string]::IsNullOrWhiteSpace($Folder)) {
        $folderName = $Folder.Trim()
        $folderNode = $null
        if (-not $Vault.TryGetFolder($folderName, [ref]$folderNode)) {
            Write-Output ("Folder `"{0}`" not found. Skipping." -f $folderName)
        }
        else {
            $folderUids = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::Ordinal)
            $stack = New-Object System.Collections.Generic.Stack[object]
            $stack.Push($folderNode)
            while ($stack.Count -gt 0) {
                $node = $stack.Pop()
                if ($null -eq $node) { continue }
                if (-not [string]::IsNullOrEmpty($node.FolderUid)) {
                    [void]$folderUids.Add($node.FolderUid)
                }
                if ($null -ne $node.Subfolders) {
                    foreach ($subUid in $node.Subfolders) {
                        $child = $null
                        if ($Vault.TryGetFolder($subUid, [ref]$child) -and $null -ne $child) {
                            $stack.Push($child)
                        }
                    }
                }
            }

            foreach ($folderUid in $folderUids) {
                $folder = $null
                if (-not $Vault.TryGetFolder($folderUid, [ref]$folder) -or $null -eq $folder) {
                    continue
                }
                if ($null -eq $folder.Records) { continue }
                foreach ($uid in $folder.Records) {
                    if ($recordUids.Contains($uid)) { continue }
                    $typed = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord($Vault, $uid, $rotationTypes)
                    if ($null -ne $typed) {
                        [void]$recordUids.Add($uid)
                    }
                }
            }
        }
    }

    $records = New-Object System.Collections.Generic.List[KeeperSecurity.Vault.TypedRecord]
    foreach ($uid in $recordUids) {
        $rec = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord($Vault, $uid, $rotationTypes)
        if ($null -ne $rec) {
            [void]$records.Add($rec)
        }
    }
    return , $records
}

function script:invokeKeeperPamRotationEdit {
    Param (
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Authentication.IAuthentication] $Auth,
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.VaultOnline] $Vault,
        [hashtable] $Options
    )

    $Vault.SyncDown().GetAwaiter().GetResult() | Out-Null

    $complexityRules = $null
    if ($null -ne $Options.Complexity -and -not [string]::IsNullOrWhiteSpace([string]$Options.Complexity)) {
        $complexityRules = [KeeperSecurity.Plugins.PAM.RotationUtils]::ParsePasswordComplexityRules([string]$Options.Complexity)
    }
    elseif (-not [string]::IsNullOrWhiteSpace([string]$Options.ComplexityJson)) {
        try {
            $bytes = [System.Text.Encoding]::UTF8.GetBytes([string]$Options.ComplexityJson)
            $complexityRules = [KeeperSecurity.Plugins.PAM.RotationUtils]::ParsePasswordComplexityJson($bytes)
        }
        catch [System.ArgumentException] {
            Write-Error -Message $_.Exception.Message -ErrorAction Stop
        }
    }

    try {
        $scheduleData = [KeeperSecurity.Plugins.PAM.RotationUtils]::ParseScheduleOptions(
            [string]$Options.ScheduleJson,
            [string]$Options.ScheduleCron,
            [bool]$Options.OnDemand,
            [bool]$Options.ScheduleConfig)
        if ($null -ne $scheduleData) {
            $scheduleData = toPamScheduleObjectList $scheduleData
        }
    }
    catch {
        Write-Error -Message $_.Exception.Message -ErrorAction Stop
    }

    $pamConfigs = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::GetConfigurationRecords($Vault)
    $configRecord = $null
    if (-not [string]::IsNullOrWhiteSpace([string]$Options.Config)) {
        $configRecord = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord(
            $Vault, [string]$Options.Config, [KeeperSecurity.Plugins.PAM.PamRecordTypes]::Configuration)
        if ($null -eq $configRecord) {
            Write-Error -Message ("Record uid {0} is not a PAM Configuration record." -f $Options.Config) -ErrorAction Stop
        }
    }

    if (-not [string]::IsNullOrWhiteSpace([string]$Options.RotationProfile)) {
        $allowed = @('general', 'iam_user', 'scripts_only', 'saas')
        $profileCheck = ([string]$Options.RotationProfile).Trim().ToLowerInvariant()
        if ($allowed -notcontains $profileCheck) {
            Write-Error -Message 'Invalid rotation profile. Allowed values: general, iam_user, scripts_only, saas' -ErrorAction Stop
        }
    }

    $records = resolvePamRotationTargetRecords -Vault $Vault -Record ([string]$Options.Record) -Folder ([string]$Options.Folder)
    if ($records.Count -eq 0) {
        $types = [string]::Join(', ', [KeeperSecurity.Plugins.PAM.PamRecordTypes]::Rotation)
        Write-Error -Message ("No PAM record is found. Valid PAM record types: {0}" -f $types) -ErrorAction Stop
    }

    Write-Output ("Selected {0} PAM record(s) for rotation" -f $records.Count)

    $skipped = New-Object System.Collections.Generic.List[object]
    $valid = New-Object System.Collections.Generic.List[object]
    $requests = New-Object System.Collections.Generic.List[Router.RouterRecordRotationRequest]
    $configuredResources = New-Object System.Collections.Generic.List[hashtable]
    $resourceTypes = [KeeperSecurity.Plugins.PAM.PamRecordTypes]::Resource

    foreach ($record in $records) {
        $typeName = [string]$record.TypeName
        if ($resourceTypes.Contains($typeName)) {
            try {
                invokePamConfigureResourceRecord `
                    -Auth $Auth -Vault $Vault -Record $record -Options $Options `
                    -PamConfigs $pamConfigs -ConfigRecord $configRecord `
                    -Skipped $skipped -ConfiguredResources $configuredResources
            }
            catch [System.InvalidOperationException] {
                if (-not [string]::IsNullOrWhiteSpace([string]$Options.Folder)) {
                    [void]$skipped.Add(@($record.Uid, $record.Title, 'Error', $_.Exception.Message))
                }
                else {
                    Write-Error -Message $_.Exception.Message -ErrorAction Stop
                }
            }
            continue
        }

        if ($typeName -ne 'pamUser') {
            continue
        }

        try {
            $editContext = resolvePamRecordEditContext -Options $Options -Record $record
            $resourceUidForDag = $null
            $configUidForDag = if ($null -ne $configRecord) { $configRecord.Uid } else { $null }

            if ($editContext.Profile -ne 'iam_user' -and -not $editContext.Noop -and -not [bool]$Options.ScheduleOnly) {
                $resourceUidForDag = resolvePamResourceUidForDag `
                    -Vault $Vault -Record $record -Options $Options `
                    -ConfigRecord $configRecord -PamConfigs $pamConfigs
                if ([string]::IsNullOrEmpty($configUidForDag)) {
                    $cached = $Vault.GetRecordRotation($record.Uid)
                    $configUidForDag = $cached.ConfigurationUid
                }
            }

            if (-not [string]::IsNullOrEmpty($resourceUidForDag) -and -not [string]::IsNullOrEmpty($configUidForDag)) {
                [KeeperSecurity.Plugins.PAM.PamRotationGraphEdit]::ConfigureUserAsync(
                    $Auth, $Vault, $record, $resourceUidForDag, $configUidForDag,
                    [bool]$editContext.Noop, [bool]$Options.ScheduleOnly
                ).GetAwaiter().GetResult() | Out-Null
                # NSF only: sync so rotation revision is fresh after graph link.
                if ([KeeperSecurity.Plugins.PAM.PamVaultHelpers]::IsKeeperNSFRecord($Vault, $record.Uid)) {
                    $Vault.SyncDown().GetAwaiter().GetResult()
                }
            }

            $request = $null
            if (tryBuildPamUserRotationRequest `
                    -Vault $Vault -Record $record -Options $Options `
                    -ConfigRecord $configRecord -PamConfigs $pamConfigs `
                    -ScheduleData $scheduleData -ComplexityRules $complexityRules `
                    -EditContext $editContext -Skipped $skipped -Valid $valid `
                    -Request ([ref]$request)) {
                [void]$requests.Add($request)
            }
        }
        catch [System.InvalidOperationException] {
            if (-not [string]::IsNullOrWhiteSpace([string]$Options.Folder)) {
                [void]$skipped.Add(@($record.Uid, $record.Title, 'Error', $_.Exception.Message))
            }
            else {
                Write-Error -Message $_.Exception.Message -ErrorAction Stop
            }
        }
    }

    if ($skipped.Count -gt 0) {
        Write-Output ''
        Write-Output 'The following record(s) were skipped:'
        Write-Output ("{0,-22} {1,-28} {2,-28} {3}" -f 'Record UID', 'Record Title', 'Problem', 'Description')
        foreach ($row in $skipped) {
            Write-Output ("{0,-22} {1,-28} {2,-28} {3}" -f $row[0], $row[1], $row[2], $row[3])
        }
    }

    foreach ($summary in $configuredResources) {
        Write-Output ''
        Write-Output ("Resource `"{0}`" ({1}) configured for PAM rotation." -f $summary.RecordTitle, $summary.RecordUid)
        Write-Output (" PAM Configuration: {0}" -f $summary.ConfigUid)
        if (-not [string]::IsNullOrEmpty($summary.AdminUserUid)) {
            Write-Output (" Admin user linked: {0}" -f $summary.AdminUserUid)
        }
        if ($summary.RotationEnabled -eq $true) {
            Write-Output ' Rotation: Enabled'
        }
        elseif ($summary.RotationEnabled -eq $false) {
            Write-Output ' Rotation: Disabled'
        }
    }

    if ($requests.Count -eq 0) {
        return
    }

    if ($skipped.Count -gt 0 -and -not [bool]$Options.Force) {
        if (-not (confirmPamYesNo -Prompt 'Do you want to cancel password rotation?' -DefaultYes $true)) {
            return
        }
    }

    if ($valid.Count -gt 0) {
        Write-Output ''
        Write-Output 'The following record(s) will be updated:'
        Write-Output ("{0,-22} {1,-24} {2,-8} {3,-22} {4,-22} {5,-12} {6}" -f `
            'Record UID', 'Record Title', 'Enabled', 'Configuration UID', 'Resource UID', 'Schedule', 'Complexity')
        foreach ($row in $valid) {
            $enabled = if ($row[2]) { 'X' } else { '-' }
            Write-Output ("{0,-22} {1,-24} {2,-8} {3,-22} {4,-22} {5,-12} {6}" -f `
                $row[0], $row[1], $enabled, $row[3], $row[4], $row[5], $row[6])
        }
    }

    if (-not [bool]$Options.Force) {
        if (-not (confirmPamYesNo -Prompt 'Do you want to update password rotation?' -DefaultYes $true)) {
            return
        }
    }

    $unsupportedRevision = $false
    $failures = New-Object System.Collections.Generic.List[string]
    foreach ($request in $requests) {
        $recordUid = [KeeperSecurity.Utils.CryptoUtils]::Base64UrlEncode($request.RecordUid.ToByteArray())
        try {
            [KeeperSecurity.Plugins.PAM.RouterUtils]::SetRecordRotationAsync($Auth, $request).GetAwaiter().GetResult() | Out-Null
        }
        catch {
            $raw = [string]$_.Exception.Message
            if ([KeeperSecurity.Plugins.PAM.PamVaultHelpers]::IsUnsupportedRotationRevisionError($raw) `
                -and [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::IsKeeperNSFRecord($Vault, $recordUid)) {
                $unsupportedRevision = $true
            }
            else {
                [void]$failures.Add(("Record `"{0}`": Set rotation error: {1}" -f $recordUid, $raw))
            }
        }
    }

    $Vault.SyncDown().GetAwaiter().GetResult() | Out-Null

    if ($unsupportedRevision) {
        Write-Warning 'Rotation was not updated because this feature is not supported in production yet. Coming soon.'
    }

    if ($failures.Count -gt 0) {
        Write-Error -Message ([string]::Join([Environment]::NewLine, $failures)) -ErrorAction Stop
    }
}

function script:resolvePamRecordEditContext {
    Param (
        [hashtable] $Options,
        [KeeperSecurity.Vault.TypedRecord] $Record
    )

    $profile = $null
    if (-not [string]::IsNullOrWhiteSpace([string]$Options.RotationProfile)) {
        $profile = ([string]$Options.RotationProfile).Trim().ToLowerInvariant()
    }
    elseif (-not [string]::IsNullOrWhiteSpace([string]$Options.IamAadConfig)) {
        $profile = 'iam_user'
    }

    $noop = ($profile -eq 'scripts_only') -or (testPamNoopRecord -Record $Record)
    if ($profile -eq 'saas') {
        if ([string]::IsNullOrWhiteSpace([string]$Options.SaasConfigUid)) {
            Write-Error -Message 'SaaS rotation profile requires --saas-config-uid to be specified.' -ErrorAction Stop
        }
        $noop = $true
    }

    return @{ Profile = $profile; Noop = $noop }
}

function script:resolvePamResourceUidForDag {
    Param (
        [KeeperSecurity.Vault.VaultOnline] $Vault,
        [KeeperSecurity.Vault.TypedRecord] $Record,
        [hashtable] $Options,
        [KeeperSecurity.Vault.TypedRecord] $ConfigRecord,
        [System.Collections.Generic.Dictionary[string, KeeperSecurity.Vault.TypedRecord]] $PamConfigs
    )

    if (-not [string]::IsNullOrWhiteSpace([string]$Options.Resource)) {
        $resource = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord($Vault, [string]$Options.Resource, $null)
        if ($null -eq $resource) {
            Write-Error -Message ("Resource '{0}' not found" -f $Options.Resource) -ErrorAction Stop
        }
        return $resource.Uid
    }

    $cached = $Vault.GetRecordRotation($Record.Uid)
    if ($null -ne $cached -and -not [string]::IsNullOrEmpty($cached.ResourceUid)) {
        $configUid = if ($null -ne $ConfigRecord) { $ConfigRecord.Uid } else { $cached.ConfigurationUid }
        if (-not [string]::Equals($cached.ResourceUid, $configUid, [System.StringComparison]::Ordinal)) {
            return $cached.ResourceUid
        }
    }

    $configUid = if ($null -ne $ConfigRecord) { $ConfigRecord.Uid } else { $cached.ConfigurationUid }
    if (-not [string]::IsNullOrEmpty($configUid) -and $PamConfigs.ContainsKey($configUid)) {
        return (getPamDefaultResourceUidFromConfig -Config $PamConfigs[$configUid])
    }
    return $null
}

function script:invokePamConfigureResourceRecord {
    Param (
        [KeeperSecurity.Authentication.IAuthentication] $Auth,
        [KeeperSecurity.Vault.VaultOnline] $Vault,
        [KeeperSecurity.Vault.TypedRecord] $Record,
        [hashtable] $Options,
        [System.Collections.Generic.Dictionary[string, KeeperSecurity.Vault.TypedRecord]] $PamConfigs,
        [KeeperSecurity.Vault.TypedRecord] $ConfigRecord,
        [System.Collections.Generic.List[object]] $Skipped,
        [System.Collections.Generic.List[hashtable]] $ConfiguredResources
    )

    $configUid = if ($null -ne $ConfigRecord) { $ConfigRecord.Uid } else { $null }
    if ([string]::IsNullOrEmpty($configUid) -and -not [string]::IsNullOrWhiteSpace([string]$Options.Config)) {
        $resolved = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord(
            $Vault, [string]$Options.Config, [KeeperSecurity.Plugins.PAM.PamRecordTypes]::Configuration)
        $configUid = $resolved.Uid
    }

    if ([string]::IsNullOrEmpty($configUid)) {
        [void]$Skipped.Add(@($Record.Uid, $Record.Title, 'No PAM Configuration', 'Specify a configuration UID parameter [--config]'))
        return
    }

    if (-not $PamConfigs.ContainsKey($configUid)) {
        [void]$Skipped.Add(@($Record.Uid, $Record.Title, 'PAM Configuration is invalid', 'Specify a configuration UID parameter [--config]'))
        return
    }

    [KeeperSecurity.Plugins.PAM.PamRotationGraphEdit]::ConfigureResourceAsync(
        $Auth, $Vault, $Record, $configUid, [string]$Options.AdminUser,
        [bool]$Options.Enable, [bool]$Options.Disable
    ).GetAwaiter().GetResult() | Out-Null

    $adminUserUid = $null
    if (-not [string]::IsNullOrWhiteSpace([string]$Options.AdminUser)) {
        $adminUser = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord(
            $Vault, ([string]$Options.AdminUser).Trim(), @('pamUser'))
        $adminUserUid = $adminUser.Uid
    }

    $rotationEnabled = [KeeperSecurity.Plugins.PAM.RotationUtils]::ResolveRotationEnabled(
        [bool]$Options.Enable, [bool]$Options.Disable)

    [void]$ConfiguredResources.Add(@{
        RecordUid       = $Record.Uid
        RecordTitle     = $Record.Title
        ConfigUid       = $configUid
        AdminUserUid    = $adminUserUid
        RotationEnabled = $rotationEnabled
    })
}

function script:tryBuildPamUserRotationRequest {
    Param (
        [KeeperSecurity.Vault.VaultOnline] $Vault,
        [KeeperSecurity.Vault.TypedRecord] $Record,
        [hashtable] $Options,
        [KeeperSecurity.Vault.TypedRecord] $ConfigRecord,
        [System.Collections.Generic.Dictionary[string, KeeperSecurity.Vault.TypedRecord]] $PamConfigs,
        $ScheduleData,
        $ComplexityRules,
        [hashtable] $EditContext,
        [System.Collections.Generic.List[object]] $Skipped,
        [System.Collections.Generic.List[object]] $Valid,
        [ref] $Request
    )

    $Request.Value = $null
    $cached = $Vault.GetRecordRotation($Record.Uid)
    $profile = $EditContext.Profile
    $noop = [bool]$EditContext.Noop

    if ([bool]$Options.ScheduleOnly) {
        if (-not [string]::IsNullOrWhiteSpace([string]$Options.Folder) -and ($null -eq $cached -or $cached.Disabled)) {
            [void]$Skipped.Add(@($Record.Uid, $Record.Title, 'Rotation not enabled', 'Skipped'))
            return $false
        }
        if ($null -eq $cached) {
            [void]$Skipped.Add(@($Record.Uid, $Record.Title, 'No rotation info', 'Skipped'))
            return $false
        }
    }

    $configUid = if ($null -ne $ConfigRecord) { $ConfigRecord.Uid } else { $null }
    if ([string]::IsNullOrEmpty($configUid) -and -not [string]::IsNullOrWhiteSpace([string]$Options.IamAadConfig)) {
        $configUid = ([string]$Options.IamAadConfig).Trim()
    }
    if ([string]::IsNullOrEmpty($configUid) -and $profile -eq 'iam_user') {
        if (-not [string]::IsNullOrWhiteSpace([string]$Options.Config)) {
            $configUid = ([string]$Options.Config).Trim()
        }
        else {
            $configUid = $cached.ConfigurationUid
        }
    }
    if ([string]::IsNullOrEmpty($configUid) -and $null -ne $cached -and -not [string]::IsNullOrEmpty($cached.ConfigurationUid)) {
        $configUid = $cached.ConfigurationUid
    }

    if ([string]::IsNullOrEmpty($configUid)) {
        [void]$Skipped.Add(@($Record.Uid, $Record.Title, 'No current PAM Configuration', 'Specify a configuration UID parameter [--config]'))
        return $false
    }

    if (-not $PamConfigs.ContainsKey($configUid)) {
        [void]$Skipped.Add(@($Record.Uid, $Record.Title, 'PAM Configuration is invalid', 'Specify a configuration UID parameter [--config]'))
        return $false
    }

    $pamConfig = $PamConfigs[$configUid]
    $recordSchedule = $null
    if ($null -ne $ScheduleData) {
        $recordSchedule = toPamScheduleObjectList $ScheduleData
    }
    elseif ($null -ne $cached -and -not [bool]$Options.ScheduleConfig -and -not [string]::IsNullOrEmpty($cached.Schedule)) {
        try {
            $recordSchedule = toPamScheduleObjectList (
                [KeeperSecurity.Plugins.PAM.RotationUtils]::ParseScheduleJsonString($cached.Schedule))
        }
        catch {
            $recordSchedule = New-Object 'System.Collections.Generic.List[object]'
        }
    }
    elseif ([bool]$Options.ScheduleConfig) {
        # Missing/empty defaultRotationSchedule => On-Demand (Commander/Python parity).
        $fromConfig = getPamDefaultScheduleFromConfig -Config $pamConfig
        $recordSchedule = if ($null -eq $fromConfig) {
            New-Object 'System.Collections.Generic.List[object]'
        }
        else {
            toPamScheduleObjectList $fromConfig
        }
    }

    $pwdComplexity = [byte[]]@()
    if ($null -ne $ComplexityRules) {
        if ([KeeperSecurity.Plugins.PAM.RotationUtils]::IsClearedPasswordComplexity($ComplexityRules)) {
            $pwdComplexity = [byte[]]@()
        }
        else {
            $pwdComplexity = [KeeperSecurity.Plugins.PAM.RotationUtils]::EncryptPasswordComplexity($ComplexityRules, $Record.RecordKey)
        }
    }
    elseif ($null -ne $cached -and $null -ne $cached.PasswordComplexity) {
        $pwdComplexity = $cached.PasswordComplexity
    }

    $disabled = if ($null -ne $cached) { [bool]$cached.Disabled } else { $false }
    if ([bool]$Options.Enable) { $disabled = $false }
    elseif ([bool]$Options.Disable) { $disabled = $true }

    $resourceUid = $null
    if ($profile -eq 'iam_user' -or -not [string]::IsNullOrWhiteSpace([string]$Options.IamAadConfig)) {
        $resourceUid = $null
        $noop = $false
    }
    elseif ($profile -eq 'saas') {
        $resourceUid = $null
        $noop = $true
    }
    elseif ($noop) {
        $resourceUid = $null
    }
    elseif (-not [string]::IsNullOrWhiteSpace([string]$Options.Resource)) {
        $resource = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord($Vault, [string]$Options.Resource, $null)
        if ($null -eq $resource) {
            Write-Error -Message ("Resource '{0}' not found" -f $Options.Resource) -ErrorAction Stop
        }
        $resourceUid = $resource.Uid
    }
    elseif ($profile -eq 'general' -and [string]::IsNullOrWhiteSpace([string]$Options.Resource)) {
        Write-Error -Message 'General rotation profile requires --resource to be specified.' -ErrorAction Stop
    }
    elseif ($null -ne $cached -and -not [string]::IsNullOrEmpty($cached.ResourceUid) -and
            -not [string]::Equals($cached.ResourceUid, $configUid, [System.StringComparison]::Ordinal)) {
        $resourceUid = $cached.ResourceUid
    }
    elseif (-not [bool]$Options.ScheduleOnly) {
        $resourceUid = getPamDefaultResourceUidFromConfig -Config $pamConfig
        if ([string]::IsNullOrEmpty($resourceUid) -and -not $noop) {
            Write-Error -Message (
                ("Record `"{0}`" is not associated with any resource. Please use Set-KeeperPamRotation -Record RECORD -Resource RESOURCE to associate it." -f $Record.Uid)
            ) -ErrorAction Stop
        }
    }

    if (-not [string]::IsNullOrEmpty($resourceUid) -and
        [string]::Equals($resourceUid, $configUid, [System.StringComparison]::Ordinal)) {
        $resourceUid = $null
    }

    $recordScheduleList = toPamScheduleObjectList $recordSchedule
    $scheduleType = [KeeperSecurity.Plugins.PAM.RotationUtils]::GetScheduleType($recordScheduleList)
    $complexityDisplay = ''
    $decoded = $null
    if (-not [KeeperSecurity.Plugins.PAM.RotationUtils]::TryDecryptPasswordComplexity($pwdComplexity, $Record.RecordKey, [ref]$decoded)) {
        $complexityDisplay = '[decrypt failed]'
    }
    else {
        $complexityDisplay = [KeeperSecurity.Plugins.PAM.RotationUtils]::FormatPasswordComplexityDisplay($decoded)
    }

    [void]$Valid.Add(@(
        $Record.Uid,
        $Record.Title,
        (-not $disabled),
        $configUid,
        $(if ($null -eq $resourceUid) { '' } else { $resourceUid }),
        $scheduleType,
        $complexityDisplay
    ))

    $rq = New-Object Router.RouterRecordRotationRequest
    $rq.RecordUid = [Google.Protobuf.ByteString]::CopyFrom((toPamUidBytes -Uid $Record.Uid))
    $rq.ConfigurationUid = [Google.Protobuf.ByteString]::CopyFrom((toPamUidBytes -Uid $configUid))
    $rq.Schedule = [KeeperSecurity.Plugins.PAM.RotationUtils]::BuildSchedulePayload(
        [string]$Options.ScheduleJson, [bool]$Options.OnDemand, $recordScheduleList)
    $rq.PwdComplexity = [Google.Protobuf.ByteString]::CopyFrom($pwdComplexity)
    $rq.Disabled = $disabled
    $rq.Noop = $noop
    $rq.Revision = [long]$Vault.ResolveRecordRotationRevisionAsync($Record.Uid).GetAwaiter().GetResult()

    if (-not $noop -and -not [string]::IsNullOrEmpty($resourceUid)) {
        $rq.ResourceUid = [Google.Protobuf.ByteString]::CopyFrom((toPamUidBytes -Uid $resourceUid))
    }

    if ($profile -eq 'saas' -and -not [string]::IsNullOrWhiteSpace([string]$Options.SaasConfigUid)) {
        $rq.SaasConfiguration = [Google.Protobuf.ByteString]::CopyFrom(
            (toPamUidBytes -Uid (([string]$Options.SaasConfigUid).Trim())))
    }

    $Request.Value = $rq
    return $true
}

# --- PAM config helpers ---

function script:getPamEnterpriseAuth {
    $enterprise = getEnterprise
    if (-not $enterprise -or -not $enterprise.loader -or -not $enterprise.loader.Auth) {
        Write-Error -Message 'Enterprise authentication is not available.' -ErrorAction Stop
    }

    return $enterprise.loader.Auth
}

function script:getPamVault {
    $vault = getVault
    if (-not $vault) {
        Write-Error -Message 'Vault is not available.' -ErrorAction Stop
    }

    return $vault
}

function script:resolvePamGatewayController {
    Param (
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Plugins.PAM.IPamPlugin] $Plugin,
        [Parameter(Mandatory = $true)]
        [string] $Identifier,
        [KeeperSecurity.Vault.VaultOnline] $Vault = $null
    )

    $trimmed = $Identifier.Trim()
    if ([string]::IsNullOrWhiteSpace($trimmed)) {
        return $null
    }

    # Help catch sheet mistakes where SharedFolder UID is passed as -Gateway.
    if ($null -ne $Vault) {
        [KeeperSecurity.Vault.SharedFolder]$sf = $null
        if ($Vault.TryGetSharedFolder($trimmed, [ref]$sf) -and $null -ne $sf) {
            Write-Host ("Gateway `"$trimmed`" matches shared folder `"$($sf.Name)`". " +
                'Use a gateway/controller UID or name, not the shared-folder UID.')
            return $null
        }
    }

    $controllers = getPamControllerList -Plugin $Plugin
    if ($controllers -is [object[]] -and $controllers.Length -eq 1 -and
        $controllers[0] -is [System.Collections.Generic.List[KeeperSecurity.Plugins.PAM.PamController]]) {
        $controllers = $controllers[0]
    }
    if ($null -eq $controllers) {
        $controllers = New-Object 'System.Collections.Generic.List[KeeperSecurity.Plugins.PAM.PamController]'
    }

    $controllerCount = 0
    try { $controllerCount = [int]$controllers.Count } catch { $controllerCount = 0 }
    if ($controllerCount -eq 0) {
        # Controllers empty after failed sync — retry once before giving up.
        [void](syncPamPlugin -Plugin $Plugin -Reload $true -ThrowOnError $false)
        $controllers = getPamControllerList -Plugin $Plugin
        if ($controllers -is [object[]] -and $controllers.Length -eq 1 -and
            $controllers[0] -is [System.Collections.Generic.List[KeeperSecurity.Plugins.PAM.PamController]]) {
            $controllers = $controllers[0]
        }
    }

    $controller = [KeeperSecurity.Plugins.PAM.GatewayUtils]::FindGateway($controllers, $trimmed)
    if ($controller -is [KeeperSecurity.Plugins.PAM.PamController]) {
        return $controller
    }

    $nameMatches = 0
    $nameMatch = $null
    foreach ($item in $controllers) {
        if ($item -is [KeeperSecurity.Plugins.PAM.PamController] -and
            [string]::Equals($item.ControllerName, $trimmed, [System.StringComparison]::OrdinalIgnoreCase)) {
            $nameMatches++
            $nameMatch = $item
        }
    }
    if ($nameMatches -gt 1) {
        throw (New-Object KeeperSecurity.Plugins.PAM.PamGatewayAmbiguousException($trimmed))
    }
    if ($nameMatches -eq 1) {
        return $nameMatch
    }

    return $null
}

function script:resolvePamConfigurationFolderUid {
    Param (
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.VaultOnline] $Vault,
        [Parameter(Mandatory = $true)]
        [string] $Identifier
    )

    if ([string]::IsNullOrWhiteSpace($Identifier)) {
        return $null
    }

    $trimmed = $Identifier.Trim()
    return [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolvePamConfigurationFolderUid($Vault, $trimmed)
}

function script:writePamComingSoonMessage {
    Param (
        [string] $Environment
    )

    if ([string]::IsNullOrWhiteSpace($Environment)) {
        return $false
    }

    $displayName = $null
    try {
        if ([KeeperSecurity.Plugins.PAM.PamConfigTypes]::IsComingSoonEnvironment($Environment.Trim(), [ref]$displayName)) {
            if ([string]::IsNullOrWhiteSpace($displayName)) {
                $displayName = $Environment.Trim()
            }
            Write-Host "Environment $displayName is not supported yet. It will be supported in a future release."
            return $true
        }
    }
    catch {
        Write-Debug "IsComingSoonEnvironment check failed: $($_.Exception.Message)"
    }

    return $false
}

# SIG # Begin signature block
# MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB7HluF4M2HdC3U
# OEL9n/chfFelEizTPgqki0o55/3+hqCCITswggWNMIIEdaADAgECAhAOmxiO+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
# BDEiBCCr3uxjZBPWG9whRrDlFb7ZRVD/H67Jh3AVLVkYN86MlTANBgkqhkiG9w0B
# AQEFAASCAYA/YVon+iWNfGKDqDKKWLA71jqotmbbtrmWTjvBw24NEiUHEvMrT685
# CmPhsBjQaVhS7YOxIyUB4zDYxg1jZJYk5UKB2WH2U+LtLobNTk79ja6RqKAEeYcY
# BNZJjp9JtOp45PUJQirbAH1ep/h/sx0KZOUq0tA7jftydLr+c/8vNG5YlKDF+nuT
# JWvwaB4PyRbbI5K+AWeA73eqOxICG1WG5n66clYAM0bPaH/Heq5SH+vsX4b112sQ
# 7DY9+OBCtfMg3fnYDZs+pW9rwFR2kRuXuepkUhyit3QEEX2qI6jby7+1sq6lKJUi
# Up3M6oP+tVuzY6bnwcRK4Dmi8C/lpZV8QfcuGZRJHCs2ON6s0bNE7Thb0pGEHT+i
# HGNdwmBeE/EhWda+T0CfnB/sMGhqVuKpQ++4NVAIjsf3nbiEeug7dQE1BXxi4V8s
# De+9nG5hTHUwFnmcCkZtlrMJGF+YErwGceJoFS2ySrcdKqq9yK9v3FkGHerp0wN+
# aJQBq/xf6WmhggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD
# VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD
# ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg
# Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN
# AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwODE0MjExOTIyWjAv
# BgkqhkiG9w0BCQQxIgQgWr9Qb/ZeEpm2N8QzjqnW/pRJZ1xiRFWH+KLYDjnjyQQw
# DQYJKoZIhvcNAQEBBQAEggIAwbDvsOG4OhVnR3haVUy0GtchfwGUjSbmlpGqiZE+
# YnkHlyCJ3jqwt/3rpYxoiCG/Cn6+P97oQ01zgMLl6xQfsTIw8fE77Qc/dZOTC90X
# 6wtiEAan6/4hehf/ZBluHpF8UgcAtdVyTJYJyjVXqbUeRwEf4dtRzkbkmXHybAnR
# pe2gAtt8zP7k+Oud+X/5gPwqZt3HXsoTn91sKozmwWBRRV6RRbOQUVGdcmk3xqFq
# jfaMLsNPd99fr7Ku1yKSc78+nKHYLaDD9m6uCi9SRsk00coWETqstTjFlbkTYuT8
# YhtjiF0ine2Bm4CGvybTZ/nKW9Ce+ePcUoGF/d1rWSsGD+Vjs7/elkEoO3DrmZzt
# tWY+OHBvLMcvU5XVKBrhI2hago9eMsOaGj+zHvSO1PeZKuwR77tfmL4xlvYO7T46
# fnkK7+2lXaCSQXyUHDbYQvE1WKu1mnqkj58nYxmsJHFU0x+WR7GFNp1Rl8VpXomE
# zkDvOMmjgqZim3JlwWCUgiy5gXxwZ/IEVR6qNGKUv4KCOBiepDL7tjyIfnBAO+C2
# t2hBphIysaFANCqahifw7Fr4Q6JHk9pN4oBkfRS2UAWnCcnA1gZ7YDcUdRqbUaud
# UJyM/mekkNXGu4MoHzYMD/UMdwvubHJpx0EtqqTQOIgfflXR/AoXPWqRqSpBlOp6
# HDg=
# SIG # End signature block