PAM/Rotation.ps1

#requires -Version 5.1

function script:getPamRotationScheduleRows {
    Param (
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Plugins.PAM.IPamPlugin] $Plugin,
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.VaultOnline] $Vault,
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Authentication.IAuthentication] $Auth,
        [bool] $VerboseOutput
    )

    $schedulesResponse = [KeeperSecurity.Plugins.PAM.RouterUtils]::GetRotationSchedulesAsync($Auth).GetAwaiter().GetResult()
    $schedules = @()
    if ($null -ne $schedulesResponse -and $null -ne $schedulesResponse.Schedules) {
        $schedules = @($schedulesResponse.Schedules)
    }

    $configs = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::GetConfigurationRecords($Vault)
    $rows = New-Object 'System.Collections.Generic.List[object]'

    $prepared = New-Object 'System.Collections.Generic.List[object]'
    foreach ($schedule in $schedules) {
        $recordUid = encodePamByteString $schedule.RecordUid
        if ([string]::IsNullOrEmpty($recordUid)) {
            continue
        }

        $record = $null
        if (-not [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::TryGetUserRecord($Vault, $recordUid, [ref]$record)) {
            continue
        }

        $sortTitle = if ($record.Title) { $record.Title } else { $recordUid }
        $prepared.Add(@{
                Schedule  = $schedule
                Record    = $record
                RecordUid = $recordUid
                SortTitle = $sortTitle
            })
    }

    foreach ($item in ($prepared | Sort-Object { $_.SortTitle })) {
        $schedule = $item.Schedule
        $record = $item.Record
        $recordUid = $item.RecordUid

        $controllerUid = encodePamByteString $schedule.ControllerUid
        $configUid = encodePamByteString $schedule.ConfigurationUid
        $configRecord = $null
        if (-not [string]::IsNullOrEmpty($configUid) -and $configs.ContainsKey($configUid)) {
            $configRecord = $configs[$configUid]
        }

        $scheduleText = [KeeperSecurity.Plugins.PAM.RotationUtils]::FormatScheduleDisplay($schedule.ScheduleData, $schedule.NoSchedule)
        $gatewayName = resolvePamRotationGatewayName -Plugin $Plugin -GatewayUid $controllerUid -FallbackName $null
        $configText = if ($configRecord) {
            "$($configRecord.Title) ($($configRecord.TypeName))"
        }
        else {
            '[No config found]'
        }

        if ($VerboseOutput) {
            $rows.Add(@{
                    RecordUid           = $recordUid
                    RecordTitle         = $record.Title
                    RecordType          = $record.TypeName
                    Schedule            = $scheduleText
                    Gateway             = $gatewayName
                    GatewayUid          = $controllerUid
                    PamConfiguration    = $configText
                    PamConfigurationUid = $configUid
                })
        }
        else {
            $rows.Add(@{
                    RecordUid        = $recordUid
                    RecordTitle      = $record.Title
                    RecordType       = $record.TypeName
                    Schedule         = $scheduleText
                    Gateway          = $gatewayName
                    PamConfiguration = $configText
                })
        }
    }

    return $rows.ToArray()
}

function script:writePamRotationListTable {
    Param (
        $Rows,
        [bool] $VerboseOutput = $false
    )

    $rowArray = @($Rows)
    if ($rowArray.Count -eq 0) {
        return
    }

    if ($VerboseOutput) {
        $table = $rowArray | Select-Object `
            @{ Name = 'Record UID'; Expression = { $_.RecordUid } }, `
            @{ Name = 'Record Title'; Expression = { $_.RecordTitle } }, `
            @{ Name = 'Record Type'; Expression = { $_.RecordType } }, `
            @{ Name = 'Schedule'; Expression = { $_.Schedule } }, `
            @{ Name = 'Gateway'; Expression = { $_.Gateway } }, `
            @{ Name = 'Gateway UID'; Expression = { $_.GatewayUid } }, `
            @{ Name = 'PAM Configuration (Type)'; Expression = { $_.PamConfiguration } }, `
            @{ Name = 'PAM Configuration UID'; Expression = { $_.PamConfigurationUid } } |
            Format-Table -AutoSize | Out-String -Width 4096
    }
    else {
        $table = $rowArray | Select-Object `
            @{ Name = 'Record UID'; Expression = { $_.RecordUid } }, `
            @{ Name = 'Record Title'; Expression = { $_.RecordTitle } }, `
            @{ Name = 'Record Type'; Expression = { $_.RecordType } }, `
            @{ Name = 'Schedule'; Expression = { $_.Schedule } }, `
            @{ Name = 'Gateway'; Expression = { $_.Gateway } }, `
            @{ Name = 'PAM Configuration (Type)'; Expression = { $_.PamConfiguration } } |
            Format-Table -AutoSize | Out-String -Width 4096
    }

    if (-not [string]::IsNullOrWhiteSpace($table)) {
        Write-Output $table.TrimEnd()
    }
}

function script:resolvePamRotationGatewayName {
    Param (
        [object] $Plugin,
        [string] $GatewayUid,
        [string] $FallbackName
    )

    if (-not [string]::IsNullOrWhiteSpace($GatewayUid) -and $GatewayUid -ne '-' -and $Plugin) {
        $controller = $null
        try {
            $controller = $Plugin.Controllers.GetEntity($GatewayUid)
        }
        catch {
            $controller = $null
        }

        if (-not $controller) {
            foreach ($item in $Plugin.Controllers.GetAll()) {
                if ($item -and [string]::Equals($item.ControllerUid, $GatewayUid, [StringComparison]::Ordinal)) {
                    $controller = $item
                    break
                }
            }
        }

        if ($controller -and -not [string]::IsNullOrWhiteSpace($controller.ControllerName)) {
            return $controller.ControllerName
        }
    }

    if (-not [string]::IsNullOrWhiteSpace($FallbackName)) {
        return $FallbackName
    }

    if (-not [string]::IsNullOrWhiteSpace($GatewayUid) -and $GatewayUid -ne '-') {
        return $GatewayUid
    }

    return '-'
}

function script:convertPamRotationComplexityDetailToHashtable {
    Param (
        [object] $Detail
    )

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

    if ($Detail -is [string]) {
        return $Detail
    }

    if ($Detail -isnot [System.Collections.IDictionary]) {
        return $Detail
    }

    $ht = [ordered]@{}
    foreach ($key in $Detail.Keys) {
        $ht[[string]$key] = $Detail[$key]
    }
    return $ht
}

function script:getPamRotationInfoModel {
    Param (
        [Parameter(Mandatory = $true)]
        [object] $RotationInfo,
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.TypedRecord] $Record,
        [object] $Schedule,
        [object] $Plugin = $null,
        [KeeperSecurity.Vault.VaultOnline] $Vault = $null
    )

    $statusName = [string]$RotationInfo.Status
    $isReady = ($RotationInfo.Status.ToString() -eq 'RrsOnline')
    $configUid = encodePamByteString $RotationInfo.ConfigurationUid
    $gatewayUid = encodePamByteString $RotationInfo.ControllerUid
    if ([string]::IsNullOrEmpty($gatewayUid)) { $gatewayUid = '-' }
    $gatewayName = resolvePamRotationGatewayName -Plugin $Plugin -GatewayUid $gatewayUid -FallbackName $RotationInfo.ControllerName
    $adminResourceUid = encodePamByteString $RotationInfo.ResourceUid
    if ([string]::IsNullOrEmpty($adminResourceUid)) { $adminResourceUid = $null }

    $pwdComplexityRaw = $null
    if (-not [string]::IsNullOrEmpty($RotationInfo.PwdComplexity)) {
        $pwdComplexityRaw = [KeeperSecurity.Utils.CryptoUtils]::Base64UrlDecode($RotationInfo.PwdComplexity)
    }
    $pwdComplexityDetail = $null
    $pwdComplexityDecryptFailed = $false
    if ($pwdComplexityRaw -and $pwdComplexityRaw.Length -gt 0) {
        $pwdComplexityDecryptFailed = -not [KeeperSecurity.Plugins.PAM.RotationUtils]::TryDecryptPasswordComplexity(
            $pwdComplexityRaw, $Record.RecordKey, [ref]$pwdComplexityDetail)
    }

    $scheduleType = $null
    $scheduleData = $null
    $scheduleText = $null
    if ($Schedule) {
        $scheduleType = if ($Schedule.NoSchedule) { 'manual' } else { 'scheduled' }
        $scheduleData = $Schedule.ScheduleData
        $scheduleText = if ($Schedule.NoSchedule) {
            'Manual Rotation'
        }
        else {
            [KeeperSecurity.Plugins.PAM.RotationUtils]::FormatScheduleDisplay($Schedule.ScheduleData, $false)
        }
    }

    $complexityDetailOut = $null
    $complexityDisplay = $null
    if ($pwdComplexityDecryptFailed) {
        $complexityDetailOut = '[decrypt failed]'
        $complexityDisplay = '[decrypt failed]'
    }
    elseif ($null -ne $pwdComplexityDetail) {
        $complexityDetailOut = convertPamRotationComplexityDetailToHashtable (
            [KeeperSecurity.Plugins.PAM.RotationUtils]::PasswordComplexityToDetail($pwdComplexityDetail))
        $complexityDisplay = formatPamPasswordComplexityInfoDisplay -Rules $pwdComplexityDetail
    }

    $hasComplexity = ($pwdComplexityRaw -and $pwdComplexityRaw.Length -gt 0)

    $scriptName = $null
    if (-not [string]::IsNullOrWhiteSpace($RotationInfo.ScriptName)) {
        $scriptName = [string]$RotationInfo.ScriptName
    }

    $useDefaultSchedule = $false
    if ($null -ne $Vault -and -not [string]::IsNullOrWhiteSpace($configUid)) {
        $useDefaultSchedule = testPamUsesDefaultRotationSchedule -Vault $Vault -RecordUid $Record.Uid -ConfigUid $configUid
    }

    return @{
        StatusName                 = $statusName
        IsReady                    = $isReady
        ConfigUid                  = $configUid
        NodeId                     = $RotationInfo.NodeId
        GatewayName                = $gatewayName
        GatewayUid                 = $gatewayUid
        AdminResourceUid           = $adminResourceUid
        HasComplexity              = $hasComplexity
        PasswordComplexity         = if ($hasComplexity) { $RotationInfo.PwdComplexity } else { $null }
        ComplexityDetail           = $complexityDetailOut
        ComplexityDisplay          = $complexityDisplay
        ScheduleType               = $scheduleType
        ScheduleData               = $scheduleData
        ScheduleText               = $scheduleText
        UseDefaultRotationSchedule = $useDefaultSchedule
        Disabled                   = $RotationInfo.Disabled
        ScriptName                 = $scriptName
    }
}

function script:writePamRotationInfoTable {
    Param (
        [Parameter(Mandatory = $true)]
        [object] $Model
    )

    if ($Model.IsReady) {
        Write-Output "Rotation Status: Ready to rotate ($($Model.StatusName))"
        Write-Output "PAM Config UID: $($Model.ConfigUid)"
        Write-Output "Node ID: $($Model.NodeId)"
        Write-Output "Gateway Name: $($Model.GatewayName)"
        Write-Output "Gateway UID: $($Model.GatewayUid)"

        if ($Model.AdminResourceUid) {
            Write-Output "Admin Resource Uid: $($Model.AdminResourceUid)"
        }

        if ($Model.HasComplexity) {
            Write-Output "Password Complexity: $($Model.PasswordComplexity)"
            if (-not [string]::IsNullOrEmpty($Model.ComplexityDisplay)) {
                Write-Output "Password Complexity Data: $($Model.ComplexityDisplay)"
            }
        }
        else {
            Write-Output 'Password Complexity: [not set]'
        }

        Write-Output "Is Rotation Disabled: $($Model.Disabled)"

        if (-not [string]::IsNullOrEmpty($Model.ScheduleText)) {
            Write-Output "Schedule: $($Model.ScheduleText)"
        }

        Write-Output ''
        Write-Output 'Manual rotation is not supported yet. Coming soon.'
    }
    else {
        Write-Output "Rotation Status: Not ready to rotate ($($Model.StatusName))"
    }
}

function script:writePamRotationInfoJson {
    Param (
        [Parameter(Mandatory = $true)]
        [object] $Model
    )

    $result = [ordered]@{
        status                         = $Model.StatusName
        ready_to_rotate                = $Model.IsReady
        pam_config_uid                 = $Model.ConfigUid
        node_id                        = $Model.NodeId
        gateway_name                   = $Model.GatewayName
        gateway_uid                    = $Model.GatewayUid
        admin_resource_uid             = $Model.AdminResourceUid
        password_complexity            = $Model.PasswordComplexity
        password_complexity_detail     = $Model.ComplexityDetail
        schedule_type                  = $Model.ScheduleType
        schedule_data                  = $Model.ScheduleData
        use_default_rotation_schedule  = [bool]$Model.UseDefaultRotationSchedule
        disabled                       = $Model.Disabled
        script_name                    = $Model.ScriptName
    }

    Write-Output ($result | ConvertTo-Json -Depth 8)
}

function script:escapePamCsvField {
    Param ([object] $Value)

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

    $text = $null
    if (($Value -is [System.Collections.IDictionary]) -or
        (($Value -is [System.Collections.IEnumerable]) -and -not ($Value -is [string]))) {
        $text = ($Value | ConvertTo-Json -Compress -Depth 8)
    }
    else {
        $text = [string]$Value
    }

    if ($text -match '[,"\r\n]') {
        return '"' + ($text.Replace('"', '""')) + '"'
    }
    return $text
}

function script:writePamRotationInfoCsv {
    Param (
        [Parameter(Mandatory = $true)]
        [object] $Model
    )

    $headers = @(
        'status', 'ready_to_rotate', 'pam_config_uid', 'node_id', 'gateway_name', 'gateway_uid',
        'admin_resource_uid', 'password_complexity', 'password_complexity_detail', 'schedule_type',
        'schedule_data', 'use_default_rotation_schedule', 'disabled', 'script_name'
    )
    $values = @(
        $Model.StatusName
        $Model.IsReady
        $Model.ConfigUid
        $Model.NodeId
        $Model.GatewayName
        $Model.GatewayUid
        $Model.AdminResourceUid
        $Model.PasswordComplexity
        $Model.ComplexityDetail
        $Model.ScheduleType
        $Model.ScheduleData
        $Model.UseDefaultRotationSchedule
        $Model.Disabled
        $Model.ScriptName
    )

    Write-Output ($headers -join ',')
    Write-Output (($values | ForEach-Object { escapePamCsvField $_ }) -join ',')
}

function script:testPamRotationScriptField {
    Param (
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.TypedField[KeeperSecurity.Vault.FieldScript]] $Field
    )

    return ($Field.FieldName -eq 'script' -or $Field.FieldLabel -eq 'rotationScripts')
}

function script:getPamRotationScriptFields {
    Param (
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.TypedRecord] $Record
    )

    $fields = New-Object 'System.Collections.Generic.List[KeeperSecurity.Vault.TypedField[KeeperSecurity.Vault.FieldScript]]'
    foreach ($field in $Record.Fields) {
        if ($field -is [KeeperSecurity.Vault.TypedField[KeeperSecurity.Vault.FieldScript]]) {
            $scriptField = [KeeperSecurity.Vault.TypedField[KeeperSecurity.Vault.FieldScript]]$field
            if (testPamRotationScriptField -Field $scriptField) {
                [void]$fields.Add($scriptField)
            }
        }
    }

    return , $fields
}

function script:getPamRotationScriptField {
    Param (
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.TypedRecord] $Record
    )

    foreach ($field in $Record.Fields) {
        if ($field -is [KeeperSecurity.Vault.TypedField[KeeperSecurity.Vault.FieldScript]]) {
            $scriptField = [KeeperSecurity.Vault.TypedField[KeeperSecurity.Vault.FieldScript]]$field
            if (testPamRotationScriptField -Field $scriptField) {
                return $scriptField
            }
        }
    }

    return $null
}

function script:getOrCreatePamRotationScriptField {
    Param (
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.TypedRecord] $Record
    )

    $scriptField = getPamRotationScriptField -Record $Record
    if ($scriptField) {
        return $scriptField
    }

    $scriptField = New-Object KeeperSecurity.Vault.TypedField[KeeperSecurity.Vault.FieldScript]('script', 'rotationScripts')
    $Record.Fields.Add($scriptField)
    return $scriptField
}

function script:getPamRotationFileRefUids {
    Param (
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.TypedRecord] $Record
    )

    $facade = New-Object KeeperSecurity.Vault.TypedRecordFacade[KeeperSecurity.Vault.TypedRecordFileRef]($Record)
    $uids = New-Object 'System.Collections.Generic.HashSet[string]'
    if ($null -ne $facade.Fields.FileRef) {
        foreach ($uid in $facade.Fields.FileRef.Values) {
            if (-not [string]::IsNullOrEmpty($uid)) {
                [void]$uids.Add($uid)
            }
        }
    }

    return , $uids
}

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

    $refs = New-Object 'System.Collections.Generic.List[string]'
    if (-not $Credentials) {
        return [string[]]@()
    }

    foreach ($credential in $Credentials) {
        if ([string]::IsNullOrWhiteSpace($credential)) {
            continue
        }

        $keeperRecord = $null
        if ($Vault.TryGetKeeperRecord($credential.Trim(), [ref]$keeperRecord)) {
            [void]$refs.Add($keeperRecord.Uid)
        }
    }

    return (toPamStringArray -Collection $refs)
}

function script:toPamStringArray {
    Param ($Collection)

    if ($null -eq $Collection) {
        return [string[]]@()
    }

    return [string[]]@($Collection)
}

function script:getPamScriptLeafName {
    Param ([string] $Value)

    if ([string]::IsNullOrWhiteSpace($Value)) {
        return ''
    }

    $trimmed = $Value.Trim().Trim('"').Trim("'")
    $normalized = $trimmed.Replace('/', [System.IO.Path]::DirectorySeparatorChar).Replace('\', [System.IO.Path]::DirectorySeparatorChar)
    try {
        $leaf = [System.IO.Path]::GetFileName($normalized)
        if (-not [string]::IsNullOrWhiteSpace($leaf)) {
            return $leaf
        }
    }
    catch {
    }

    return $trimmed
}

function script:findPamRotationScriptValue {
    Param (
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.VaultOnline] $Vault,
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.TypedRecord] $Record,
        [Parameter(Mandatory = $true)]
        [string] $ScriptName
    )

    $scriptField = getPamRotationScriptField -Record $Record
    if (-not $scriptField) {
        return $null, $null
    }

    $input = $ScriptName.Trim().Trim('"').Trim("'")
    $leaf = getPamScriptLeafName -Value $input
    $needles = New-Object 'System.Collections.Generic.List[string]'
    [void]$needles.Add($input)
    if (-not [string]::IsNullOrWhiteSpace($leaf) -and -not [string]::Equals($leaf, $input, [System.StringComparison]::OrdinalIgnoreCase)) {
        [void]$needles.Add($leaf)
    }

    foreach ($scriptValue in $scriptField.Values) {
        if ($null -eq $scriptValue -or [string]::IsNullOrEmpty($scriptValue.FileRef)) {
            continue
        }

        foreach ($needle in $needles) {
            if ([string]::Equals($scriptValue.FileRef, $needle, [System.StringComparison]::OrdinalIgnoreCase)) {
                return $scriptValue, $scriptField
            }
        }

        $keeperRecord = $null
        if (-not $Vault.TryGetKeeperRecord($scriptValue.FileRef, [ref]$keeperRecord) -or $null -eq $keeperRecord) {
            continue
        }

        $names = New-Object 'System.Collections.Generic.List[string]'
        if (-not [string]::IsNullOrEmpty($keeperRecord.Uid)) { [void]$names.Add($keeperRecord.Uid) }
        if (-not [string]::IsNullOrEmpty($keeperRecord.Title)) { [void]$names.Add($keeperRecord.Title) }
        $fileRecord = $keeperRecord -as [KeeperSecurity.Vault.FileRecord]
        if ($fileRecord -and -not [string]::IsNullOrEmpty($fileRecord.Name)) {
            [void]$names.Add($fileRecord.Name)
        }

        foreach ($name in $names) {
            foreach ($needle in $needles) {
                if ([string]::Equals($name, $needle, [System.StringComparison]::OrdinalIgnoreCase)) {
                    return $scriptValue, $scriptField
                }
            }
        }
    }

    return $null, $scriptField
}

function script:testPamRotationScriptRecord {
    Param (
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.TypedRecord] $Record
    )

    $typeName = if ($Record.TypeName) { $Record.TypeName } else { '' }
    return [KeeperSecurity.Plugins.PAM.PamRecordTypes]::Script.Contains($typeName)
}

function script:testPamRotationRecordPattern {
    Param (
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.KeeperRecord] $Record,
        [string] $Pattern
    )

    if ([string]::IsNullOrEmpty($Pattern)) {
        return $true
    }

    if ([string]::Equals($Record.Uid, $Pattern, [System.StringComparison]::OrdinalIgnoreCase)) {
        return $true
    }

    return ($null -ne $Record.Title -and $Record.Title.IndexOf($Pattern, [System.StringComparison]::OrdinalIgnoreCase) -ge 0)
}

function script:getPamRotationRunCommand {
    Param (
        [string] $RunCommand,
        [string] $ScriptCommand
    )

    if (-not [string]::IsNullOrWhiteSpace($RunCommand)) {
        return $RunCommand.Trim()
    }

    if (-not [string]::IsNullOrWhiteSpace($ScriptCommand)) {
        $trimmed = $ScriptCommand.Trim()
        if (-not (testPamRotationScriptVerb -Value $trimmed)) {
            return $trimmed
        }
    }

    return ''
}

function script:resolvePamRotationScriptRecordId {
    Param (
        [string] $Record,
        [string] $RecordUid
    )

    if (-not [string]::IsNullOrWhiteSpace($Record)) {
        return $Record.Trim()
    }

    if (-not [string]::IsNullOrWhiteSpace($RecordUid)) {
        return $RecordUid.Trim()
    }

    return $null
}

function script:writePamRotationScriptTable {
    Param (
        $Rows
    )

    if ($null -eq $Rows) {
        return
    }
    if ($Rows -is [System.Array]) {
        $rowArray = $Rows
    }
    elseif ($Rows.PSObject.Methods['ToArray']) {
        $rowArray = $Rows.ToArray()
    }
    else {
        $rowArray = [object[]]::new(0)
        foreach ($row in $Rows) {
            $rowArray += $row
        }
    }

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

    $table = $rowArray | Select-Object `
        @{ Name = 'Record UID'; Expression = { $_.RecordUid } }, `
        @{ Name = 'Title'; Expression = { $_.Title } }, `
        @{ Name = 'Record Type'; Expression = { $_.RecordType } }, `
        @{ Name = 'Script UID'; Expression = { $_.ScriptUid } }, `
        @{ Name = 'Script Name'; Expression = { $_.ScriptName } }, `
        @{ Name = 'Records'; Expression = { $_.Records } }, `
        @{ Name = 'Command'; Expression = { $_.Command } } |
        Format-Table -AutoSize | Out-String -Width 4096

    if (-not [string]::IsNullOrWhiteSpace($table)) {
        Write-Output $table.TrimEnd()
    }
}

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

    $rows = New-Object 'System.Collections.Generic.List[object]'
    foreach ($record in @($Vault.KeeperRecords)) {
        $typed = $record -as [KeeperSecurity.Vault.TypedRecord]
        if (-not $typed -or -not (testPamRotationScriptRecord -Record $typed)) {
            continue
        }

        if (-not (testPamRotationRecordPattern -Record $typed -Pattern $Pattern)) {
            continue
        }

        foreach ($scriptField in (getPamRotationScriptFields -Record $typed)) {
            foreach ($script in $scriptField.Values) {
                if ([string]::IsNullOrEmpty($script.FileRef)) {
                    continue
                }

                $fileRecord = $null
                $vault.TryGetKeeperRecord($script.FileRef, [ref]$fileRecord) | Out-Null
                $recordRefs = if ($script.RecordRef) { ($script.RecordRef -join ', ') } else { '' }
                $scriptName = if ($fileRecord) { $fileRecord.Title } else { '[inaccessible]' }

                $rows.Add(@{
                        RecordUid  = $typed.Uid
                        Title      = $typed.Title
                        RecordType = $typed.TypeName
                        ScriptUid  = $script.FileRef
                        ScriptName = $scriptName
                        Records    = $recordRefs
                        Command    = if ($script.Command) { $script.Command } else { '' }
                    })
            }
        }
    }

    return $rows.ToArray()
}

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

    return resolvePamRotationRecord -Vault $Vault -Identifier $RecordId -AllowedTypes ([KeeperSecurity.Plugins.PAM.PamRecordTypes]::Script)
}

function Get-KeeperPamRotationList {
    <#
        .Synopsis
        List PAM record rotation schedules.

        .Description
        Shows pamUser rotation schedules with record, schedule, gateway, and PAM configuration details.

        .Parameter VerboseOutput
        Include Gateway UID and PAM Configuration UID columns.

        .Example
        Get-KeeperPamRotationList

        .Example
        Get-KeeperPamRotationList -VerboseOutput
        pam-rot-list -v
    #>

    [CmdletBinding()]
    Param (
        [Alias('v')]
        [switch] $VerboseOutput
    )

    $plugin = ensurePamPlugin
    if (-not $plugin) {
        Write-Error -Message 'PAM plugin is not available. Enterprise admin access is required.' -ErrorAction Stop
    }

    $vault = getPamRotationVault
    $auth = getPamEnterpriseAuth
    $rows = @(getPamRotationScheduleRows -Plugin $plugin -Vault $vault -Auth $auth -VerboseOutput $VerboseOutput.IsPresent)

    if ($rows.Count -eq 0) {
        Write-Output 'No pamUser rotation schedules found.'
        return
    }

    writePamRotationListTable -Rows $rows -VerboseOutput $VerboseOutput.IsPresent

    Write-Output ''
    Write-Output 'Manual rotation is not supported yet. Coming soon.'
}

function Get-KeeperPamRotationInfo {
    <#
        .Synopsis
        Show rotation status for a PAM record.

        .Description
        Displays readiness, gateway, PAM config, schedule, password complexity, and disabled state.

        .Parameter Record
        Record UID, name, or title.

        .Parameter RecordUid
        Record UID alias for -Record.

        .Parameter Format
        Output format: table (default), csv, or json.

        .Example
        Get-KeeperPamRotationInfo -Record "<uid>"

        .Example
        Get-KeeperPamRotationInfo -r "My PAM User" -Format json
        pam-rot-info -r "<uid>"
        pam-rot-info -RecordUid "<uid>" -Format csv
    #>

    [CmdletBinding()]
    Param (
        [Alias('r')]
        [string] $Record,

        [Alias('record-uid')]
        [string] $RecordUid,

        [ValidateSet('table', 'csv', 'json')]
        [string] $Format = 'table'
    )

    $recordId = if (-not [string]::IsNullOrWhiteSpace($Record)) { $Record } else { $RecordUid }
    if ([string]::IsNullOrWhiteSpace($recordId)) {
        Write-Output '--record or --record-uid is required'
        return
    }

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

    $plugin = ensurePamPlugin -SyncIfNeeded $false
    $auth = getPamEnterpriseAuth
    try {
        $resolved = resolvePamRotationRecord -Vault $vault -Identifier $recordId -AllowedTypes ([KeeperSecurity.Plugins.PAM.PamRecordTypes]::Rotation)
    }
    catch [System.InvalidOperationException] {
        return
    }
    if (-not $resolved) {
        Write-Output "Record '$recordId' not found"
        return
    }

    $rotationInfo = [KeeperSecurity.Plugins.PAM.RotationUtils]::GetRotationInfoAsync($auth, $resolved.Uid).GetAwaiter().GetResult()
    $schedulesResponse = [KeeperSecurity.Plugins.PAM.RouterUtils]::GetRotationSchedulesAsync($auth).GetAwaiter().GetResult()
    $schedule = $null
    if ($null -ne $schedulesResponse -and $null -ne $schedulesResponse.Schedules) {
        foreach ($item in $schedulesResponse.Schedules) {
            if ((encodePamByteString $item.RecordUid) -eq $resolved.Uid) {
                $schedule = $item
                break
            }
        }
    }

    $model = getPamRotationInfoModel -RotationInfo $rotationInfo -Record $resolved -Schedule $schedule -Plugin $plugin -Vault $vault
    switch ($Format) {
        'json' { writePamRotationInfoJson -Model $model }
        'csv' { writePamRotationInfoCsv -Model $model }
        default { writePamRotationInfoTable -Model $model }
    }
}

function Set-KeeperPamRotation {
    <#
        .Synopsis
        Create or update PAM record rotation configuration.

        .Description
        Create or update rotation settings for a PAM record.
        Supports single record (-Record) or bulk folder setup (-Folder).

        .Parameter Record
        Record UID, name, or pattern to configure for rotation.

        .Parameter Folder
        Folder UID or name for bulk rotation setup.

        .Parameter Force
        Skip confirmation prompts.

        .Parameter Config
        UID or title of the PAM configuration record.

        .Parameter IamAadConfig
        UID of a PAM configuration for IAM or Azure AD users (instead of -Resource).

        .Parameter Resource
        UID or title of the admin resource record.

        .Parameter ScheduleJson
        Rotation schedule as JSON. Example:
        '{"type": "WEEKLY", "weekday": "SATURDAY", "time": "22:00", "tz": "America/New_York"}'

        .Parameter ScheduleCron
        CRON schedule string (6-field rotation format).

        .Parameter OnDemand
        Configure manual on-demand rotation.

        .Parameter ScheduleConfig
        Inherit schedule from the PAM configuration record.

        .Parameter ScheduleOnly
        Update only the rotation schedule without changing other settings.

        .Parameter Complexity
        Password complexity CSV: length,upper,lower,digits,symbols[,special-chars].
        Example: 20,1,4,2,2,.=+-

        .Parameter ComplexityJson
        Password complexity rules as JSON (alternative to -Complexity).

        .Parameter AdminUser
        PAM user record UID to set as admin credential on the resource.

        .Parameter Enable
        Enable rotation for the record.

        .Parameter Disable
        Disable rotation for the record.

        .Parameter RotationProfile
        Optional rotation profile: general, iam_user, scripts_only, saas.

        .Parameter SaasConfigUid
        SaaS configuration UID when using the saas rotation profile.

        .Example
        Set-KeeperPamRotation -Record "My PAM User" -Config "AWS Config" -Resource "My Server" -OnDemand

        .Example
        Set-KeeperPamRotation -r "<uid>" -sj '{"type": "MONTHLY_BY_DAY", "monthDay": 1, "time": "04:00", "tz": "America/Chicago"}' -c "<config-uid>" -rs "<resource-uid>"

        .Example
        Set-KeeperPamRotation -r "<uid>" -x "20,1,4,2,2,.=+-" -c "<config-uid>" -rs "<resource-uid>" -Force

        .Example
        Set-KeeperPamRotation -Folder "<folder-uid>" -c "<config-uid>" -od -so -Force
    #>

    [CmdletBinding(PositionalBinding = $false)]
    Param (
        [Alias('r')]
        [string] $Record,

        [Alias('fd')]
        [string] $Folder,

        [Alias('c')]
        [string] $Config,

        [Alias('iac')]
        [string] $IamAadConfig,

        [Alias('rp')]
        [string] $RotationProfile,

        [string] $SaasConfigUid,

        [Alias('rs')]
        [string] $Resource,

        [Alias('sj')]
        [string] $ScheduleJson,

        [Alias('sc')]
        [string] $ScheduleCron,

        [Alias('od')]
        [switch] $OnDemand,

        [Alias('sf')]
        [switch] $ScheduleConfig,

        [Alias('so')]
        [switch] $ScheduleOnly,

        [Alias('x')]
        [string] $Complexity,

        [string] $ComplexityJson,

        [Alias('a')]
        [string] $AdminUser,

        [Alias('e')]
        [switch] $Enable,

        [Alias('d')]
        [switch] $Disable,

        [Alias('f')]
        [switch] $Force
    )

    if ([string]::IsNullOrWhiteSpace($Record) -and [string]::IsNullOrWhiteSpace($Folder)) {
        Write-Output '--record or --folder is required'
        return
    }

    if ($Enable.IsPresent -and $Disable.IsPresent) {
        Write-Output 'Cannot use both --enable and --disable at the same time.'
        return
    }

    if (-not [string]::IsNullOrWhiteSpace($Record) -and -not [string]::IsNullOrWhiteSpace($Folder)) {
        Write-Output 'Cannot use both --record and --folder at the same time.'
        return
    }

    if (-not [string]::IsNullOrWhiteSpace($Resource) -and -not [string]::IsNullOrWhiteSpace($IamAadConfig)) {
        Write-Output 'Cannot use both --resource and --iam-aad-config at once. --resource configures users on a resource; --iam-aad-config configures IAM/Azure AD users.'
        return
    }

    $plugin = ensurePamPlugin
    if (-not $plugin) {
        Write-Error -Message 'PAM plugin is not available. Enterprise admin access is required.' -ErrorAction Stop
    }

    $vault = getPamRotationVault
    $auth = getPamEnterpriseAuth

    invokeKeeperPamRotationEdit -Auth $auth -Vault $vault -Options @{
        Record          = $Record
        Folder          = $Folder
        Config          = $Config
        IamAadConfig    = $IamAadConfig
        RotationProfile = $RotationProfile
        SaasConfigUid   = $SaasConfigUid
        Resource        = $Resource
        ScheduleJson    = $ScheduleJson
        ScheduleCron    = $ScheduleCron
        OnDemand        = $OnDemand.IsPresent
        ScheduleConfig  = $ScheduleConfig.IsPresent
        ScheduleOnly    = $ScheduleOnly.IsPresent
        Complexity      = $Complexity
        ComplexityJson  = $ComplexityJson
        AdminUser       = $AdminUser
        Enable          = $Enable.IsPresent
        Disable         = $Disable.IsPresent
        Force           = $Force.IsPresent
    }
}

function Get-KeeperPamRotationScript {
    <#
        .Synopsis
        List post-rotation scripts on PAM records.

        .Description
        Lists post-rotation scripts attached to PAM records.

        .Parameter Pattern
        Record UID or title filter.

        .Parameter Record
        Alias for filtering by record UID or title (same as -Pattern).

        .Example
        Get-KeeperPamRotationScript
        Get-KeeperPamRotationScript -Pattern "My PAM User"
    #>

    [CmdletBinding()]
    Param (
        [string] $Pattern,
        [Alias('r')]
        [string] $Record
    )

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

    $filter = if (-not [string]::IsNullOrWhiteSpace($Pattern)) { $Pattern.Trim() } else { $Record }
    $rows = @(getPamRotationScriptListRows -Vault $vault -Pattern $filter)
    if ($rows.Count -eq 0) {
        Write-Output 'No post-rotation scripts found.'
        return
    }

    writePamRotationScriptTable -Rows $rows
}

function Add-KeeperPamRotationScript {
    <#
        .Synopsis
        Upload and attach a post-rotation script to a PAM record.

        .Description
        Uploads a local script file and attaches it as a post-rotation script.

        .Parameter Record
        Record UID or title (pamUser / pamDirectory).

        .Parameter Script
        Local file path to upload.

        .Parameter RunCommand
        Command line to run the script after rotation.

        .Parameter ScriptCommand
        Alias for -RunCommand (unless value is a script subcommand name).

        .Parameter AddCredential
        Record UID(s) with rotation credentials to link.

        .Example
        Add-KeeperPamRotationScript -Record "<uid>" -Script "C:\scripts\rotate.ps1" -RunCommand "powershell -File rotate.ps1"
        Add-KeeperPamRotationScript -Record "<uid>" -Script "/tmp/rotate.sh" -RunCommand "bash rotate.sh"
        Add-KeeperPamRotationScript fajHNL7_T3qsuzt2MDGEiw -Script "C:\scripts\rotate.ps1"
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0)]
        [Alias('r')]
        [string] $Record,

        [Alias('record-uid')]
        [string] $RecordUid,

        [Parameter(Position = 1)]
        [string] $Script,

        [Alias('script-command')]
        [string] $ScriptCommand,

        [Alias('run-command')]
        [string] $RunCommand,

        [Alias('add-credential', 'ac')]
        [string[]] $AddCredential
    )

    $recordId = resolvePamRotationScriptRecordId -Record $Record -RecordUid $RecordUid
    if ([string]::IsNullOrWhiteSpace($recordId)) {
        Write-Output '--record is required (or provide record UID as a positional argument)'
        return
    }

    if ([string]::IsNullOrWhiteSpace($Script)) {
        Write-Output '--script is required'
        return
    }

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

    try {
        $resolved = resolvePamRotationScriptRecord -Vault $vault -RecordId $recordId
    }
    catch [System.InvalidOperationException] {
        return
    }
    if (-not $resolved) {
        Write-Output "Record '$recordId' not found"
        return
    }

    $filePath = [Environment]::ExpandEnvironmentVariables($Script.Trim())
    if (-not (Test-Path -LiteralPath $filePath)) {
        Write-Output "File `"$Script`" not found."
        return
    }

    $runCommand = getPamRotationRunCommand -RunCommand $RunCommand -ScriptCommand $ScriptCommand
    $scriptField = getOrCreatePamRotationScriptField -Record $resolved
    $preRefs = getPamRotationFileRefUids -Record $resolved
    if ($null -eq $preRefs) {
        $preRefs = New-Object 'System.Collections.Generic.HashSet[string]'
    }

    $uploadTask = New-Object KeeperSecurity.Vault.FileAttachmentUploadTask($filePath, $null, $true)
    try {
        $vault.UploadAttachment($resolved, $uploadTask).GetAwaiter().GetResult() | Out-Null
    }
    catch {
        Write-Output $_.Exception.Message
        return
    }
    finally {
        if ($uploadTask) {
            $uploadTask.Dispose()
        }
    }

    $postRefs = getPamRotationFileRefUids -Record $resolved
    if ($null -eq $postRefs) {
        $postRefs = New-Object 'System.Collections.Generic.HashSet[string]'
    }
    $newUids = @(
        $postRefs | Where-Object {
            -not [string]::IsNullOrEmpty($_) -and -not $preRefs.Contains($_)
        }
    )
    if ($newUids.Count -ne 1) {
        Write-Output 'Failed to determine uploaded script file UID. Only the record owner can attach post-rotation scripts.'
        return
    }

    $facade = New-Object KeeperSecurity.Vault.TypedRecordFacade[KeeperSecurity.Vault.TypedRecordFileRef]($resolved)
    if ($null -ne $facade.Fields.FileRef) {
        [void]$facade.Fields.FileRef.Values.Remove($newUids[0])
    }

    $scriptValue = New-Object KeeperSecurity.Vault.FieldScript
    $scriptValue.FileRef = $newUids[0]
    $scriptValue.RecordRef = resolvePamRotationCredentialUids -Vault $vault -Credentials $AddCredential
    $scriptValue.Command = $runCommand
    $scriptField.Values.Add($scriptValue)

    if (-not (updatePamRotationScriptRecord -Vault $vault -Record $resolved -Action add)) {
        return
    }
    Write-Output "Script added to record '$($resolved.Title)' ($($resolved.Uid))."
}

function Set-KeeperPamRotationScript {
    <#
        .Synopsis
        Update a post-rotation script on a PAM record.

        .Description
        Updates an existing post-rotation script on a PAM record.

        .Parameter Record
        Record UID or title.

        .Parameter Script
        Script file UID or name to update.

        .Parameter RunCommand
        New command line to run the script.

        .Parameter AddCredential
        Credential record UID(s) to link.

        .Parameter RemoveCredential
        Credential record UID(s) to unlink.

        .Example
        Set-KeeperPamRotationScript -Record "<uid>" -Script "rotate.ps1" -RunCommand "powershell -File rotate.ps1" -ac @("<cred-uid>")
        Set-KeeperPamRotationScript fajHNL7_T3qsuzt2MDGEiw -Script E5HGAlv7lHIkGa5PzPXgbQ -RunCommand "powershell -File rotate_password_v2.ps1"
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0)]
        [Alias('r')]
        [string] $Record,

        [Alias('record-uid')]
        [string] $RecordUid,

        [Parameter(Position = 1)]
        [string] $Script,

        [Alias('script-command')]
        [string] $ScriptCommand,

        [Alias('run-command')]
        [string] $RunCommand,

        [Alias('add-credential', 'ac')]
        [string[]] $AddCredential,

        [Alias('remove-credential', 'rc')]
        [string[]] $RemoveCredential
    )

    $recordId = resolvePamRotationScriptRecordId -Record $Record -RecordUid $RecordUid
    if ([string]::IsNullOrWhiteSpace($recordId)) {
        Write-Output '--record is required (or provide record UID as a positional argument)'
        return
    }

    if ([string]::IsNullOrWhiteSpace($Script)) {
        Write-Output '--script is required'
        return
    }

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

    try {
        $resolved = resolvePamRotationScriptRecord -Vault $vault -RecordId $recordId
    }
    catch [System.InvalidOperationException] {
        return
    }
    if (-not $resolved) {
        Write-Output "Record '$recordId' not found"
        return
    }

    $scriptValue, $scriptField = findPamRotationScriptValue -Vault $vault -Record $resolved -ScriptName $Script.Trim()
    if (-not $scriptField) {
        Write-Output "Record '$($resolved.Title)' has no rotation scripts."
        return
    }
    if (-not $scriptValue) {
        Write-Output "Record '$($resolved.Title)' does not have script '$Script'. Use Script UID or name from Get-KeeperPamRotationScript (file path basename is also accepted)."
        return
    }

    $modified = $false
    $refs = New-Object 'System.Collections.Generic.HashSet[string]'
    if ($scriptValue.RecordRef) {
        foreach ($item in $scriptValue.RecordRef) {
            if (-not [string]::IsNullOrEmpty($item)) {
                [void]$refs.Add($item)
            }
        }
    }

    if ($RemoveCredential) {
        foreach ($cred in (resolvePamRotationCredentialUids -Vault $vault -Credentials $RemoveCredential)) {
            if ($refs.Remove($cred)) {
                $modified = $true
            }
        }
    }

    if ($AddCredential) {
        foreach ($cred in (resolvePamRotationCredentialUids -Vault $vault -Credentials $AddCredential)) {
            if ($refs.Add($cred)) {
                $modified = $true
            }
        }
    }

    if ($modified) {
        $scriptValue.RecordRef = toPamStringArray -Collection $refs
    }

    $runCommand = getPamRotationRunCommand -RunCommand $RunCommand -ScriptCommand $ScriptCommand
    if (-not [string]::IsNullOrWhiteSpace($runCommand)) {
        $scriptValue.Command = $runCommand
        $modified = $true
    }

    if (-not $modified) {
        Write-Output 'Nothing to do'
        return
    }

    if (-not (updatePamRotationScriptRecord -Vault $vault -Record $resolved -Action edit)) {
        return
    }
    Write-Output "Script updated on record '$($resolved.Title)' ($($resolved.Uid))."
}

function Remove-KeeperPamRotationScript {
    <#
        .Synopsis
        Remove a post-rotation script from a PAM record.

        .Description
        Removes a post-rotation script from a PAM record.

        .Parameter Record
        Record UID or title.

        .Parameter Script
        Script file UID or name to remove.

        .Example
        Remove-KeeperPamRotationScript -Record "<uid>" -Script "rotate.ps1"
        Remove-KeeperPamRotationScript fajHNL7_T3qsuzt2MDGEiw -Script CTUH9gBqQK_iJoph_APtNQ
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0)]
        [Alias('r')]
        [string] $Record,

        [Alias('record-uid')]
        [string] $RecordUid,

        [Parameter(Position = 1)]
        [string] $Script
    )

    $recordId = resolvePamRotationScriptRecordId -Record $Record -RecordUid $RecordUid
    if ([string]::IsNullOrWhiteSpace($recordId)) {
        Write-Output '--record is required (or provide record UID as a positional argument)'
        return
    }

    if ([string]::IsNullOrWhiteSpace($Script)) {
        Write-Output '--script is required'
        return
    }

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

    try {
        $resolved = resolvePamRotationScriptRecord -Vault $vault -RecordId $recordId
    }
    catch [System.InvalidOperationException] {
        return
    }
    if (-not $resolved) {
        Write-Output "Record '$recordId' not found"
        return
    }

    $scriptValue, $scriptField = findPamRotationScriptValue -Vault $vault -Record $resolved -ScriptName $Script.Trim()
    if (-not $scriptField) {
        Write-Output "Record '$($resolved.Title)' has no rotation scripts."
        return
    }
    if (-not $scriptValue) {
        Write-Output "Record '$($resolved.Title)' does not have script '$Script'."
        return
    }

    [void]$scriptField.Values.Remove($scriptValue)
    if (-not (updatePamRotationScriptRecord -Vault $vault -Record $resolved -Action remove)) {
        return
    }
    Write-Output "Script removed from record '$($resolved.Title)' ($($resolved.Uid))."
}

New-Alias -Name pam-rotation-list -Value Get-KeeperPamRotationList -ErrorAction SilentlyContinue
New-Alias -Name pam-rot-list -Value Get-KeeperPamRotationList -ErrorAction SilentlyContinue
New-Alias -Name pam-rotation-info -Value Get-KeeperPamRotationInfo -ErrorAction SilentlyContinue
New-Alias -Name pam-rot-info -Value Get-KeeperPamRotationInfo -ErrorAction SilentlyContinue
New-Alias -Name pam-rotation-edit -Value Set-KeeperPamRotation -ErrorAction SilentlyContinue
New-Alias -Name pam-rot-edit -Value Set-KeeperPamRotation -ErrorAction SilentlyContinue
New-Alias -Name pam-rotation-new -Value Set-KeeperPamRotation -ErrorAction SilentlyContinue
New-Alias -Name pam-rot-new -Value Set-KeeperPamRotation -ErrorAction SilentlyContinue
New-Alias -Name pam-rotation-script-list -Value Get-KeeperPamRotationScript -ErrorAction SilentlyContinue
New-Alias -Name pam-rot-script-list -Value Get-KeeperPamRotationScript -ErrorAction SilentlyContinue
New-Alias -Name pam-rotation-script-add -Value Add-KeeperPamRotationScript -ErrorAction SilentlyContinue
New-Alias -Name pam-rot-script-add -Value Add-KeeperPamRotationScript -ErrorAction SilentlyContinue
New-Alias -Name pam-rotation-script-edit -Value Set-KeeperPamRotationScript -ErrorAction SilentlyContinue
New-Alias -Name pam-rot-script-edit -Value Set-KeeperPamRotationScript -ErrorAction SilentlyContinue
New-Alias -Name pam-rotation-script-delete -Value Remove-KeeperPamRotationScript -ErrorAction SilentlyContinue
New-Alias -Name pam-rot-script-delete -Value Remove-KeeperPamRotationScript -ErrorAction SilentlyContinue

# SIG # Begin signature block
# MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBBv/3vAFdnT4ns
# fYY3Ps9WO3UN+dfN+O7HSwy+cscKa6CCITswggWNMIIEdaADAgECAhAOmxiO+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
# BDEiBCCgDoVgYUaWa139rEqCW5PUFppt+6ltzd53UJNOOMQauzANBgkqhkiG9w0B
# AQEFAASCAYBw/MzcIcp9yG4EJXFWtFBJKCyu+/aomFPPsK7Wg9u85u308O5/1ilF
# XjsJC2i3GucDRjExshRxYYIVWcXzS60cDTIsx7uikYMivcxJVZsqh07U9DlcIaL5
# 6kwFjQrcqGfbn9tnd5fwaLRLwL+fYkDtc+eCXXGkhSTM3W0CUNNTPfcxkjCxnQTD
# iHq3s/H831GsgAY0A8brZbIo2LKXT0J/bslV0Xfh9TxMfEIUh7JEXgOlFR5x38TW
# aavJOnEUGB+58Shzdg//A75MnfMGVSUdLsdHXSSRg0dqb1zsmJ+8RaMvj9Zh+bYe
# I4goNqOROicifLdwImjpOVsHcN++5q9w+CXoL8J0anD/W86pR8WAJs9h8E90gvvv
# Rw+u3K1/+fNadyTQCGlNYeso8d/ogwEkX+XkQn0b1LvvkofHfk0mwSTSHXXUsNUR
# aV/BG+p8IxO9nEv06i05oXf17cxdC6OURU3LJddmCW2GaRtMCc5WN5u3/K3oLLLt
# EglHkZOVf2ShggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD
# VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD
# ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg
# Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN
# AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwODE0MjExOTIyWjAv
# BgkqhkiG9w0BCQQxIgQgCD1P3hcZpOrfUeVf2PESoYYl79w8j30JbxY4fF3zc4Uw
# DQYJKoZIhvcNAQEBBQAEggIAFVOzWiu8fj6ay3DpqKMn6+tR0vscIX4ifPlvCHW7
# a9OgW58PJOGmutj/IX3Q/doE/Ry4bO6FpSU3qDDq+5ur61pPgXbe24HnpRWA++YH
# zYKPv7LcBEPZRW5bDVzWt8ZusM1o2pXlKhKYp9Iql10rmvGXMp9HNgDPYhmtmCir
# OiGkeWXQNuyvz1u+feqfWZLz/zDKgwXVTbO6LJB4lUDZowLo8DC7RgwWzCzSHT64
# ij2owULtekUJnY1LW7ABcHHsVY1M9iGeWjwcn5f/snPbMa7dp5tSivn+PCVWrNTA
# BnEZfzptYBQQGgQcfaaeETXpsTDm4Rg+4Tb407GbsMV6f8JBzTS0bIo653PHaMjF
# OKN6LgC6gq3Yl0k2goyNDWAyLBG2n8uzhLCF6RU/Tc6Bi2P4zUsaqy7k0CM0b/tz
# vYlS7ZqjHyCaDTXpk6yZ/L6l8ohCRlteA7VBMvS6O1nE+EHr31Qz1e85sXsU65jj
# rqU4bAoXa1LSdNDfyF+Z2YxXxLWJ/K1DXX8wr0JMbXmkTGw8MX9iuoG8xSu8sian
# v2EHigaWZXU2yPBNpvKbLDyJhSOMFOZOA9XaaJr0eSrWjRkFWKngA1SDD7y2gm7A
# FxEr9B7zXeXsQOTTWvXr+f7dRcw4wkE0FT9vsI5Wv/SWD3p10/epoCIP4Ff1Q6Yy
# yBE=
# SIG # End signature block