From ca35a0a902d2298a0dc27c3911f0708aa04c28f2 Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Thu, 27 Aug 2026 15:13:11 +0530 Subject: [PATCH 1/3] pam rotation command implementation. --- KeeperSdk/src/index.ts | 62 +++ KeeperSdk/src/pam/PamManager.ts | 111 ++++++ KeeperSdk/src/pam/index.ts | 85 ++++ KeeperSdk/src/pam/rotation/RotationManager.ts | 153 +++++++ .../src/pam/rotation/addRotationScript.ts | 91 +++++ .../src/pam/rotation/deleteRotationScript.ts | 84 ++++ KeeperSdk/src/pam/rotation/editRotation.ts | 374 ++++++++++++++++++ .../src/pam/rotation/editRotationScript.ts | 110 ++++++ KeeperSdk/src/pam/rotation/getRotationInfo.ts | 205 ++++++++++ KeeperSdk/src/pam/rotation/index.ts | 98 +++++ .../src/pam/rotation/listRotationScripts.ts | 115 ++++++ KeeperSdk/src/pam/rotation/listRotations.ts | 196 +++++++++ .../src/pam/rotation/rotationConstants.ts | 31 ++ KeeperSdk/src/pam/rotation/rotationHelpers.ts | 188 +++++++++ .../src/pam/rotation/rotationScriptHelpers.ts | 195 +++++++++ .../src/pam/rotation/rotationScriptTypes.ts | 116 ++++++ KeeperSdk/src/pam/rotation/rotationTypes.ts | 174 ++++++++ KeeperSdk/src/utils/constants.ts | 6 + KeeperSdk/src/vault/KeeperVault.ts | 102 +++++ keeperapi/src/restMessages.ts | 20 + 20 files changed, 2516 insertions(+) create mode 100644 KeeperSdk/src/pam/rotation/RotationManager.ts create mode 100644 KeeperSdk/src/pam/rotation/addRotationScript.ts create mode 100644 KeeperSdk/src/pam/rotation/deleteRotationScript.ts create mode 100644 KeeperSdk/src/pam/rotation/editRotation.ts create mode 100644 KeeperSdk/src/pam/rotation/editRotationScript.ts create mode 100644 KeeperSdk/src/pam/rotation/getRotationInfo.ts create mode 100644 KeeperSdk/src/pam/rotation/index.ts create mode 100644 KeeperSdk/src/pam/rotation/listRotationScripts.ts create mode 100644 KeeperSdk/src/pam/rotation/listRotations.ts create mode 100644 KeeperSdk/src/pam/rotation/rotationConstants.ts create mode 100644 KeeperSdk/src/pam/rotation/rotationHelpers.ts create mode 100644 KeeperSdk/src/pam/rotation/rotationScriptHelpers.ts create mode 100644 KeeperSdk/src/pam/rotation/rotationScriptTypes.ts create mode 100644 KeeperSdk/src/pam/rotation/rotationTypes.ts diff --git a/KeeperSdk/src/index.ts b/KeeperSdk/src/index.ts index 0b5416ac..9540c2a2 100644 --- a/KeeperSdk/src/index.ts +++ b/KeeperSdk/src/index.ts @@ -955,6 +955,49 @@ export { resolvePamConfigFolderName, formatPamConfigFolderDisplay, placePamConfigurationInFolder, + RotationManager, + listRotationSchedules, + formatRotationSchedulesTable, + renderRotationSchedulesAsciiTable, + formatRotationSchedulesJson, + formatRotationSchedulesOutput, + getRotationInfo, + formatRotationInfoJson, + formatRotationInfoOutput, + editRotation, + validateRotationInput, + RotationListFormat, + PAM_USER_RECORD_TYPE, + RECORD_INACCESSIBLE_LABEL, + RECORD_UNTITLED_LABEL, + RECORD_UNKNOWN_TYPE_LABEL, + NO_CONFIG_FOUND_LABEL, + GATEWAY_DOES_NOT_EXIST_LABEL, + MANUAL_ROTATION_LABEL, + EMPTY_SCHEDULE_LABEL, + EMPTY_ROTATION_SCHEDULES_MESSAGE, + DEFAULT_ROTATION_SCHEDULE_LABEL, + RECORD_ROTATION_KIND, + ROTATION_LIST_DEFAULT_HEADERS, + ROTATION_LIST_VERBOSE_HEADERS, + ROTATION_STATUS_ONLINE, + MISSING_VALUE_LABEL, + getVaultRecord, + recordExistsInVault, + getVaultRecordTitleType, + formatScheduleDataString, + formatRotationSchedule, + rotationStatusName, + isRotationOnline, + decryptPasswordComplexity, + isAdminResourceValid, + usesDefaultRotationSchedule, + resolveScheduleEnrichment, + buildPamConfigurationUidSet, + resolvePamConfigDisplay, + findGatewayByControllerUid, + buildOnlineGatewayUidSet, + resolveGatewayName, } from './pam' export type { ListGatewaysOptions, @@ -1014,6 +1057,25 @@ export type { RemovePamConfigurationResult, PamConfigFolderKind, PamConfigFolderTarget, + RotationListFormatInput, + ListRotationSchedulesOptions, + RotationListRow, + ListRotationSchedulesResult, + FormattedRotationSchedulesTable, + FormatRotationSchedulesTableOptions, + RenderRotationSchedulesAsciiTableOptions, + RotationScheduleJsonEntry, + RotationSchedulesJsonPayload, + RotationScheduleType, + PasswordComplexityDetail, + GetRotationInfoInput, + RotationInfoResult, + RotationInfoJsonPayload, + EditRotationInput, + EditRotationResult, + RotationProfile, + PasswordComplexityInput, + ScheduleData, } from './pam' export type { diff --git a/KeeperSdk/src/pam/PamManager.ts b/KeeperSdk/src/pam/PamManager.ts index ab4f5746..6f6c34a1 100644 --- a/KeeperSdk/src/pam/PamManager.ts +++ b/KeeperSdk/src/pam/PamManager.ts @@ -2,6 +2,7 @@ import type { Auth } from '@keeper-security/keeperapi' import type { InMemoryStorage } from '../storage/InMemoryStorage' import { ConfigManager } from './config/ConfigManager' import { GatewayManager } from './gateway/GatewayManager' +import { RotationManager } from './rotation/RotationManager' import type { FormatPamConfigurationsTableOptions, FormattedPamConfigurationsTable, @@ -30,16 +31,39 @@ import type { SetGatewayMaxInstancesInput, SetGatewayMaxInstancesResult, } from './gateway/gatewayTypes' +import type { + FormatRotationSchedulesTableOptions, + FormattedRotationSchedulesTable, + GetRotationInfoInput, + ListRotationSchedulesOptions, + ListRotationSchedulesResult, + RenderRotationSchedulesAsciiTableOptions, + RotationInfoResult, + EditRotationInput, + EditRotationResult, +} from './rotation/rotationTypes' +import type { + ListRotationScriptsOptions, + ListRotationScriptsResult, + AddRotationScriptInput, + AddRotationScriptResult, + EditRotationScriptInput, + EditRotationScriptResult, + DeleteRotationScriptInput, + DeleteRotationScriptResult, +} from './rotation/rotationScriptTypes' export type AuthProvider = () => Auth export class PamManager { private readonly gatewayManager: GatewayManager private readonly configManager: ConfigManager + private readonly rotationManager: RotationManager constructor(storage: InMemoryStorage, authProvider: AuthProvider) { this.gatewayManager = new GatewayManager(storage, authProvider) this.configManager = new ConfigManager(storage, authProvider) + this.rotationManager = new RotationManager(storage, authProvider) } public getGatewayManager(): GatewayManager { @@ -50,6 +74,10 @@ export class PamManager { return this.configManager } + public getRotationManager(): RotationManager { + return this.rotationManager + } + public async listGateways(options: ListGatewaysOptions = {}): Promise { return this.gatewayManager.listGateways(options) } @@ -135,4 +163,87 @@ export class PamManager { ): string { return this.configManager.formatPamConfigurationsOutput(result, options) } + + public async listRotationSchedules(options: ListRotationSchedulesOptions = {}): Promise { + return this.rotationManager.listRotationSchedules(options) + } + + public formatRotationSchedulesTable( + result: ListRotationSchedulesResult, + options: FormatRotationSchedulesTableOptions = {} + ): FormattedRotationSchedulesTable { + return this.rotationManager.formatRotationSchedulesTable(result, options) + } + + public renderRotationSchedulesAsciiTable( + table: FormattedRotationSchedulesTable, + options: RenderRotationSchedulesAsciiTableOptions = {} + ): string { + return this.rotationManager.renderRotationSchedulesAsciiTable(table, options) + } + + public formatRotationSchedulesJson( + result: ListRotationSchedulesResult, + options: ListRotationSchedulesOptions = {} + ): string { + return this.rotationManager.formatRotationSchedulesJson(result, options) + } + + public formatRotationSchedulesOutput( + result: ListRotationSchedulesResult, + options: ListRotationSchedulesOptions = {} + ): string { + return this.rotationManager.formatRotationSchedulesOutput(result, options) + } + + public async getRotationInfo(input: GetRotationInfoInput): Promise { + return this.rotationManager.getRotationInfo(input) + } + + public formatRotationInfoJson(result: RotationInfoResult): string { + return this.rotationManager.formatRotationInfoJson(result) + } + + public formatRotationInfoOutput( + result: RotationInfoResult, + options: Pick = {} + ): string { + return this.rotationManager.formatRotationInfoOutput(result, options) + } + + public async editRotation(input: EditRotationInput): Promise { + return this.rotationManager.editRotation(input) + } + + public async listRotationScripts( + options: ListRotationScriptsOptions = {} + ): Promise { + return this.rotationManager.listRotationScripts(options) + } + + public formatRotationScriptsTable(result: ListRotationScriptsResult): string[][] { + return this.rotationManager.formatRotationScriptsTable(result) + } + + public formatRotationScriptsJson(result: ListRotationScriptsResult): string { + return this.rotationManager.formatRotationScriptsJson(result) + } + + public async addRotationScript( + input: AddRotationScriptInput + ): Promise { + return this.rotationManager.addRotationScript(input) + } + + public async editRotationScript( + input: EditRotationScriptInput + ): Promise { + return this.rotationManager.editRotationScript(input) + } + + public async deleteRotationScript( + input: DeleteRotationScriptInput + ): Promise { + return this.rotationManager.deleteRotationScript(input) + } } diff --git a/KeeperSdk/src/pam/index.ts b/KeeperSdk/src/pam/index.ts index 368ef8bc..b9291f85 100644 --- a/KeeperSdk/src/pam/index.ts +++ b/KeeperSdk/src/pam/index.ts @@ -168,3 +168,88 @@ export type { RemovedPamConfiguration, RemovePamConfigurationResult, } from './config' + +export { + RotationManager, + listRotationSchedules, + formatRotationSchedulesTable, + renderRotationSchedulesAsciiTable, + formatRotationSchedulesJson, + formatRotationSchedulesOutput, + getRotationInfo, + formatRotationInfoJson, + formatRotationInfoOutput, + editRotation, + validateRotationInput, + listRotationScripts, + formatRotationScriptsTable, + formatRotationScriptsJson, + addRotationScript, + editRotationScript, + deleteRotationScript, + RotationListFormat, + PAM_USER_RECORD_TYPE, + RECORD_INACCESSIBLE_LABEL, + RECORD_UNTITLED_LABEL, + RECORD_UNKNOWN_TYPE_LABEL, + NO_CONFIG_FOUND_LABEL, + GATEWAY_DOES_NOT_EXIST_LABEL, + MANUAL_ROTATION_LABEL, + EMPTY_SCHEDULE_LABEL, + EMPTY_ROTATION_SCHEDULES_MESSAGE, + DEFAULT_ROTATION_SCHEDULE_LABEL, + RECORD_ROTATION_KIND, + ROTATION_LIST_DEFAULT_HEADERS, + ROTATION_LIST_VERBOSE_HEADERS, + ROTATION_STATUS_ONLINE, + MISSING_VALUE_LABEL, + getVaultRecord, + recordExistsInVault, + getVaultRecordTitleType, + formatScheduleDataString, + formatRotationSchedule, + rotationStatusName, + isRotationOnline, + decryptPasswordComplexity, + isAdminResourceValid, + usesDefaultRotationSchedule, + resolveScheduleEnrichment, + buildPamConfigurationUidSet, + resolvePamConfigDisplay, + findGatewayByControllerUid, + buildOnlineGatewayUidSet, + resolveGatewayName, +} from './rotation' +export type { + AuthProvider as RotationAuthProvider, + RotationListFormatInput, + ListRotationSchedulesOptions, + RotationListRow, + ListRotationSchedulesResult, + FormattedRotationSchedulesTable, + FormatRotationSchedulesTableOptions, + RenderRotationSchedulesAsciiTableOptions, + RotationScheduleJsonEntry, + RotationSchedulesJsonPayload, + RotationScheduleType, + PasswordComplexityDetail, + GetRotationInfoInput, + RotationInfoResult, + RotationInfoJsonPayload, + RotationProfile, + PasswordComplexityInput, + ScheduleData, + EditRotationInput, + EditRotationResult, + RotationScriptValue, + RotationScript, + ListRotationScriptsResult, + AddRotationScriptInput, + AddRotationScriptResult, + EditRotationScriptInput, + EditRotationScriptResult, + DeleteRotationScriptInput, + DeleteRotationScriptResult, + RotationScriptListFormat, + ListRotationScriptsOptions, +} from './rotation' diff --git a/KeeperSdk/src/pam/rotation/RotationManager.ts b/KeeperSdk/src/pam/rotation/RotationManager.ts new file mode 100644 index 00000000..fb056830 --- /dev/null +++ b/KeeperSdk/src/pam/rotation/RotationManager.ts @@ -0,0 +1,153 @@ +import type { Auth } from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { KeeperSdkError, ResultCodes } from '../../utils' +import { formatRotationInfoJson, formatRotationInfoOutput, getRotationInfo } from './getRotationInfo' +import { + formatRotationSchedulesJson, + formatRotationSchedulesOutput, + formatRotationSchedulesTable, + listRotationSchedules, + renderRotationSchedulesAsciiTable, +} from './listRotations' +import { editRotation, validateRotationInput } from './editRotation' +import { + listRotationScripts, + formatRotationScriptsTable, + formatRotationScriptsJson, +} from './listRotationScripts' +import { addRotationScript } from './addRotationScript' +import { editRotationScript } from './editRotationScript' +import { deleteRotationScript } from './deleteRotationScript' +import type { + FormatRotationSchedulesTableOptions, + FormattedRotationSchedulesTable, + GetRotationInfoInput, + ListRotationSchedulesOptions, + ListRotationSchedulesResult, + RenderRotationSchedulesAsciiTableOptions, + RotationInfoResult, + EditRotationInput, + EditRotationResult, +} from './rotationTypes' +import type { + ListRotationScriptsOptions, + ListRotationScriptsResult, + AddRotationScriptInput, + AddRotationScriptResult, + EditRotationScriptInput, + EditRotationScriptResult, + DeleteRotationScriptInput, + DeleteRotationScriptResult, +} from './rotationScriptTypes' + +export type AuthProvider = () => Auth + +export class RotationManager { + private readonly storage: InMemoryStorage + private readonly authProvider: AuthProvider + + constructor(storage: InMemoryStorage, authProvider: AuthProvider) { + this.storage = storage + this.authProvider = authProvider + } + + private requireAuth(): Auth { + const auth = this.authProvider() + if (!auth?.sessionToken) { + throw new KeeperSdkError('Not logged in. Call login() first.', ResultCodes.NOT_LOGGED_IN) + } + return auth + } + + public async listRotationSchedules(options: ListRotationSchedulesOptions = {}): Promise { + return listRotationSchedules(this.requireAuth(), this.storage, options) + } + + public formatRotationSchedulesTable( + result: ListRotationSchedulesResult, + options: FormatRotationSchedulesTableOptions = {} + ): FormattedRotationSchedulesTable { + return formatRotationSchedulesTable(result, options) + } + + public renderRotationSchedulesAsciiTable( + table: FormattedRotationSchedulesTable, + options: RenderRotationSchedulesAsciiTableOptions = {} + ): string { + return renderRotationSchedulesAsciiTable(table, options) + } + + public formatRotationSchedulesJson( + result: ListRotationSchedulesResult, + options: ListRotationSchedulesOptions = {} + ): string { + return formatRotationSchedulesJson(result, options) + } + + public formatRotationSchedulesOutput( + result: ListRotationSchedulesResult, + options: ListRotationSchedulesOptions = {} + ): string { + return formatRotationSchedulesOutput(result, options) + } + + public async getRotationInfo(input: GetRotationInfoInput): Promise { + return getRotationInfo(this.requireAuth(), this.storage, input) + } + + public formatRotationInfoJson(result: RotationInfoResult): string { + return formatRotationInfoJson(result) + } + + public formatRotationInfoOutput( + result: RotationInfoResult, + options: Pick = {} + ): string { + return formatRotationInfoOutput(result, options) + } + + public async editRotation(input: EditRotationInput): Promise { + const errors = validateRotationInput(input) + if (errors.length > 0) { + throw new KeeperSdkError( + `Invalid rotation input: ${errors.join('; ')}`, + ResultCodes.PAM_ROTATION_RECORD_REQUIRED + ) + } + return editRotation(this.requireAuth(), this.storage, input) + } + + /* ========== Rotation Script Operations ========== */ + + public async listRotationScripts( + options: ListRotationScriptsOptions = {} + ): Promise { + return listRotationScripts(this.requireAuth(), this.storage, options) + } + + public formatRotationScriptsTable(result: ListRotationScriptsResult): string[][] { + return formatRotationScriptsTable(result) + } + + public formatRotationScriptsJson(result: ListRotationScriptsResult): string { + return formatRotationScriptsJson(result) + } + + public async addRotationScript( + input: AddRotationScriptInput + ): Promise { + return addRotationScript(this.requireAuth(), this.storage, input) + } + + public async editRotationScript( + input: EditRotationScriptInput + ): Promise { + return editRotationScript(this.requireAuth(), this.storage, input) + } + + public async deleteRotationScript( + input: DeleteRotationScriptInput + ): Promise { + return deleteRotationScript(this.requireAuth(), this.storage, input) + } +} diff --git a/KeeperSdk/src/pam/rotation/addRotationScript.ts b/KeeperSdk/src/pam/rotation/addRotationScript.ts new file mode 100644 index 00000000..663d77be --- /dev/null +++ b/KeeperSdk/src/pam/rotation/addRotationScript.ts @@ -0,0 +1,91 @@ +import type { Auth } from '@keeper-security/keeperapi' +import type { DRecord } from '@keeper-security/keeperapi' +import { generateUid } from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { getRecordTitle, getRecordType } from '../../records/RecordUtils' +import type { AddRotationScriptInput, AddRotationScriptResult, RotationScriptValue } from './rotationScriptTypes' +import { KeeperSdkError, ResultCodes, extractErrorMessage } from '../../utils' +import { + getSinglePamRecord, + getRecordTitleSafe, + validateScriptFileExists, + updatePamRecordFields, +} from './rotationScriptHelpers' +import { SCRIPT_FIELD_TYPE, SCRIPT_FIELD_LABEL } from './rotationConstants' +import type { PamRecordData } from './rotationScriptTypes' + +export async function addRotationScript( + auth: Auth, + storage: InMemoryStorage, + input: AddRotationScriptInput +): Promise { + const warnings: string[] = [] + + try { + const scriptPath = input.scriptPath?.trim() + if (!scriptPath) { + throw new KeeperSdkError( + 'Script file path is required', + ResultCodes.INVALID_PATTERN + ) + } + + const expandedPath = validateScriptFileExists(scriptPath) + const record = getSinglePamRecord(storage, input.record) + const recordType = getRecordType(record) + const currentRevision = record.revision || 0 + + const recordData = (record.data as PamRecordData) || { fields: [] } + const dataFields = recordData.fields || [] + + const fs = require('fs') + const path = require('path') + const fileName = path.basename(expandedPath) + + const fileUid = generateUid() + const scriptValue: RotationScriptValue = { + fileRef: fileUid, + recordRef: [], + command: input.scriptCommand || '', + } + + if (Array.isArray(input.credentialUids)) { + for (const credUid of input.credentialUids) { + const credRecord = storage.getByUid(1 as any, credUid) + if (!credRecord) { + warnings.push(`Credential record not found: ${credUid}`) + continue + } + scriptValue.recordRef.push(credUid) + } + } + + const newScriptField = { + type: SCRIPT_FIELD_TYPE, + label: SCRIPT_FIELD_LABEL, + value: [scriptValue], + } + + dataFields.push(newScriptField) + recordData.fields = dataFields + ;(record as any).data = recordData + + await updatePamRecordFields(auth, record, recordType, dataFields, currentRevision, storage) + + return { + success: true, + recordUid: record.uid, + scriptFileUid: fileUid, + message: `Script "${fileName}" added to record "${getRecordTitleSafe(record)}"`, + warnings, + } + } catch (err) { + if (err instanceof KeeperSdkError) { + throw err + } + throw new KeeperSdkError( + `Failed to add rotation script: ${extractErrorMessage(err)}`, + ResultCodes.PAM_CONFIG_CREATE_FAILED + ) + } +} diff --git a/KeeperSdk/src/pam/rotation/deleteRotationScript.ts b/KeeperSdk/src/pam/rotation/deleteRotationScript.ts new file mode 100644 index 00000000..e903b8bb --- /dev/null +++ b/KeeperSdk/src/pam/rotation/deleteRotationScript.ts @@ -0,0 +1,84 @@ +import type { Auth } from '@keeper-security/keeperapi' +import type { DRecord } from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { getRecordType } from '../../records/RecordUtils' +import type { DeleteRotationScriptInput, DeleteRotationScriptResult } from './rotationScriptTypes' +import { KeeperSdkError, ResultCodes, extractErrorMessage } from '../../utils' +import { + getSinglePamRecord, + getRecordTitleSafe, + findScriptFieldsInRecord, + findScriptByUidOrName, + updatePamRecordFields, +} from './rotationScriptHelpers' +import type { PamRecordData } from './rotationScriptTypes' + +/** + * Delete a rotation script from a PAM record + */ +export async function deleteRotationScript( + auth: Auth, + storage: InMemoryStorage, + input: DeleteRotationScriptInput +): Promise { + const warnings: string[] = [] + + try { + const record = getSinglePamRecord(storage, input.record) + const recordType = getRecordType(record) + const currentRevision = record.revision || 0 + + const recordData = (record.data as PamRecordData) || { fields: [] } + const dataFields = recordData.fields || [] + + const scriptFields = findScriptFieldsInRecord(recordData) + if (scriptFields.length === 0) { + throw new KeeperSdkError( + `Record "${getRecordTitleSafe(record)}" has no rotation scripts`, + ResultCodes.PAM_CONFIG_NOT_FOUND + ) + } + + let targetScript = scriptFields[0] + const scriptName = input.script?.trim() + + if (scriptName) { + const found = findScriptByUidOrName(storage, recordData, scriptName) + if (!found) { + throw new KeeperSdkError( + `Record "${getRecordTitleSafe(record)}" does not have script "${scriptName}"`, + ResultCodes.PAM_CONFIG_NOT_FOUND + ) + } + targetScript = found.location + } + + const field = dataFields[targetScript.fieldIndex] + const scriptArray = field.value as any[] + scriptArray.splice(targetScript.scriptIndex, 1) + + if (scriptArray.length === 0) { + dataFields.splice(targetScript.fieldIndex, 1) + } + + recordData.fields = dataFields + ;(record as any).data = recordData + + await updatePamRecordFields(auth, record, recordType, dataFields, currentRevision, storage) + + return { + success: true, + recordUid: record.uid, + message: `Script "${targetScript.script.fileRef}" deleted from record "${getRecordTitleSafe(record)}"`, + warnings, + } + } catch (err) { + if (err instanceof KeeperSdkError) { + throw err + } + throw new KeeperSdkError( + `Failed to delete rotation script: ${extractErrorMessage(err)}`, + ResultCodes.PAM_CONFIG_EDIT_FAILED + ) + } +} diff --git a/KeeperSdk/src/pam/rotation/editRotation.ts b/KeeperSdk/src/pam/rotation/editRotation.ts new file mode 100644 index 00000000..bcae04b1 --- /dev/null +++ b/KeeperSdk/src/pam/rotation/editRotation.ts @@ -0,0 +1,374 @@ +import type { Auth, DRecordRotation } from '@keeper-security/keeperapi' +import { normal64Bytes, platform, Router, setRecordRotationMessage } from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../../utils' +import { getRecordTitle } from '../../records/RecordUtils' +import { + EditRotationInput, + EditRotationResult, + PasswordComplexityInput, + RotationProfile, + ScheduleData, +} from './rotationTypes' +import { + getVaultRecord, + recordExistsInVault, + getVaultRecordTitleType, +} from './rotationHelpers' +import { RECORD_ROTATION_KIND } from './rotationConstants' + +const DEFAULT_PAM_SPECIAL_CHAR = '!@#$%^&*()_+-=[]{}|;:,.<>?' + +export async function editRotation( + auth: Auth, + storage: InMemoryStorage, + input: EditRotationInput +): Promise { + const recordUid = input.recordUid?.trim() || '' + if (!recordUid) { + throw new KeeperSdkError( + 'Record UID is required for PAM rotation edit.', + ResultCodes.PAM_ROTATION_RECORD_REQUIRED + ) + } + + let recordUidBytes: Uint8Array + try { + recordUidBytes = normal64Bytes(recordUid) + } catch (err) { + throw new KeeperSdkError( + `Invalid record UID "${recordUid}": ${extractErrorMessage(err)}`, + ResultCodes.PAM_ROTATION_RECORD_REQUIRED + ) + } + + const record = getVaultRecord(storage, recordUid) + if (!record) { + throw new KeeperSdkError( + `Record UID "${recordUid}" not found in vault.`, + ResultCodes.PAM_ROTATION_RECORD_REQUIRED + ) + } + + const validRecords: EditRotationResult['validRecords'] = [] + const skippedRecords: EditRotationResult['skippedRecords'] = [] + + try { + const currentRotation = storage.getByUid( + RECORD_ROTATION_KIND, + recordUid + ) + + if (!currentRotation && !input.configUid && !input.iamAadConfigUid && !input.saasConfigUid) { + throw new KeeperSdkError( + `Record "${recordUid}" does not have rotation configured yet. ` + + `You must provide a PAM Configuration UID (--config) to configure rotation for this record.`, + ResultCodes.PAM_ROTATION_RECORD_REQUIRED + ) + } + + let configUid = input.configUid?.trim() + if (!configUid && currentRotation?.configurationUid) { + configUid = currentRotation.configurationUid + } + + if (!configUid && !currentRotation) { + throw new KeeperSdkError( + `PAM Configuration UID is required when setting up rotation for the first time. ` + + `Provide it with --config, --iam-aad-config, or --saas-config.`, + ResultCodes.PAM_ROTATION_RECORD_REQUIRED + ) + } + + let resourceUid = input.resourceUid?.trim() + if (!resourceUid && currentRotation?.resourceUid) { + resourceUid = currentRotation.resourceUid + } + + let currentRotationRevision = 0 + if (currentRotation?.revision) { + currentRotationRevision = currentRotation.revision + } + + let scheduleData = validateAndBuildScheduleData(input) + if (!scheduleData && currentRotation?.schedule) { + try { + scheduleData = typeof currentRotation.schedule === 'string' + ? JSON.parse(currentRotation.schedule) + : currentRotation.schedule + } catch (e) { + scheduleData = null + } + } + + let currentSchedule = '' + let currentComplexity = new Uint8Array() + let currentDisabled = false + let currentResourceUid = new Uint8Array() + + if (currentRotation) { + if (currentRotation.schedule) { + try { + currentSchedule = typeof currentRotation.schedule === 'string' + ? currentRotation.schedule + : JSON.stringify(currentRotation.schedule) + } catch (e) { + currentSchedule = '' + } + } + + if (currentRotation.pwdComplexity) { + if (typeof currentRotation.pwdComplexity === 'string') { + try { + currentComplexity = platform.base64ToBytes(currentRotation.pwdComplexity) + } catch (e) { + currentComplexity = new Uint8Array() + } + } else if (currentRotation.pwdComplexity instanceof Uint8Array) { + currentComplexity = currentRotation.pwdComplexity + } else if (typeof currentRotation.pwdComplexity === 'object') { + const reencrypted = await encryptPasswordComplexity( + storage, + recordUid, + currentRotation.pwdComplexity as any + ) + if (reencrypted) { + currentComplexity = platform.base64ToBytes(reencrypted) + } + } + } + + currentDisabled = currentRotation.disabled === true + + if (currentRotation.resourceUid) { + try { + currentResourceUid = normal64Bytes(currentRotation.resourceUid) + } catch (e) { + currentResourceUid = new Uint8Array() + } + } + } + + let passwordComplexityEncrypted = currentComplexity + if (input.passwordComplexity) { + const encrypted = await encryptPasswordComplexity( + storage, + recordUid, + input.passwordComplexity + ) + if (encrypted) { + passwordComplexityEncrypted = platform.base64ToBytes(encrypted) + } + } + + let finalScheduleData = scheduleData + if (!finalScheduleData && currentSchedule) { + try { + finalScheduleData = typeof currentSchedule === 'string' + ? JSON.parse(currentSchedule) + : currentSchedule + } catch (e) { + finalScheduleData = null + } + } + + let finalDisabled = currentDisabled + if (input.enable === true) { + finalDisabled = false + } else if (input.disable === true) { + finalDisabled = true + } + + let finalResourceUidBytes = currentResourceUid + if (input.resourceUid?.trim()) { + finalResourceUidBytes = normal64Bytes(input.resourceUid) + } + + let schedule = finalScheduleData ? formatScheduleType(finalScheduleData) : 'On-Demand' + let complexity = input.passwordComplexity ? formatComplexity(input.passwordComplexity) : '' + + const configUidBytes = configUid ? normal64Bytes(configUid) : new Uint8Array() + + const rotationRequest: Router.IRouterRecordRotationRequest = { + revision: currentRotationRevision, + recordUid: recordUidBytes, + configurationUid: configUidBytes, + resourceUid: finalResourceUidBytes, + schedule: finalScheduleData ? JSON.stringify(finalScheduleData) : currentSchedule, + pwdComplexity: passwordComplexityEncrypted, + disabled: finalDisabled, + noop: input.scheduleOnly ? true : false, + } + + const message = setRecordRotationMessage(rotationRequest) + await auth.executeRouterRestAction(message) + + const finalEnabled = !finalDisabled + + validRecords.push({ + recordUid, + recordTitle: getRecordTitle(record) || '[untitled]', + enabled: finalEnabled, + configUid: configUid || '', + resourceUid: resourceUid, + schedule, + complexity, + }) + + return { + successful: true, + validRecords, + skippedRecords, + message: + skippedRecords.length > 0 + ? `Updated ${validRecords.length} record(s), skipped ${skippedRecords.length} record(s)` + : `Successfully updated ${validRecords.length} record(s)`, + } + } catch (err) { + throw new KeeperSdkError( + `Failed to update rotation: ${extractErrorMessage(err)}`, + ResultCodes.PAM_ROTATION_INFO_FAILED + ) + } +} + +function validateAndBuildScheduleData(input: EditRotationInput): ScheduleData[] | null { + if (input.onDemand) { + return [] + } + + if (input.scheduleJson && input.scheduleJson.length > 0) { + return input.scheduleJson + } + + if (input.scheduleCron) { + return [ + { + type: 'CRON', + expression: input.scheduleCron, + }, + ] + } + + if (input.scheduleConfig) { + return null + } + + return null +} + +function formatScheduleType(scheduleData: ScheduleData[] | null): string { + if (!scheduleData || scheduleData.length === 0) { + return 'On-Demand' + } + return (scheduleData[0] as any).type || 'Scheduled' +} + +function formatComplexity(complexity: PasswordComplexityInput): string { + const specialChars = complexity.specialChars || DEFAULT_PAM_SPECIAL_CHAR + return `${complexity.length},${complexity.caps},${complexity.lowercase},${complexity.digits},${complexity.special},${specialChars}` +} + +async function encryptPasswordComplexity( + storage: InMemoryStorage, + recordUid: string, + complexity: PasswordComplexityInput | undefined +): Promise { + if (!complexity) { + return null + } + + const recordKey = await storage.getKeyBytes(recordUid) + if (!recordKey) { + return null + } + + try { + const specialChars = complexity.specialChars || DEFAULT_PAM_SPECIAL_CHAR + const complexityData = { + length: complexity.length, + caps: complexity.caps, + lowercase: complexity.lowercase, + digits: complexity.digits, + special: complexity.special, + specialChars, + } + + const plainText = JSON.stringify(complexityData) + const encrypted = await platform.aesGcmEncrypt( + platform.stringToBytes(plainText), + recordKey + ) + return platform.bytesToBase64(encrypted) + } catch (err) { + throw new KeeperSdkError( + `Failed to encrypt password complexity: ${extractErrorMessage(err)}`, + ResultCodes.PAM_ROTATION_INFO_FAILED + ) + } +} + +function validateRotationProfile(input: EditRotationInput): string[] { + const errors: string[] = [] + const profile = input.rotationProfile + + if (profile === 'general') { + if (!input.resourceUid) { + errors.push('General rotation profile requires resourceUid') + } + } else if (profile === 'iam_user') { + if (!input.iamAadConfigUid && !input.configUid) { + errors.push('IAM user rotation requires iamAadConfigUid or configUid') + } + } else if (profile === 'scripts_only') { + if (!input.configUid) { + errors.push('Scripts-only rotation requires configUid') + } + } else if (profile === 'saas') { + if (!input.saasConfigUid) { + errors.push('SaaS rotation requires saasConfigUid') + } + } + + return errors +} + +export function validateRotationInput(input: EditRotationInput): string[] { + const errors: string[] = [] + + if (!input.recordUid && !input.folderUid) { + errors.push('Either recordUid or folderUid is required') + } + + if (input.recordUid && input.folderUid) { + errors.push('Cannot specify both recordUid and folderUid') + } + + if (input.enable && input.disable) { + errors.push('Cannot enable and disable rotation at the same time') + } + + const scheduleCount = + (input.onDemand ? 1 : 0) + + (input.scheduleJson ? 1 : 0) + + (input.scheduleCron ? 1 : 0) + + (input.scheduleConfig ? 1 : 0) + + if (scheduleCount > 1) { + errors.push('Only one schedule option can be specified') + } + + if (input.passwordComplexity) { + if ( + input.passwordComplexity.length < 1 || + input.passwordComplexity.caps < 0 || + input.passwordComplexity.lowercase < 0 || + input.passwordComplexity.digits < 0 || + input.passwordComplexity.special < 0 + ) { + errors.push('Password complexity values must be non-negative') + } + } + + return errors +} diff --git a/KeeperSdk/src/pam/rotation/editRotationScript.ts b/KeeperSdk/src/pam/rotation/editRotationScript.ts new file mode 100644 index 00000000..6815be73 --- /dev/null +++ b/KeeperSdk/src/pam/rotation/editRotationScript.ts @@ -0,0 +1,110 @@ +import type { Auth } from '@keeper-security/keeperapi' +import type { DRecord } from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { VaultObjectKind } from '../../folders/folderHelpers' +import { getRecordTitle, getRecordType } from '../../records/RecordUtils' +import type { EditRotationScriptInput, EditRotationScriptResult } from './rotationScriptTypes' +import { KeeperSdkError, ResultCodes, extractErrorMessage } from '../../utils' +import { + getSinglePamRecord, + getRecordTitleSafe, + findScriptByUidOrName, + updatePamRecordFields, +} from './rotationScriptHelpers' +import type { PamRecordData } from './rotationScriptTypes' + + +export async function editRotationScript( + auth: Auth, + storage: InMemoryStorage, + input: EditRotationScriptInput +): Promise { + const warnings: string[] = [] + + try { + const scriptName = input.script?.trim() + if (!scriptName) { + throw new KeeperSdkError( + 'Script UID or name is required', + ResultCodes.INVALID_PATTERN + ) + } + + const record = getSinglePamRecord(storage, input.record) + const recordType = getRecordType(record) + const currentRevision = record.revision || 0 + + const recordData = (record.data as PamRecordData) || { fields: [] } + const dataFields = recordData.fields || [] + + const found = findScriptByUidOrName(storage, recordData, scriptName) + if (!found) { + throw new KeeperSdkError( + `Record "${getRecordTitleSafe(record)}" does not have script "${scriptName}"`, + ResultCodes.PAM_CONFIG_NOT_FOUND + ) + } + + const scriptValue = found.location.script + let modified = false + + const credentialSet = new Set(scriptValue.recordRef || []) + + if (Array.isArray(input.removeCredentials) && input.removeCredentials.length > 0) { + for (const credUid of input.removeCredentials) { + credentialSet.delete(credUid) + } + modified = true + } + + if (Array.isArray(input.addCredentials) && input.addCredentials.length > 0) { + for (const credUid of input.addCredentials) { + const credRecord = storage.getByUid(VaultObjectKind.Record, credUid) + if (!credRecord) { + warnings.push(`Credential record not found: ${credUid}`) + continue + } + credentialSet.add(credUid) + } + modified = true + } + + if (modified) { + scriptValue.recordRef = Array.from(credentialSet) + } + + if (input.scriptCommand !== undefined && input.scriptCommand !== null) { + scriptValue.command = input.scriptCommand + modified = true + } + + if (!modified) { + return { + success: true, + recordUid: record.uid, + message: 'No changes specified', + warnings, + } + } + + recordData.fields = dataFields + ;(record as any).data = recordData + + await updatePamRecordFields(auth, record, recordType, dataFields, currentRevision, storage) + + return { + success: true, + recordUid: record.uid, + message: `Script updated for record "${getRecordTitleSafe(record)}"`, + warnings, + } + } catch (err) { + if (err instanceof KeeperSdkError) { + throw err + } + throw new KeeperSdkError( + `Failed to edit rotation script: ${extractErrorMessage(err)}`, + ResultCodes.PAM_CONFIG_EDIT_FAILED + ) + } +} diff --git a/KeeperSdk/src/pam/rotation/getRotationInfo.ts b/KeeperSdk/src/pam/rotation/getRotationInfo.ts new file mode 100644 index 00000000..eaf19f34 --- /dev/null +++ b/KeeperSdk/src/pam/rotation/getRotationInfo.ts @@ -0,0 +1,205 @@ +import type { Auth } from '@keeper-security/keeperapi' +import { + getRotationInfoMessage, + normal64Bytes, + pamGetRotationSchedulesMessage, +} from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../../utils' +import { toFiniteNumber, webSafeUidFromBytes } from '../gateway/gatewayHelpers' +import { MISSING_VALUE_LABEL, MANUAL_ROTATION_LABEL } from './rotationConstants' +import { + decryptPasswordComplexity, + formatScheduleDataString, + isAdminResourceValid, + isRotationOnline, + resolveScheduleEnrichment, + rotationStatusName, + usesDefaultRotationSchedule, +} from './rotationHelpers' +import { + RotationListFormat, + type GetRotationInfoInput, + type RotationInfoJsonPayload, + type RotationInfoResult, +} from './rotationTypes' + +export async function getRotationInfo( + auth: Auth, + storage: InMemoryStorage, + input: GetRotationInfoInput +): Promise { + const recordUid = input.recordUid?.trim() || '' + if (!recordUid) { + throw new KeeperSdkError( + 'Record UID is required for pam rotation info.', + ResultCodes.PAM_ROTATION_RECORD_REQUIRED + ) + } + + let recordUidBytes: Uint8Array + try { + recordUidBytes = normal64Bytes(recordUid) + } catch (err) { + throw new KeeperSdkError( + `Invalid record UID "${recordUid}": ${extractErrorMessage(err)}`, + ResultCodes.PAM_ROTATION_RECORD_REQUIRED + ) + } + + let rotationInfo + try { + rotationInfo = await auth.executeRest(getRotationInfoMessage({ uid: recordUidBytes })) + } catch (err) { + throw new KeeperSdkError( + `Failed to get PAM rotation info: ${extractErrorMessage(err)}`, + ResultCodes.PAM_ROTATION_INFO_FAILED + ) + } + + const status = rotationStatusName(rotationInfo.status) + if (!isRotationOnline(rotationInfo.status)) { + return { + status, + readyToRotate: false, + useDefaultRotationSchedule: false, + recordUid, + } + } + + const pamConfigUid = webSafeUidFromBytes(rotationInfo.configurationUid) + const gatewayUid = webSafeUidFromBytes(rotationInfo.controllerUid) || MISSING_VALUE_LABEL + const adminResourceUid = rotationInfo.resourceUid?.length + ? webSafeUidFromBytes(rotationInfo.resourceUid) + : null + const passwordComplexity = rotationInfo.pwdComplexity || null + const passwordComplexityDetail = await decryptPasswordComplexity(storage, recordUid, passwordComplexity) + + let scheduleType = null as RotationInfoResult['scheduleType'] + let scheduleData = null as string | null + try { + const schedulesResponse = await auth.executeRouterRest(pamGetRotationSchedulesMessage({})) + const enrichment = resolveScheduleEnrichment(schedulesResponse.schedules ?? [], recordUidBytes) + scheduleType = enrichment.scheduleType + scheduleData = enrichment.scheduleData + } catch { + // Schedule enrichment is optional; REST rotation info remains valid without it. + } + + let scheduleDisplay: string | null = null + if (scheduleType === 'manual') scheduleDisplay = MANUAL_ROTATION_LABEL + else if (scheduleType === 'scheduled') scheduleDisplay = formatScheduleDataString(scheduleData) + + return { + status, + readyToRotate: true, + useDefaultRotationSchedule: usesDefaultRotationSchedule(storage, recordUid, pamConfigUid), + recordUid, + pamConfigUid, + nodeId: toFiniteNumber(rotationInfo.nodeId) || undefined, + gatewayName: rotationInfo.controllerName || MISSING_VALUE_LABEL, + gatewayUid, + adminResourceUid, + adminResourceValid: adminResourceUid ? isAdminResourceValid(storage, adminResourceUid, pamConfigUid) : null, + passwordComplexity, + passwordComplexityDetail, + scheduleType, + scheduleData, + scheduleDisplay, + disabled: rotationInfo.disabled === true, + scriptName: rotationInfo.scriptName || null, + } +} + +export function formatRotationInfoJson(result: RotationInfoResult): string { + const payload: RotationInfoJsonPayload = { + status: result.status, + ready_to_rotate: result.readyToRotate, + use_default_rotation_schedule: result.useDefaultRotationSchedule, + } + + if (!result.readyToRotate) { + return JSON.stringify(payload, null, 2) + } + + payload.record_uid = result.recordUid + payload.pam_config_uid = result.pamConfigUid + payload.node_id = result.nodeId + payload.gateway_name = result.gatewayName + payload.gateway_uid = result.gatewayUid + payload.admin_resource_uid = result.adminResourceUid ?? null + payload.admin_resource_valid = result.adminResourceValid ?? null + payload.password_complexity = result.passwordComplexity ?? null + payload.password_complexity_detail = result.passwordComplexityDetail ?? null + payload.schedule_type = result.scheduleType ?? null + payload.schedule_data = result.scheduleData ?? null + payload.disabled = result.disabled === true + payload.script_name = result.scriptName ?? null + + return JSON.stringify(payload, null, 2) +} + +function formatPasswordComplexityData(detail: NonNullable): string { + const symbolsChars = + detail.specialChars != null && String(detail.specialChars).trim() !== '' + ? String(detail.specialChars) + : 'None' + return [ + `Length: ${detail.length ?? ''}`, + `Lowercase: ${detail.lowercase ?? ''}`, + `Uppercase: ${detail.caps ?? ''}`, + `Digits: ${detail.digits ?? ''}`, + `Symbols: ${detail.special ?? ''}`, + `Symbols Chars: ${symbolsChars}`, + ].join('; ') +} + +function formatRotationStatusLine(result: RotationInfoResult): string { + if (result.readyToRotate || result.status === 'RRS_ONLINE') { + return 'Ready to rotate (RRS_ONLINE)' + } + return result.status || '' +} + +function formatScheduleTypeLine(result: RotationInfoResult): string { + if (result.scheduleType === 'manual') return 'Manual Rotation' + if (result.scheduleType === 'scheduled') { + return result.scheduleDisplay || result.scheduleData || 'Scheduled' + } + return '' +} + +function formatRotationInfoDetail(result: RotationInfoResult): string { + const rows: Array<[string, string]> = [['Rotation Status', formatRotationStatusLine(result)]] + + if (result.readyToRotate) { + rows.push(['PAM Config UID', result.pamConfigUid || '']) + if (result.nodeId != null) rows.push(['Node ID', String(result.nodeId)]) + rows.push([ + 'Gateway Name where the rotation will be performed', + result.gatewayName || MISSING_VALUE_LABEL, + ]) + rows.push(['Gateway Uid', result.gatewayUid || MISSING_VALUE_LABEL]) + if (result.adminResourceUid) rows.push(['Admin Resource Uid', result.adminResourceUid]) + if (result.passwordComplexity) rows.push(['Password Complexity', result.passwordComplexity]) + if (result.passwordComplexityDetail) { + rows.push(['Password Complexity Data', formatPasswordComplexityData(result.passwordComplexityDetail)]) + } + rows.push(['Is Rotation Disabled', result.disabled ? 'True' : 'False']) + + const scheduleType = formatScheduleTypeLine(result) + if (scheduleType) rows.push(['Schedule Type', scheduleType]) + } + + const labelWidth = Math.max(...rows.map(([label]) => label.length), 1) + return rows.map(([label, value]) => `${label.padStart(labelWidth)}: ${value}`).join('\n') +} + +export function formatRotationInfoOutput( + result: RotationInfoResult, + options: Pick = {} +): string { + const format = String(options.format || RotationListFormat.Table).toLowerCase() + if (format === RotationListFormat.Json) return formatRotationInfoJson(result) + return formatRotationInfoDetail(result) +} diff --git a/KeeperSdk/src/pam/rotation/index.ts b/KeeperSdk/src/pam/rotation/index.ts new file mode 100644 index 00000000..fe124158 --- /dev/null +++ b/KeeperSdk/src/pam/rotation/index.ts @@ -0,0 +1,98 @@ +export { RotationManager } from './RotationManager' +export type { AuthProvider } from './RotationManager' + +export { + listRotationSchedules, + formatRotationSchedulesTable, + renderRotationSchedulesAsciiTable, + formatRotationSchedulesJson, + formatRotationSchedulesOutput, +} from './listRotations' + +export { getRotationInfo, formatRotationInfoJson, formatRotationInfoOutput } from './getRotationInfo' + +export { editRotation, validateRotationInput } from './editRotation' + +export { + listRotationScripts, + formatRotationScriptsTable, + formatRotationScriptsJson, +} from './listRotationScripts' + +export { addRotationScript } from './addRotationScript' +export { editRotationScript } from './editRotationScript' +export { deleteRotationScript } from './deleteRotationScript' + +export { RotationListFormat } from './rotationTypes' +export type { + RotationListFormatInput, + ListRotationSchedulesOptions, + RotationListRow, + ListRotationSchedulesResult, + FormattedRotationSchedulesTable, + FormatRotationSchedulesTableOptions, + RenderRotationSchedulesAsciiTableOptions, + RotationScheduleJsonEntry, + RotationSchedulesJsonPayload, + RotationScheduleType, + PasswordComplexityDetail, + GetRotationInfoInput, + RotationInfoResult, + RotationInfoJsonPayload, + RotationProfile, + PasswordComplexityInput, + ScheduleData, + EditRotationInput, + EditRotationResult, +} from './rotationTypes' + +export type { + RotationScriptValue, + RotationScript, + ListRotationScriptsResult, + AddRotationScriptInput, + AddRotationScriptResult, + EditRotationScriptInput, + EditRotationScriptResult, + DeleteRotationScriptInput, + DeleteRotationScriptResult, + RotationScriptListFormat, + ListRotationScriptsOptions, +} from './rotationScriptTypes' + +export { + PAM_USER_RECORD_TYPE, + RECORD_INACCESSIBLE_LABEL, + RECORD_UNTITLED_LABEL, + RECORD_UNKNOWN_TYPE_LABEL, + NO_CONFIG_FOUND_LABEL, + GATEWAY_DOES_NOT_EXIST_LABEL, + MANUAL_ROTATION_LABEL, + EMPTY_SCHEDULE_LABEL, + EMPTY_ROTATION_SCHEDULES_MESSAGE, + DEFAULT_ROTATION_SCHEDULE_LABEL, + RECORD_ROTATION_KIND, + ROTATION_LIST_DEFAULT_HEADERS, + ROTATION_LIST_VERBOSE_HEADERS, + ROTATION_STATUS_ONLINE, + MISSING_VALUE_LABEL, +} from './rotationConstants' + +export { + getVaultRecord, + recordExistsInVault, + getVaultRecordTitleType, + formatScheduleDataString, + formatRotationSchedule, + rotationStatusName, + isRotationOnline, + decryptPasswordComplexity, + isAdminResourceValid, + usesDefaultRotationSchedule, + resolveScheduleEnrichment, + buildPamConfigurationUidSet, + resolvePamConfigDisplay, + findGatewayByControllerUid, + buildOnlineGatewayUidSet, + resolveGatewayName, +} from './rotationHelpers' diff --git a/KeeperSdk/src/pam/rotation/listRotationScripts.ts b/KeeperSdk/src/pam/rotation/listRotationScripts.ts new file mode 100644 index 00000000..75239331 --- /dev/null +++ b/KeeperSdk/src/pam/rotation/listRotationScripts.ts @@ -0,0 +1,115 @@ +import type { Auth } from '@keeper-security/keeperapi' +import type { DRecord } from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { VaultObjectKind } from '../../folders/folderHelpers' +import { getRecordTitle, getRecordType } from '../../records/RecordUtils' +import type { RotationScript, ListRotationScriptsResult, ListRotationScriptsOptions } from './rotationScriptTypes' +import { findScriptFieldsInRecord } from './rotationScriptHelpers' +import type { PamRecordData } from './rotationScriptTypes' + +export async function listRotationScripts( + _auth: Auth, + storage: InMemoryStorage, + options: ListRotationScriptsOptions = {} +): Promise { + const pattern = options.pattern?.toLowerCase() || '' + const scripts: RotationScript[] = [] + + try { + const allRecords = storage.getRecords() + + for (const record of allRecords) { + if (!record) continue + + const recordType = getRecordType(record) + if (recordType !== 'pamUser' && recordType !== 'pamDirectory') { + continue + } + + if (pattern) { + const recordUid = record.uid.toLowerCase() + const title = (getRecordTitle(record) || '').toLowerCase() + if (!recordUid.includes(pattern) && !title.includes(pattern)) { + continue + } + } + + const recordData = (record.data as PamRecordData) || { fields: [] } + const scriptFields = findScriptFieldsInRecord(recordData) + + if (scriptFields.length > 0) { + const location = scriptFields[0] + const scriptValue = location.script + + const fileRecord = storage.getByUid(VaultObjectKind.Record, scriptValue.fileRef) + if (fileRecord) { + const recordRefs = scriptValue.recordRef || [] + const command = scriptValue.command || '' + + scripts.push({ + recordUid: record.uid, + recordTitle: getRecordTitle(record) || '', + recordType, + scriptUid: scriptValue.fileRef, + scriptName: getRecordTitle(fileRecord) || '', + recordRefs: Array.isArray(recordRefs) ? recordRefs : [], + command: typeof command === 'string' ? command : '', + }) + } + } + } + + return { + success: true, + scripts, + message: `Found ${scripts.length} rotation script(s)`, + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err) + return { + success: false, + scripts: [], + message: `Failed to list rotation scripts: ${message}`, + } + } +} + +export function formatRotationScriptsTable(result: ListRotationScriptsResult): string[][] { + if (!result.success || result.scripts.length === 0) { + return [] + } + + const headers = [ + 'Record UID', + 'Record Title', + 'Record Type', + 'Script UID', + 'Script Name', + 'Credentials', + 'Command', + ] + + const rows = result.scripts.map((script) => [ + script.recordUid, + script.recordTitle, + script.recordType, + script.scriptUid, + script.scriptName, + script.recordRefs.join(', '), + script.command, + ]) + + return [headers, ...rows] +} + +export function formatRotationScriptsJson(result: ListRotationScriptsResult): string { + return JSON.stringify( + { + success: result.success, + message: result.message, + scripts: result.scripts, + }, + null, + 2 + ) +} diff --git a/KeeperSdk/src/pam/rotation/listRotations.ts b/KeeperSdk/src/pam/rotation/listRotations.ts new file mode 100644 index 00000000..8e6f5735 --- /dev/null +++ b/KeeperSdk/src/pam/rotation/listRotations.ts @@ -0,0 +1,196 @@ +import type { Auth } from '@keeper-security/keeperapi' +import { pamGetOnlineControllersMessage, pamGetRotationSchedulesMessage } from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../../utils' +import { + fetchEnterprisePamControllers, + webSafeUidFromBytes, +} from '../gateway/gatewayHelpers' +import { + EMPTY_ROTATION_SCHEDULES_MESSAGE, + PAM_USER_RECORD_TYPE, + RECORD_UNKNOWN_TYPE_LABEL, + RECORD_UNTITLED_LABEL, + ROTATION_LIST_DEFAULT_HEADERS, + ROTATION_LIST_VERBOSE_HEADERS, +} from './rotationConstants' +import { + buildOnlineGatewayUidSet, + buildPamConfigurationUidSet, + findGatewayByControllerUid, + formatRotationSchedule, + getVaultRecordTitleType, + recordExistsInVault, + resolveGatewayName, + resolvePamConfigDisplay, +} from './rotationHelpers' +import { + RotationListFormat, + type FormatRotationSchedulesTableOptions, + type FormattedRotationSchedulesTable, + type ListRotationSchedulesOptions, + type ListRotationSchedulesResult, + type RenderRotationSchedulesAsciiTableOptions, + type RotationListRow, + type RotationScheduleJsonEntry, + type RotationSchedulesJsonPayload, +} from './rotationTypes' + +export async function listRotationSchedules( + auth: Auth, + storage: InMemoryStorage, + _options: ListRotationSchedulesOptions = {} +): Promise { + let schedules + try { + const response = await auth.executeRouterRest(pamGetRotationSchedulesMessage({})) + schedules = response.schedules ?? [] + } catch (err) { + throw new KeeperSdkError( + `Failed to list PAM rotation schedules: ${extractErrorMessage(err)}`, + ResultCodes.PAM_ROTATION_LIST_FAILED + ) + } + + let allGateways + let onlineControllers + try { + ;[allGateways, onlineControllers] = await Promise.all([ + fetchEnterprisePamControllers(auth, ResultCodes.PAM_ROTATION_LIST_FAILED), + auth.executeRouterRest(pamGetOnlineControllersMessage()).then((response) => response.controllers ?? []), + ]) + } catch (err) { + throw new KeeperSdkError( + `Failed to list PAM gateways for rotation schedules: ${extractErrorMessage(err)}`, + ResultCodes.PAM_ROTATION_LIST_FAILED + ) + } + + const onlineUids = buildOnlineGatewayUidSet(onlineControllers) + const pamConfigUids = buildPamConfigurationUidSet(storage) + const rotations: RotationListRow[] = [] + + for (const schedule of schedules) { + const recordUid = webSafeUidFromBytes(schedule.recordUid) + const configurationUid = webSafeUidFromBytes(schedule.configurationUid) + const gatewayUid = webSafeUidFromBytes(schedule.controllerUid) + const [recordTitle, recordType] = getVaultRecordTitleType(storage, recordUid) + + if (recordType !== PAM_USER_RECORD_TYPE) continue + + const gateway = findGatewayByControllerUid(allGateways, schedule.controllerUid) + rotations.push({ + recordUid, + recordTitle: recordTitle || RECORD_UNTITLED_LABEL, + recordType: recordType || RECORD_UNKNOWN_TYPE_LABEL, + schedule: formatRotationSchedule(schedule), + gatewayName: resolveGatewayName(gateway), + gatewayUid, + gatewayOnline: !!gatewayUid && onlineUids.has(gatewayUid), + pamConfigDisplay: resolvePamConfigDisplay(storage, configurationUid, pamConfigUids), + pamConfigurationUid: configurationUid, + recordAccessible: recordExistsInVault(storage, recordUid), + }) + } + + rotations.sort((a, b) => (a.recordTitle || '').localeCompare(b.recordTitle || '')) + + return { + rotations, + message: rotations.length === 0 ? EMPTY_ROTATION_SCHEDULES_MESSAGE : undefined, + } +} + +export function formatRotationSchedulesTable( + result: ListRotationSchedulesResult, + options: FormatRotationSchedulesTableOptions = {} +): FormattedRotationSchedulesTable { + const verbose = options.verbose === true + const headers: string[] = [...ROTATION_LIST_DEFAULT_HEADERS] + if (verbose) headers.push(...ROTATION_LIST_VERBOSE_HEADERS) + + const rows = result.rotations.map((rotation) => { + const row: string[] = [ + rotation.recordUid, + rotation.recordTitle, + rotation.recordType, + rotation.schedule, + rotation.gatewayName, + rotation.gatewayOnline ? 'Online' : 'Offline', + rotation.pamConfigDisplay, + ] + if (verbose) { + row.push(rotation.gatewayUid, rotation.pamConfigurationUid) + } + return row + }) + + return { headers, rows } +} + +export function renderRotationSchedulesAsciiTable( + table: FormattedRotationSchedulesTable, + options: RenderRotationSchedulesAsciiTableOptions = {} +): string { + const minColWidth = options.minColWidth ?? 2 + const widths = table.headers.map((header, col) => { + let width = Math.max(header.length, minColWidth) + for (const row of table.rows) { + width = Math.max(width, (row[col] || '').length) + } + return width + }) + + const formatRow = (cells: string[]): string => + cells.map((cell, i) => (cell || '').padEnd(widths[i])).join(' ').trimEnd() + + return [ + formatRow([...table.headers]), + widths.map((w) => '-'.repeat(w)).join(' '), + ...table.rows.map(formatRow), + ].join('\n') +} + +function toJsonEntry(rotation: RotationListRow, verbose: boolean): RotationScheduleJsonEntry { + const entry: RotationScheduleJsonEntry = { + record_uid: rotation.recordUid, + record_title: rotation.recordTitle, + record_type: rotation.recordType, + schedule: rotation.schedule, + gateway_name: rotation.gatewayName, + gateway_online: rotation.gatewayOnline, + pam_config: rotation.pamConfigDisplay, + record_accessible: rotation.recordAccessible, + } + if (verbose) { + entry.gateway_uid = rotation.gatewayUid + entry.pam_configuration_uid = rotation.pamConfigurationUid + } + return entry +} + +export function formatRotationSchedulesJson( + result: ListRotationSchedulesResult, + options: ListRotationSchedulesOptions = {} +): string { + const verbose = options.verbose === true + const payload: RotationSchedulesJsonPayload = { + rotations: result.rotations.map((rotation) => toJsonEntry(rotation, verbose)), + } + if (result.message) payload.message = result.message + return JSON.stringify(payload, null, 2) +} + +export function formatRotationSchedulesOutput( + result: ListRotationSchedulesResult, + options: ListRotationSchedulesOptions = {} +): string { + const format = String(options.format || RotationListFormat.Table).toLowerCase() + if (format === RotationListFormat.Json) return formatRotationSchedulesJson(result, options) + + if (result.message && result.rotations.length === 0) return result.message + + return renderRotationSchedulesAsciiTable( + formatRotationSchedulesTable(result, { verbose: options.verbose }) + ) +} diff --git a/KeeperSdk/src/pam/rotation/rotationConstants.ts b/KeeperSdk/src/pam/rotation/rotationConstants.ts new file mode 100644 index 00000000..d3e18cdc --- /dev/null +++ b/KeeperSdk/src/pam/rotation/rotationConstants.ts @@ -0,0 +1,31 @@ +export const DEFAULT_ROTATION_SCHEDULE_LABEL = 'Default Rotation Schedule' as const +export const EMPTY_SCHEDULE_LABEL = 'No Schedule' as const +export const GATEWAY_DOES_NOT_EXIST_LABEL = '[Gateway Does Not Exist]' as const +export const MANUAL_ROTATION_LABEL = 'Manual Rotation' as const +export const NO_CONFIG_FOUND_LABEL = '[No Config Found]' as const +export const RECORD_INACCESSIBLE_LABEL = '[Inaccessible Record]' as const +export const RECORD_UNKNOWN_TYPE_LABEL = '[Unknown Type]' as const +export const RECORD_UNTITLED_LABEL = '[Untitled]' as const +export const PAM_USER_RECORD_TYPE = 'pamUser' as const +export const RECORD_ROTATION_KIND = 'non_shared_data' as const +export const EMPTY_ROTATION_SCHEDULES_MESSAGE = 'No rotation schedules found' as const +export const ROTATION_LIST_DEFAULT_HEADERS = [ + 'Record', + 'Status', + 'Schedule', + 'Last', +] as const +export const ROTATION_LIST_VERBOSE_HEADERS = [ + 'Record UID', + 'Record Title', + 'Type', + 'Status', + 'Schedule', + 'Last', + 'Next', +] as const +export const ROTATION_STATUS_ONLINE = 'RRS_ONLINE' as const +export const MISSING_VALUE_LABEL = 'N/A' as const + +export const SCRIPT_FIELD_TYPE = 'script' as const +export const SCRIPT_FIELD_LABEL = 'rotationScripts' as const diff --git a/KeeperSdk/src/pam/rotation/rotationHelpers.ts b/KeeperSdk/src/pam/rotation/rotationHelpers.ts new file mode 100644 index 00000000..90854476 --- /dev/null +++ b/KeeperSdk/src/pam/rotation/rotationHelpers.ts @@ -0,0 +1,188 @@ +import type { DRecord, DRecordRotation, PAM } from '@keeper-security/keeperapi' +import { normal64Bytes, platform, Router } from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { VaultObjectKind } from '../../folders/folderHelpers' +import { getKeeperDriveRecord } from '../../nestedShareFolders/nsfHelpers' +import { getRecordFields, getRecordTitle, getRecordType } from '../../records/RecordUtils' +import { SCHEDULE_FIELD_TYPE } from '../config/configConstants' +import { listPamConfigurationRecords, parsePamResources } from '../config/configHelpers' +import { controllerUidsEqual, webSafeUidFromBytes } from '../gateway/gatewayHelpers' +import { + DEFAULT_ROTATION_SCHEDULE_LABEL, + EMPTY_SCHEDULE_LABEL, + GATEWAY_DOES_NOT_EXIST_LABEL, + MANUAL_ROTATION_LABEL, + NO_CONFIG_FOUND_LABEL, + RECORD_INACCESSIBLE_LABEL, + RECORD_ROTATION_KIND, + RECORD_UNKNOWN_TYPE_LABEL, + RECORD_UNTITLED_LABEL, + ROTATION_STATUS_ONLINE, +} from './rotationConstants' +import type { PasswordComplexityDetail, RotationScheduleType } from './rotationTypes' + +export function getVaultRecord(storage: InMemoryStorage, recordUid: string): DRecord | undefined { + if (!recordUid) return undefined + return ( + storage.getByUid(VaultObjectKind.Record, recordUid) || getKeeperDriveRecord(storage, recordUid) + ) +} + +export function recordExistsInVault(storage: InMemoryStorage, recordUid: string): boolean { + return !!getVaultRecord(storage, recordUid) +} + +export function getVaultRecordTitleType(storage: InMemoryStorage, recordUid: string): [string, string] { + const record = getVaultRecord(storage, recordUid) + if (!record) return [RECORD_INACCESSIBLE_LABEL, RECORD_INACCESSIBLE_LABEL] + + const title = getRecordTitle(record) + const type = getRecordType(record) + const displayTitle = + title && title !== '(untitled)' && title !== '(no data)' ? title : RECORD_UNTITLED_LABEL + return [displayTitle, type || RECORD_UNKNOWN_TYPE_LABEL] +} + +export function formatScheduleDataString(scheduleData: string | null | undefined): string { + if (!scheduleData) return EMPTY_SCHEDULE_LABEL + const raw = scheduleData.replace('RotateActionJob|', '') + const parts = raw.split('.') + if (parts.length === 4) { + return `${parts[0]} on ${parts[1]} at ${parts[2]} UTC with interval count of ${parts[3]}` + } + if (parts.length === 3) { + return `${parts[0]} at ${parts[1]} UTC with interval count of ${parts[2]}` + } + return scheduleData +} + +export function formatRotationSchedule(schedule: PAM.IPAMRotationSchedule): string { + if (schedule.noSchedule === true) return MANUAL_ROTATION_LABEL + return formatScheduleDataString(schedule.scheduleData) +} + +export function rotationStatusName(status: Router.RouterRotationStatus | number | null | undefined): string { + if (status == null) return '' + const name = Router.RouterRotationStatus[status as number] + return typeof name === 'string' ? name : String(status) +} + +export function isRotationOnline(status: Router.RouterRotationStatus | number | null | undefined): boolean { + return rotationStatusName(status) === ROTATION_STATUS_ONLINE +} + +export async function decryptPasswordComplexity( + storage: InMemoryStorage, + recordUid: string, + pwdComplexityRaw: string | null | undefined +): Promise { + if (!pwdComplexityRaw) return null + const recordKey = await storage.getKeyBytes(recordUid) + if (!recordKey) return null + try { + const plain = await platform.aesGcmDecrypt(normal64Bytes(pwdComplexityRaw), recordKey) + const parsed = JSON.parse(platform.bytesToString(plain)) as PasswordComplexityDetail + return parsed && typeof parsed === 'object' ? parsed : null + } catch { + return null + } +} + +export function isAdminResourceValid( + storage: InMemoryStorage, + resourceUid: string, + configurationUid: string +): boolean { + if (!resourceUid || !configurationUid) return false + if (!recordExistsInVault(storage, resourceUid)) return false + const configuration = getVaultRecord(storage, configurationUid) + if (!configuration) return false + const resources = parsePamResources(configuration).resourceRecordUids + return resources.includes(resourceUid) +} + +function getDefaultScheduleFromPamConfig(record: DRecord): unknown[] | undefined { + const fields = getRecordFields(record) + const byLabel = fields.find( + (field) => field.type === SCHEDULE_FIELD_TYPE && field.label === DEFAULT_ROTATION_SCHEDULE_LABEL + ) + const field = byLabel || fields.find((entry) => entry.type === SCHEDULE_FIELD_TYPE) + if (!field?.value?.length) return undefined + return field.value +} + +export function usesDefaultRotationSchedule( + storage: InMemoryStorage, + recordUid: string, + configurationUid: string +): boolean { + const config = getVaultRecord(storage, configurationUid) + if (!config) return false + const defaultSchedule = getDefaultScheduleFromPamConfig(config) + if (!defaultSchedule) return false + + const cached = storage.getByUid(RECORD_ROTATION_KIND, recordUid) + if (!cached?.schedule) return false + + let recordSchedule: unknown + try { + recordSchedule = JSON.parse(cached.schedule) + } catch { + return false + } + if (!Array.isArray(recordSchedule) || recordSchedule.length === 0) return false + return JSON.stringify(recordSchedule) === JSON.stringify(defaultSchedule) +} + +export function resolveScheduleEnrichment( + schedules: readonly PAM.IPAMRotationSchedule[], + recordUidBytes: Uint8Array +): { scheduleType: RotationScheduleType | null; scheduleData: string | null } { + for (const schedule of schedules) { + if (!controllerUidsEqual(schedule.recordUid, recordUidBytes)) continue + if (schedule.noSchedule === true) { + return { scheduleType: 'manual', scheduleData: null } + } + return { + scheduleType: 'scheduled', + scheduleData: schedule.scheduleData || null, + } + } + return { scheduleType: null, scheduleData: null } +} + +export function buildPamConfigurationUidSet(storage: InMemoryStorage): Set { + return new Set(listPamConfigurationRecords(storage).map((record) => record.uid)) +} + +export function resolvePamConfigDisplay( + storage: InMemoryStorage, + configurationUid: string, + pamConfigUids: Set +): string { + if (!configurationUid || !pamConfigUids.has(configurationUid)) return NO_CONFIG_FOUND_LABEL + + const [cfgTitle, cfgType] = getVaultRecordTitleType(storage, configurationUid) + return `${cfgTitle || RECORD_UNTITLED_LABEL} (${cfgType || RECORD_UNKNOWN_TYPE_LABEL})` +} + +export function findGatewayByControllerUid( + controllers: readonly PAM.IPAMController[], + controllerUid: Uint8Array | null | undefined +): PAM.IPAMController | undefined { + if (!controllerUid?.length) return undefined + return controllers.find((controller) => controllerUidsEqual(controller.controllerUid, controllerUid)) +} + +export function buildOnlineGatewayUidSet(controllers: readonly PAM.IPAMOnlineController[]): Set { + const online = new Set() + for (const controller of controllers) { + const uid = webSafeUidFromBytes(controller.controllerUid) + if (uid) online.add(uid) + } + return online +} + +export function resolveGatewayName(controller: PAM.IPAMController | undefined): string { + return controller?.controllerName || GATEWAY_DOES_NOT_EXIST_LABEL +} diff --git a/KeeperSdk/src/pam/rotation/rotationScriptHelpers.ts b/KeeperSdk/src/pam/rotation/rotationScriptHelpers.ts new file mode 100644 index 00000000..be7b5b93 --- /dev/null +++ b/KeeperSdk/src/pam/rotation/rotationScriptHelpers.ts @@ -0,0 +1,195 @@ +import * as fs from 'fs' +import * as path from 'path' +import type { Auth, DRecord } from '@keeper-security/keeperapi' +import type { InMemoryStorage } from '../../storage/InMemoryStorage' +import { VaultObjectKind } from '../../folders/folderHelpers' +import { getRecordTitle, getRecordType } from '../../records/RecordUtils' +import { updateRecord } from '../../records/RecordOperations' +import { KeeperSdkError, ResultCodes } from '../../utils' +import { SCRIPT_FIELD_TYPE } from './rotationConstants' +import type { + PamRecordData, + RotationScriptValue, + ScriptFieldLocation, + ScriptSearchResult, +} from './rotationScriptTypes' + +export function findPamRecordsByName(storage: InMemoryStorage, searchText: string): DRecord[] { + const results: DRecord[] = [] + const searchLower = searchText.toLowerCase() + + const allRecords = storage.getRecords() + for (const record of allRecords) { + if (!record) continue + + const recordType = getRecordType(record) + if (recordType !== 'pamUser' && recordType !== 'pamDirectory') { + continue + } + + const title = getRecordTitle(record) || '' + if ( + record.uid === searchText || + record.uid.toLowerCase().includes(searchLower) || + title.toLowerCase().includes(searchLower) + ) { + results.push(record) + } + } + + return results +} + +export function getRecordTitleSafe(record: DRecord): string { + return getRecordTitle(record) || record.uid +} + +export function getSinglePamRecord(storage: InMemoryStorage, recordName: string): DRecord { + const recordNameTrimmed = recordName?.trim() + if (!recordNameTrimmed) { + throw new KeeperSdkError('Record UID or title is required', ResultCodes.INVALID_PATTERN) + } + + const records = findPamRecordsByName(storage, recordNameTrimmed) + if (records.length === 0) { + throw new KeeperSdkError( + `PAM record not found: ${recordNameTrimmed}`, + ResultCodes.PAM_CONFIG_NOT_FOUND + ) + } + + if (records.length > 1) { + throw new KeeperSdkError( + `Multiple records match "${recordNameTrimmed}". Use record UID for specificity.`, + ResultCodes.PAM_MULTIPLE_CONFIG_MATCHES + ) + } + + const record = records[0] + const recordType = getRecordType(record) + if (recordType !== 'pamUser' && recordType !== 'pamDirectory') { + throw new KeeperSdkError( + `Record "${recordNameTrimmed}" is not a PAM record (pamUser/pamDirectory)`, + ResultCodes.PAM_CONFIG_INVALID + ) + } + + return record +} + +export function findScriptFieldsInRecord(recordData: PamRecordData): ScriptFieldLocation[] { + const results: ScriptFieldLocation[] = [] + + const dataFields = recordData.fields || [] + for (let i = 0; i < dataFields.length; i++) { + const field = dataFields[i] + if (field.type !== SCRIPT_FIELD_TYPE) continue + + const value = field.value as unknown + if (!Array.isArray(value)) continue + + for (let j = 0; j < value.length; j++) { + const script = value[j] + if ( + script && + typeof script === 'object' && + 'fileRef' in script && + typeof (script as any).fileRef === 'string' + ) { + results.push({ + fieldIndex: i, + scriptIndex: j, + script: script as RotationScriptValue, + }) + } + } + } + + return results +} + +export function findScriptByUidOrName( + storage: InMemoryStorage, + recordData: PamRecordData, + searchText: string +): ScriptSearchResult | undefined { + const scripts = findScriptFieldsInRecord(recordData) + + for (const location of scripts) { + if (location.script.fileRef === searchText) { + return { location, matchType: 'uid' } + } + } + + const searchLower = searchText.toLowerCase() + for (const location of scripts) { + const scriptFile = storage.getByUid( + VaultObjectKind.Record, + location.script.fileRef + ) + if (scriptFile) { + const title = (getRecordTitle(scriptFile) || '').toLowerCase() + if (title.includes(searchLower)) { + return { location, matchType: 'name' } + } + } + } + + return undefined +} + +export function expandFilePath(filePath: string): string { + return filePath.startsWith('~') + ? path.join(process.env.HOME || '', filePath.slice(1)) + : filePath +} + +export function validateScriptFileExists(filePath: string): string { + const expandedPath = expandFilePath(filePath) + if (!fs.existsSync(expandedPath)) { + throw new KeeperSdkError( + `Script file not found: ${filePath}`, + ResultCodes.PAM_CONFIG_CREATE_FAILED + ) + } + return expandedPath +} + +export async function updatePamRecordFields( + auth: Auth, + record: DRecord, + recordType: string, + fields: unknown[], + currentRevision: number, + storage: InMemoryStorage +): Promise { + const recordKey = await storage.getKeyBytes(record.uid) + if (!recordKey) { + throw new KeeperSdkError( + `Unable to retrieve encryption key for record "${record.uid}"`, + ResultCodes.PAM_CONFIG_EDIT_FAILED + ) + } + + const recordData = (record.data as PamRecordData) || { fields: [] } + const updateResult = await updateRecord( + auth, + record.uid, + { + type: recordType, + title: getRecordTitle(record) || '', + fields: fields as any, + notes: recordData.notes || '', + }, + currentRevision, + recordKey + ) + + if (!updateResult.success) { + throw new KeeperSdkError( + `Failed to save changes: ${updateResult.status || 'unknown error'}`, + ResultCodes.PAM_CONFIG_EDIT_FAILED + ) + } + record.revision = currentRevision + 1 +} diff --git a/KeeperSdk/src/pam/rotation/rotationScriptTypes.ts b/KeeperSdk/src/pam/rotation/rotationScriptTypes.ts new file mode 100644 index 00000000..4ac72e73 --- /dev/null +++ b/KeeperSdk/src/pam/rotation/rotationScriptTypes.ts @@ -0,0 +1,116 @@ +import type { ListRotationSchedulesResult } from './rotationTypes' + +export interface PasswordComplexitySpec { + length?: number + caps?: number + lowercase?: number + digits?: number + special?: number +} + +export interface RecordField { + type: string + label?: string + value: unknown[] + required?: boolean + privacyScreen?: boolean + enforceGeneration?: boolean + complexity?: PasswordComplexitySpec +} + +export interface ScriptField extends RecordField { + type: 'script' + value: RotationScriptValue[] +} + +export interface PamRecordData { + fields: RecordField[] + custom?: RecordField[] + notes?: string +} + +export interface RotationScriptValue { + fileRef: string + recordRef: string[] + command: string +} + +export interface ScriptFieldLocation { + fieldIndex: number + scriptIndex: number + script: RotationScriptValue +} + +export interface ScriptSearchResult { + location: ScriptFieldLocation + matchType: 'uid' | 'name' +} + +/** + * Rotation script metadata + */ +export interface RotationScript { + recordUid: string + recordTitle: string + recordType: string + scriptUid: string + scriptName: string + recordRefs: string[] + command: string +} + +export interface ListRotationScriptsResult { + success: boolean + scripts: RotationScript[] + message: string +} + +export interface AddRotationScriptInput { + record: string + scriptPath: string + credentialUids?: string[] + scriptCommand?: string +} + +export interface AddRotationScriptResult { + success: boolean + recordUid: string + scriptFileUid: string + message: string + warnings: string[] +} + + +export interface EditRotationScriptInput { + record: string + script: string + addCredentials?: string[] + removeCredentials?: string[] + scriptCommand?: string +} + +export interface EditRotationScriptResult { + success: boolean + recordUid: string + message: string + warnings: string[] +} + +export interface DeleteRotationScriptInput { + record: string + script: string +} + +export interface DeleteRotationScriptResult { + success: boolean + recordUid: string + message: string + warnings: string[] +} + +export type RotationScriptListFormat = 'table' | 'json' + +export interface ListRotationScriptsOptions { + pattern?: string + format?: RotationScriptListFormat +} diff --git a/KeeperSdk/src/pam/rotation/rotationTypes.ts b/KeeperSdk/src/pam/rotation/rotationTypes.ts new file mode 100644 index 00000000..037cab7f --- /dev/null +++ b/KeeperSdk/src/pam/rotation/rotationTypes.ts @@ -0,0 +1,174 @@ +export enum RotationListFormat { + Table = 'table', + Json = 'json', +} + +export type RotationListFormatInput = RotationListFormat | `${RotationListFormat}` + +export type ListRotationSchedulesOptions = { + verbose?: boolean + format?: RotationListFormatInput +} + +export type RotationListRow = { + recordUid: string + recordTitle: string + recordType: string + schedule: string + gatewayName: string + gatewayUid: string + gatewayOnline: boolean + pamConfigDisplay: string + pamConfigurationUid: string + recordAccessible: boolean +} + +export type ListRotationSchedulesResult = { + rotations: RotationListRow[] + message?: string +} + +export type FormattedRotationSchedulesTable = { + headers: string[] + rows: string[][] +} + +export type FormatRotationSchedulesTableOptions = { + verbose?: boolean +} + +export type RenderRotationSchedulesAsciiTableOptions = { + minColWidth?: number +} + +export type RotationScheduleJsonEntry = { + record_uid: string + record_title: string + record_type: string + schedule: string + gateway_name: string + gateway_online: boolean + pam_config: string + record_accessible: boolean + gateway_uid?: string + pam_configuration_uid?: string +} + +export type RotationSchedulesJsonPayload = { + rotations: RotationScheduleJsonEntry[] + message?: string +} + +export type RotationScheduleType = 'manual' | 'scheduled' + +export type PasswordComplexityDetail = { + length?: number + lowercase?: number + caps?: number + digits?: number + special?: number + specialChars?: string +} + +export type GetRotationInfoInput = { + recordUid: string + format?: RotationListFormatInput +} + +export type RotationInfoResult = { + status: string + readyToRotate: boolean + useDefaultRotationSchedule: boolean + recordUid?: string + pamConfigUid?: string + nodeId?: number + gatewayName?: string + gatewayUid?: string + adminResourceUid?: string | null + adminResourceValid?: boolean | null + passwordComplexity?: string | null + passwordComplexityDetail?: PasswordComplexityDetail | null + scheduleType?: RotationScheduleType | null + scheduleData?: string | null + scheduleDisplay?: string | null + disabled?: boolean + scriptName?: string | null +} + +export type RotationInfoJsonPayload = { + status: string + ready_to_rotate: boolean + use_default_rotation_schedule: boolean + record_uid?: string + pam_config_uid?: string + node_id?: number + gateway_name?: string + gateway_uid?: string + admin_resource_uid?: string | null + admin_resource_valid?: boolean | null + password_complexity?: string | null + password_complexity_detail?: PasswordComplexityDetail | null + schedule_type?: RotationScheduleType | null + schedule_data?: string | null + disabled?: boolean + script_name?: string | null +} + +export type RotationProfile = 'general' | 'iam_user' | 'scripts_only' | 'saas' + +export type PasswordComplexityInput = { + length: number + caps: number + lowercase: number + digits: number + special: number + specialChars?: string +} + +export type ScheduleData = { + type: string + utcTime?: string + weekday?: string + intervalCount?: number + [key: string]: any +} + +export type EditRotationInput = { + recordUid?: string + folderUid?: string + configUid?: string + resourceUid?: string + iamAadConfigUid?: string + rotationProfile?: RotationProfile + saasConfigUid?: string + scheduleJson?: ScheduleData[] + scheduleCron?: string + onDemand?: boolean + scheduleConfig?: boolean + scheduleOnly?: boolean + passwordComplexity?: PasswordComplexityInput + adminUserUid?: string + enable?: boolean + disable?: boolean + force?: boolean +} + +export type EditRotationResult = { + successful: boolean + validRecords: Array<{ + recordUid: string + recordTitle: string + enabled: boolean + configUid: string + resourceUid?: string + schedule: string + complexity: string + }> + skippedRecords: Array<{ + recordUid: string + recordTitle: string + problem: string + description: string + }> + message?: string +} diff --git a/KeeperSdk/src/utils/constants.ts b/KeeperSdk/src/utils/constants.ts index a81e750a..f619e9b3 100644 --- a/KeeperSdk/src/utils/constants.ts +++ b/KeeperSdk/src/utils/constants.ts @@ -187,6 +187,9 @@ export enum PamErrorCode { ConfigEditFailed = 'pam_config_edit_failed', MultipleConfigMatches = 'pam_multiple_config_matches', ConfigRemoveFailed = 'pam_config_remove_failed', + RotationListFailed = 'pam_rotation_list_failed', + RotationInfoFailed = 'pam_rotation_info_failed', + RotationRecordRequired = 'pam_rotation_record_required', } export enum UserErrorCode { @@ -368,6 +371,9 @@ export const ResultCodes = { PAM_CONFIG_EDIT_FAILED: PamErrorCode.ConfigEditFailed, PAM_MULTIPLE_CONFIG_MATCHES: PamErrorCode.MultipleConfigMatches, PAM_CONFIG_REMOVE_FAILED: PamErrorCode.ConfigRemoveFailed, + PAM_ROTATION_LIST_FAILED: PamErrorCode.RotationListFailed, + PAM_ROTATION_INFO_FAILED: PamErrorCode.RotationInfoFailed, + PAM_ROTATION_RECORD_REQUIRED: PamErrorCode.RotationRecordRequired, } as const export const KEEPER_PUBLIC_HOSTS: Record = { diff --git a/KeeperSdk/src/vault/KeeperVault.ts b/KeeperSdk/src/vault/KeeperVault.ts index 00454131..09d9a551 100644 --- a/KeeperSdk/src/vault/KeeperVault.ts +++ b/KeeperSdk/src/vault/KeeperVault.ts @@ -227,6 +227,27 @@ import type { RemovePamConfigurationInput, RemovePamConfigurationResult, } from '../pam/config/configTypes' +import type { + FormatRotationSchedulesTableOptions, + FormattedRotationSchedulesTable, + GetRotationInfoInput, + ListRotationSchedulesOptions, + ListRotationSchedulesResult, + RenderRotationSchedulesAsciiTableOptions, + RotationInfoResult, + EditRotationInput, + EditRotationResult, +} from '../pam/rotation/rotationTypes' +import type { + ListRotationScriptsOptions, + ListRotationScriptsResult, + AddRotationScriptInput, + AddRotationScriptResult, + EditRotationScriptInput, + EditRotationScriptResult, + DeleteRotationScriptInput, + DeleteRotationScriptResult, +} from '../pam/rotation/rotationScriptTypes' import { buildWhoamiInfo, type WhoamiInfo } from '../account/whoamiInfo' import { ConsoleLogger, @@ -1411,6 +1432,87 @@ export class KeeperVault { return this.pamManager.formatPamConfigurationsOutput(result, options ?? {}) } + public async listRotationSchedules( + options?: ListRotationSchedulesOptions + ): Promise { + return this.pamManager.listRotationSchedules(options ?? {}) + } + + public formatRotationSchedulesTable( + result: ListRotationSchedulesResult, + options?: FormatRotationSchedulesTableOptions + ): FormattedRotationSchedulesTable { + return this.pamManager.formatRotationSchedulesTable(result, options ?? {}) + } + + public renderRotationSchedulesAsciiTable( + table: FormattedRotationSchedulesTable, + options?: RenderRotationSchedulesAsciiTableOptions + ): string { + return this.pamManager.renderRotationSchedulesAsciiTable(table, options ?? {}) + } + + public formatRotationSchedulesJson( + result: ListRotationSchedulesResult, + options?: ListRotationSchedulesOptions + ): string { + return this.pamManager.formatRotationSchedulesJson(result, options ?? {}) + } + + public formatRotationSchedulesOutput( + result: ListRotationSchedulesResult, + options?: ListRotationSchedulesOptions + ): string { + return this.pamManager.formatRotationSchedulesOutput(result, options ?? {}) + } + + public async getRotationInfo(input: GetRotationInfoInput): Promise { + return this.pamManager.getRotationInfo(input) + } + + public formatRotationInfoJson(result: RotationInfoResult): string { + return this.pamManager.formatRotationInfoJson(result) + } + + public formatRotationInfoOutput( + result: RotationInfoResult, + options?: Pick + ): string { + return this.pamManager.formatRotationInfoOutput(result, options ?? {}) + } + + public async editRotation(input: EditRotationInput): Promise { + return this.pamManager.editRotation(input) + } + + public async listRotationScripts( + options?: ListRotationScriptsOptions + ): Promise { + return this.pamManager.listRotationScripts(options ?? {}) + } + + public formatRotationScriptsTable(result: ListRotationScriptsResult): string[][] { + return this.pamManager.formatRotationScriptsTable(result) + } + + public formatRotationScriptsJson(result: ListRotationScriptsResult): string { + return this.pamManager.formatRotationScriptsJson(result) + } + + public async addRotationScript(input: AddRotationScriptInput): Promise { + return this.pamManager.addRotationScript(input) + } + + public async editRotationScript(input: EditRotationScriptInput): Promise { + return this.pamManager.editRotationScript(input) + } + + public async deleteRotationScript( + input: DeleteRotationScriptInput + ): Promise { + return this.pamManager.deleteRotationScript(input) + } + public async shareFolder(input: ShareFolderInput): Promise { const result = await this.sharedFolderManager.shareFolder(input) if (result.success) await this.syncIfNeeded() diff --git a/keeperapi/src/restMessages.ts b/keeperapi/src/restMessages.ts index e3e4e7b0..df777bd2 100644 --- a/keeperapi/src/restMessages.ts +++ b/keeperapi/src/restMessages.ts @@ -1036,6 +1036,26 @@ export const sendControllerMessage = ( ): RestMessage => createMessage(data, 'api/user/send_controller_message', Router.RouterControllerMessage, PAM.ControllerResponse) +export const pamGetRotationSchedulesMessage = ( + data: PAM.IPAMGenericUidsRequest = {} +): RestMessage => + createMessage( + data, + 'api/user/get_rotation_schedules', + PAM.PAMGenericUidsRequest, + PAM.PAMRotationSchedulesResponse + ) + +export const getRotationInfoMessage = ( + data: PAM.IPAMGenericUidRequest +): RestMessage => + createMessage(data, 'pam/get_rotation_info', PAM.PAMGenericUidRequest, Router.RouterRotationInfo) + +export const setRecordRotationMessage = ( + data: Router.IRouterRecordRotationRequest +): RestInMessage => + createInMessage(data, 'api/user/set_record_rotation', Router.RouterRecordRotationRequest) + export const pamConfigureNetworkGraphMessage = ( data: Router.IPAMNetworkConfigurationRequest ): RestInMessage => From 328074320fbeda02bfe842aa89135429a2b6340a Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Thu, 27 Aug 2026 15:43:57 +0530 Subject: [PATCH 2/3] format fix --- KeeperSdk/src/pam/PamManager.ts | 20 +++------ KeeperSdk/src/pam/rotation/RotationManager.ts | 26 ++++------- .../src/pam/rotation/addRotationScript.ts | 5 +-- KeeperSdk/src/pam/rotation/editRotation.ts | 44 +++++++------------ .../src/pam/rotation/editRotationScript.ts | 6 +-- KeeperSdk/src/pam/rotation/getRotationInfo.ts | 19 ++------ KeeperSdk/src/pam/rotation/index.ts | 6 +-- .../src/pam/rotation/listRotationScripts.ts | 10 +---- KeeperSdk/src/pam/rotation/listRotations.ts | 14 +++--- .../src/pam/rotation/rotationConstants.ts | 7 +-- KeeperSdk/src/pam/rotation/rotationHelpers.ts | 13 ++---- .../src/pam/rotation/rotationScriptHelpers.ts | 26 +++-------- .../src/pam/rotation/rotationScriptTypes.ts | 1 - KeeperSdk/src/vault/KeeperVault.ts | 12 ++--- 14 files changed, 56 insertions(+), 153 deletions(-) diff --git a/KeeperSdk/src/pam/PamManager.ts b/KeeperSdk/src/pam/PamManager.ts index 6f6c34a1..7bf23f3c 100644 --- a/KeeperSdk/src/pam/PamManager.ts +++ b/KeeperSdk/src/pam/PamManager.ts @@ -164,7 +164,9 @@ export class PamManager { return this.configManager.formatPamConfigurationsOutput(result, options) } - public async listRotationSchedules(options: ListRotationSchedulesOptions = {}): Promise { + public async listRotationSchedules( + options: ListRotationSchedulesOptions = {} + ): Promise { return this.rotationManager.listRotationSchedules(options) } @@ -215,9 +217,7 @@ export class PamManager { return this.rotationManager.editRotation(input) } - public async listRotationScripts( - options: ListRotationScriptsOptions = {} - ): Promise { + public async listRotationScripts(options: ListRotationScriptsOptions = {}): Promise { return this.rotationManager.listRotationScripts(options) } @@ -229,21 +229,15 @@ export class PamManager { return this.rotationManager.formatRotationScriptsJson(result) } - public async addRotationScript( - input: AddRotationScriptInput - ): Promise { + public async addRotationScript(input: AddRotationScriptInput): Promise { return this.rotationManager.addRotationScript(input) } - public async editRotationScript( - input: EditRotationScriptInput - ): Promise { + public async editRotationScript(input: EditRotationScriptInput): Promise { return this.rotationManager.editRotationScript(input) } - public async deleteRotationScript( - input: DeleteRotationScriptInput - ): Promise { + public async deleteRotationScript(input: DeleteRotationScriptInput): Promise { return this.rotationManager.deleteRotationScript(input) } } diff --git a/KeeperSdk/src/pam/rotation/RotationManager.ts b/KeeperSdk/src/pam/rotation/RotationManager.ts index fb056830..7a693423 100644 --- a/KeeperSdk/src/pam/rotation/RotationManager.ts +++ b/KeeperSdk/src/pam/rotation/RotationManager.ts @@ -10,11 +10,7 @@ import { renderRotationSchedulesAsciiTable, } from './listRotations' import { editRotation, validateRotationInput } from './editRotation' -import { - listRotationScripts, - formatRotationScriptsTable, - formatRotationScriptsJson, -} from './listRotationScripts' +import { listRotationScripts, formatRotationScriptsTable, formatRotationScriptsJson } from './listRotationScripts' import { addRotationScript } from './addRotationScript' import { editRotationScript } from './editRotationScript' import { deleteRotationScript } from './deleteRotationScript' @@ -59,7 +55,9 @@ export class RotationManager { return auth } - public async listRotationSchedules(options: ListRotationSchedulesOptions = {}): Promise { + public async listRotationSchedules( + options: ListRotationSchedulesOptions = {} + ): Promise { return listRotationSchedules(this.requireAuth(), this.storage, options) } @@ -119,9 +117,7 @@ export class RotationManager { /* ========== Rotation Script Operations ========== */ - public async listRotationScripts( - options: ListRotationScriptsOptions = {} - ): Promise { + public async listRotationScripts(options: ListRotationScriptsOptions = {}): Promise { return listRotationScripts(this.requireAuth(), this.storage, options) } @@ -133,21 +129,15 @@ export class RotationManager { return formatRotationScriptsJson(result) } - public async addRotationScript( - input: AddRotationScriptInput - ): Promise { + public async addRotationScript(input: AddRotationScriptInput): Promise { return addRotationScript(this.requireAuth(), this.storage, input) } - public async editRotationScript( - input: EditRotationScriptInput - ): Promise { + public async editRotationScript(input: EditRotationScriptInput): Promise { return editRotationScript(this.requireAuth(), this.storage, input) } - public async deleteRotationScript( - input: DeleteRotationScriptInput - ): Promise { + public async deleteRotationScript(input: DeleteRotationScriptInput): Promise { return deleteRotationScript(this.requireAuth(), this.storage, input) } } diff --git a/KeeperSdk/src/pam/rotation/addRotationScript.ts b/KeeperSdk/src/pam/rotation/addRotationScript.ts index 663d77be..c772cea9 100644 --- a/KeeperSdk/src/pam/rotation/addRotationScript.ts +++ b/KeeperSdk/src/pam/rotation/addRotationScript.ts @@ -24,10 +24,7 @@ export async function addRotationScript( try { const scriptPath = input.scriptPath?.trim() if (!scriptPath) { - throw new KeeperSdkError( - 'Script file path is required', - ResultCodes.INVALID_PATTERN - ) + throw new KeeperSdkError('Script file path is required', ResultCodes.INVALID_PATTERN) } const expandedPath = validateScriptFileExists(scriptPath) diff --git a/KeeperSdk/src/pam/rotation/editRotation.ts b/KeeperSdk/src/pam/rotation/editRotation.ts index bcae04b1..41890ba7 100644 --- a/KeeperSdk/src/pam/rotation/editRotation.ts +++ b/KeeperSdk/src/pam/rotation/editRotation.ts @@ -10,11 +10,7 @@ import { RotationProfile, ScheduleData, } from './rotationTypes' -import { - getVaultRecord, - recordExistsInVault, - getVaultRecordTitleType, -} from './rotationHelpers' +import { getVaultRecord, recordExistsInVault, getVaultRecordTitleType } from './rotationHelpers' import { RECORD_ROTATION_KIND } from './rotationConstants' const DEFAULT_PAM_SPECIAL_CHAR = '!@#$%^&*()_+-=[]{}|;:,.<>?' @@ -54,15 +50,12 @@ export async function editRotation( const skippedRecords: EditRotationResult['skippedRecords'] = [] try { - const currentRotation = storage.getByUid( - RECORD_ROTATION_KIND, - recordUid - ) + const currentRotation = storage.getByUid(RECORD_ROTATION_KIND, recordUid) if (!currentRotation && !input.configUid && !input.iamAadConfigUid && !input.saasConfigUid) { throw new KeeperSdkError( `Record "${recordUid}" does not have rotation configured yet. ` + - `You must provide a PAM Configuration UID (--config) to configure rotation for this record.`, + `You must provide a PAM Configuration UID (--config) to configure rotation for this record.`, ResultCodes.PAM_ROTATION_RECORD_REQUIRED ) } @@ -75,7 +68,7 @@ export async function editRotation( if (!configUid && !currentRotation) { throw new KeeperSdkError( `PAM Configuration UID is required when setting up rotation for the first time. ` + - `Provide it with --config, --iam-aad-config, or --saas-config.`, + `Provide it with --config, --iam-aad-config, or --saas-config.`, ResultCodes.PAM_ROTATION_RECORD_REQUIRED ) } @@ -93,9 +86,10 @@ export async function editRotation( let scheduleData = validateAndBuildScheduleData(input) if (!scheduleData && currentRotation?.schedule) { try { - scheduleData = typeof currentRotation.schedule === 'string' - ? JSON.parse(currentRotation.schedule) - : currentRotation.schedule + scheduleData = + typeof currentRotation.schedule === 'string' + ? JSON.parse(currentRotation.schedule) + : currentRotation.schedule } catch (e) { scheduleData = null } @@ -109,9 +103,10 @@ export async function editRotation( if (currentRotation) { if (currentRotation.schedule) { try { - currentSchedule = typeof currentRotation.schedule === 'string' - ? currentRotation.schedule - : JSON.stringify(currentRotation.schedule) + currentSchedule = + typeof currentRotation.schedule === 'string' + ? currentRotation.schedule + : JSON.stringify(currentRotation.schedule) } catch (e) { currentSchedule = '' } @@ -151,11 +146,7 @@ export async function editRotation( let passwordComplexityEncrypted = currentComplexity if (input.passwordComplexity) { - const encrypted = await encryptPasswordComplexity( - storage, - recordUid, - input.passwordComplexity - ) + const encrypted = await encryptPasswordComplexity(storage, recordUid, input.passwordComplexity) if (encrypted) { passwordComplexityEncrypted = platform.base64ToBytes(encrypted) } @@ -164,9 +155,7 @@ export async function editRotation( let finalScheduleData = scheduleData if (!finalScheduleData && currentSchedule) { try { - finalScheduleData = typeof currentSchedule === 'string' - ? JSON.parse(currentSchedule) - : currentSchedule + finalScheduleData = typeof currentSchedule === 'string' ? JSON.parse(currentSchedule) : currentSchedule } catch (e) { finalScheduleData = null } @@ -295,10 +284,7 @@ async function encryptPasswordComplexity( } const plainText = JSON.stringify(complexityData) - const encrypted = await platform.aesGcmEncrypt( - platform.stringToBytes(plainText), - recordKey - ) + const encrypted = await platform.aesGcmEncrypt(platform.stringToBytes(plainText), recordKey) return platform.bytesToBase64(encrypted) } catch (err) { throw new KeeperSdkError( diff --git a/KeeperSdk/src/pam/rotation/editRotationScript.ts b/KeeperSdk/src/pam/rotation/editRotationScript.ts index 6815be73..835fa122 100644 --- a/KeeperSdk/src/pam/rotation/editRotationScript.ts +++ b/KeeperSdk/src/pam/rotation/editRotationScript.ts @@ -13,7 +13,6 @@ import { } from './rotationScriptHelpers' import type { PamRecordData } from './rotationScriptTypes' - export async function editRotationScript( auth: Auth, storage: InMemoryStorage, @@ -24,10 +23,7 @@ export async function editRotationScript( try { const scriptName = input.script?.trim() if (!scriptName) { - throw new KeeperSdkError( - 'Script UID or name is required', - ResultCodes.INVALID_PATTERN - ) + throw new KeeperSdkError('Script UID or name is required', ResultCodes.INVALID_PATTERN) } const record = getSinglePamRecord(storage, input.record) diff --git a/KeeperSdk/src/pam/rotation/getRotationInfo.ts b/KeeperSdk/src/pam/rotation/getRotationInfo.ts index eaf19f34..9d7ee283 100644 --- a/KeeperSdk/src/pam/rotation/getRotationInfo.ts +++ b/KeeperSdk/src/pam/rotation/getRotationInfo.ts @@ -1,9 +1,5 @@ import type { Auth } from '@keeper-security/keeperapi' -import { - getRotationInfoMessage, - normal64Bytes, - pamGetRotationSchedulesMessage, -} from '@keeper-security/keeperapi' +import { getRotationInfoMessage, normal64Bytes, pamGetRotationSchedulesMessage } from '@keeper-security/keeperapi' import type { InMemoryStorage } from '../../storage/InMemoryStorage' import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../../utils' import { toFiniteNumber, webSafeUidFromBytes } from '../gateway/gatewayHelpers' @@ -69,9 +65,7 @@ export async function getRotationInfo( const pamConfigUid = webSafeUidFromBytes(rotationInfo.configurationUid) const gatewayUid = webSafeUidFromBytes(rotationInfo.controllerUid) || MISSING_VALUE_LABEL - const adminResourceUid = rotationInfo.resourceUid?.length - ? webSafeUidFromBytes(rotationInfo.resourceUid) - : null + const adminResourceUid = rotationInfo.resourceUid?.length ? webSafeUidFromBytes(rotationInfo.resourceUid) : null const passwordComplexity = rotationInfo.pwdComplexity || null const passwordComplexityDetail = await decryptPasswordComplexity(storage, recordUid, passwordComplexity) @@ -141,9 +135,7 @@ export function formatRotationInfoJson(result: RotationInfoResult): string { function formatPasswordComplexityData(detail: NonNullable): string { const symbolsChars = - detail.specialChars != null && String(detail.specialChars).trim() !== '' - ? String(detail.specialChars) - : 'None' + detail.specialChars != null && String(detail.specialChars).trim() !== '' ? String(detail.specialChars) : 'None' return [ `Length: ${detail.length ?? ''}`, `Lowercase: ${detail.lowercase ?? ''}`, @@ -175,10 +167,7 @@ function formatRotationInfoDetail(result: RotationInfoResult): string { if (result.readyToRotate) { rows.push(['PAM Config UID', result.pamConfigUid || '']) if (result.nodeId != null) rows.push(['Node ID', String(result.nodeId)]) - rows.push([ - 'Gateway Name where the rotation will be performed', - result.gatewayName || MISSING_VALUE_LABEL, - ]) + rows.push(['Gateway Name where the rotation will be performed', result.gatewayName || MISSING_VALUE_LABEL]) rows.push(['Gateway Uid', result.gatewayUid || MISSING_VALUE_LABEL]) if (result.adminResourceUid) rows.push(['Admin Resource Uid', result.adminResourceUid]) if (result.passwordComplexity) rows.push(['Password Complexity', result.passwordComplexity]) diff --git a/KeeperSdk/src/pam/rotation/index.ts b/KeeperSdk/src/pam/rotation/index.ts index fe124158..cdecce32 100644 --- a/KeeperSdk/src/pam/rotation/index.ts +++ b/KeeperSdk/src/pam/rotation/index.ts @@ -13,11 +13,7 @@ export { getRotationInfo, formatRotationInfoJson, formatRotationInfoOutput } fro export { editRotation, validateRotationInput } from './editRotation' -export { - listRotationScripts, - formatRotationScriptsTable, - formatRotationScriptsJson, -} from './listRotationScripts' +export { listRotationScripts, formatRotationScriptsTable, formatRotationScriptsJson } from './listRotationScripts' export { addRotationScript } from './addRotationScript' export { editRotationScript } from './editRotationScript' diff --git a/KeeperSdk/src/pam/rotation/listRotationScripts.ts b/KeeperSdk/src/pam/rotation/listRotationScripts.ts index 75239331..11bcef5d 100644 --- a/KeeperSdk/src/pam/rotation/listRotationScripts.ts +++ b/KeeperSdk/src/pam/rotation/listRotationScripts.ts @@ -79,15 +79,7 @@ export function formatRotationScriptsTable(result: ListRotationScriptsResult): s return [] } - const headers = [ - 'Record UID', - 'Record Title', - 'Record Type', - 'Script UID', - 'Script Name', - 'Credentials', - 'Command', - ] + const headers = ['Record UID', 'Record Title', 'Record Type', 'Script UID', 'Script Name', 'Credentials', 'Command'] const rows = result.scripts.map((script) => [ script.recordUid, diff --git a/KeeperSdk/src/pam/rotation/listRotations.ts b/KeeperSdk/src/pam/rotation/listRotations.ts index 8e6f5735..98f0dc22 100644 --- a/KeeperSdk/src/pam/rotation/listRotations.ts +++ b/KeeperSdk/src/pam/rotation/listRotations.ts @@ -2,10 +2,7 @@ import type { Auth } from '@keeper-security/keeperapi' import { pamGetOnlineControllersMessage, pamGetRotationSchedulesMessage } from '@keeper-security/keeperapi' import type { InMemoryStorage } from '../../storage/InMemoryStorage' import { extractErrorMessage, KeeperSdkError, ResultCodes } from '../../utils' -import { - fetchEnterprisePamControllers, - webSafeUidFromBytes, -} from '../gateway/gatewayHelpers' +import { fetchEnterprisePamControllers, webSafeUidFromBytes } from '../gateway/gatewayHelpers' import { EMPTY_ROTATION_SCHEDULES_MESSAGE, PAM_USER_RECORD_TYPE, @@ -142,7 +139,10 @@ export function renderRotationSchedulesAsciiTable( }) const formatRow = (cells: string[]): string => - cells.map((cell, i) => (cell || '').padEnd(widths[i])).join(' ').trimEnd() + cells + .map((cell, i) => (cell || '').padEnd(widths[i])) + .join(' ') + .trimEnd() return [ formatRow([...table.headers]), @@ -190,7 +190,5 @@ export function formatRotationSchedulesOutput( if (result.message && result.rotations.length === 0) return result.message - return renderRotationSchedulesAsciiTable( - formatRotationSchedulesTable(result, { verbose: options.verbose }) - ) + return renderRotationSchedulesAsciiTable(formatRotationSchedulesTable(result, { verbose: options.verbose })) } diff --git a/KeeperSdk/src/pam/rotation/rotationConstants.ts b/KeeperSdk/src/pam/rotation/rotationConstants.ts index d3e18cdc..1be40811 100644 --- a/KeeperSdk/src/pam/rotation/rotationConstants.ts +++ b/KeeperSdk/src/pam/rotation/rotationConstants.ts @@ -9,12 +9,7 @@ export const RECORD_UNTITLED_LABEL = '[Untitled]' as const export const PAM_USER_RECORD_TYPE = 'pamUser' as const export const RECORD_ROTATION_KIND = 'non_shared_data' as const export const EMPTY_ROTATION_SCHEDULES_MESSAGE = 'No rotation schedules found' as const -export const ROTATION_LIST_DEFAULT_HEADERS = [ - 'Record', - 'Status', - 'Schedule', - 'Last', -] as const +export const ROTATION_LIST_DEFAULT_HEADERS = ['Record', 'Status', 'Schedule', 'Last'] as const export const ROTATION_LIST_VERBOSE_HEADERS = [ 'Record UID', 'Record Title', diff --git a/KeeperSdk/src/pam/rotation/rotationHelpers.ts b/KeeperSdk/src/pam/rotation/rotationHelpers.ts index 90854476..1f046a11 100644 --- a/KeeperSdk/src/pam/rotation/rotationHelpers.ts +++ b/KeeperSdk/src/pam/rotation/rotationHelpers.ts @@ -23,9 +23,7 @@ import type { PasswordComplexityDetail, RotationScheduleType } from './rotationT export function getVaultRecord(storage: InMemoryStorage, recordUid: string): DRecord | undefined { if (!recordUid) return undefined - return ( - storage.getByUid(VaultObjectKind.Record, recordUid) || getKeeperDriveRecord(storage, recordUid) - ) + return storage.getByUid(VaultObjectKind.Record, recordUid) || getKeeperDriveRecord(storage, recordUid) } export function recordExistsInVault(storage: InMemoryStorage, recordUid: string): boolean { @@ -38,8 +36,7 @@ export function getVaultRecordTitleType(storage: InMemoryStorage, recordUid: str const title = getRecordTitle(record) const type = getRecordType(record) - const displayTitle = - title && title !== '(untitled)' && title !== '(no data)' ? title : RECORD_UNTITLED_LABEL + const displayTitle = title && title !== '(untitled)' && title !== '(no data)' ? title : RECORD_UNTITLED_LABEL return [displayTitle, type || RECORD_UNKNOWN_TYPE_LABEL] } @@ -88,11 +85,7 @@ export async function decryptPasswordComplexity( } } -export function isAdminResourceValid( - storage: InMemoryStorage, - resourceUid: string, - configurationUid: string -): boolean { +export function isAdminResourceValid(storage: InMemoryStorage, resourceUid: string, configurationUid: string): boolean { if (!resourceUid || !configurationUid) return false if (!recordExistsInVault(storage, resourceUid)) return false const configuration = getVaultRecord(storage, configurationUid) diff --git a/KeeperSdk/src/pam/rotation/rotationScriptHelpers.ts b/KeeperSdk/src/pam/rotation/rotationScriptHelpers.ts index be7b5b93..35af4bd4 100644 --- a/KeeperSdk/src/pam/rotation/rotationScriptHelpers.ts +++ b/KeeperSdk/src/pam/rotation/rotationScriptHelpers.ts @@ -7,12 +7,7 @@ import { getRecordTitle, getRecordType } from '../../records/RecordUtils' import { updateRecord } from '../../records/RecordOperations' import { KeeperSdkError, ResultCodes } from '../../utils' import { SCRIPT_FIELD_TYPE } from './rotationConstants' -import type { - PamRecordData, - RotationScriptValue, - ScriptFieldLocation, - ScriptSearchResult, -} from './rotationScriptTypes' +import type { PamRecordData, RotationScriptValue, ScriptFieldLocation, ScriptSearchResult } from './rotationScriptTypes' export function findPamRecordsByName(storage: InMemoryStorage, searchText: string): DRecord[] { const results: DRecord[] = [] @@ -52,10 +47,7 @@ export function getSinglePamRecord(storage: InMemoryStorage, recordName: string) const records = findPamRecordsByName(storage, recordNameTrimmed) if (records.length === 0) { - throw new KeeperSdkError( - `PAM record not found: ${recordNameTrimmed}`, - ResultCodes.PAM_CONFIG_NOT_FOUND - ) + throw new KeeperSdkError(`PAM record not found: ${recordNameTrimmed}`, ResultCodes.PAM_CONFIG_NOT_FOUND) } if (records.length > 1) { @@ -123,10 +115,7 @@ export function findScriptByUidOrName( const searchLower = searchText.toLowerCase() for (const location of scripts) { - const scriptFile = storage.getByUid( - VaultObjectKind.Record, - location.script.fileRef - ) + const scriptFile = storage.getByUid(VaultObjectKind.Record, location.script.fileRef) if (scriptFile) { const title = (getRecordTitle(scriptFile) || '').toLowerCase() if (title.includes(searchLower)) { @@ -139,18 +128,13 @@ export function findScriptByUidOrName( } export function expandFilePath(filePath: string): string { - return filePath.startsWith('~') - ? path.join(process.env.HOME || '', filePath.slice(1)) - : filePath + return filePath.startsWith('~') ? path.join(process.env.HOME || '', filePath.slice(1)) : filePath } export function validateScriptFileExists(filePath: string): string { const expandedPath = expandFilePath(filePath) if (!fs.existsSync(expandedPath)) { - throw new KeeperSdkError( - `Script file not found: ${filePath}`, - ResultCodes.PAM_CONFIG_CREATE_FAILED - ) + throw new KeeperSdkError(`Script file not found: ${filePath}`, ResultCodes.PAM_CONFIG_CREATE_FAILED) } return expandedPath } diff --git a/KeeperSdk/src/pam/rotation/rotationScriptTypes.ts b/KeeperSdk/src/pam/rotation/rotationScriptTypes.ts index 4ac72e73..58aa21d2 100644 --- a/KeeperSdk/src/pam/rotation/rotationScriptTypes.ts +++ b/KeeperSdk/src/pam/rotation/rotationScriptTypes.ts @@ -80,7 +80,6 @@ export interface AddRotationScriptResult { warnings: string[] } - export interface EditRotationScriptInput { record: string script: string diff --git a/KeeperSdk/src/vault/KeeperVault.ts b/KeeperSdk/src/vault/KeeperVault.ts index 09d9a551..f1a2f728 100644 --- a/KeeperSdk/src/vault/KeeperVault.ts +++ b/KeeperSdk/src/vault/KeeperVault.ts @@ -1432,9 +1432,7 @@ export class KeeperVault { return this.pamManager.formatPamConfigurationsOutput(result, options ?? {}) } - public async listRotationSchedules( - options?: ListRotationSchedulesOptions - ): Promise { + public async listRotationSchedules(options?: ListRotationSchedulesOptions): Promise { return this.pamManager.listRotationSchedules(options ?? {}) } @@ -1485,9 +1483,7 @@ export class KeeperVault { return this.pamManager.editRotation(input) } - public async listRotationScripts( - options?: ListRotationScriptsOptions - ): Promise { + public async listRotationScripts(options?: ListRotationScriptsOptions): Promise { return this.pamManager.listRotationScripts(options ?? {}) } @@ -1507,9 +1503,7 @@ export class KeeperVault { return this.pamManager.editRotationScript(input) } - public async deleteRotationScript( - input: DeleteRotationScriptInput - ): Promise { + public async deleteRotationScript(input: DeleteRotationScriptInput): Promise { return this.pamManager.deleteRotationScript(input) } From 3370a092c384a80e9dc93f13de7f74d5f70cfff6 Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Thu, 27 Aug 2026 15:45:09 +0530 Subject: [PATCH 3/3] keeperApi format fix --- keeperapi/src/restMessages.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/keeperapi/src/restMessages.ts b/keeperapi/src/restMessages.ts index df777bd2..c976e039 100644 --- a/keeperapi/src/restMessages.ts +++ b/keeperapi/src/restMessages.ts @@ -1039,12 +1039,7 @@ export const sendControllerMessage = ( export const pamGetRotationSchedulesMessage = ( data: PAM.IPAMGenericUidsRequest = {} ): RestMessage => - createMessage( - data, - 'api/user/get_rotation_schedules', - PAM.PAMGenericUidsRequest, - PAM.PAMRotationSchedulesResponse - ) + createMessage(data, 'api/user/get_rotation_schedules', PAM.PAMGenericUidsRequest, PAM.PAMRotationSchedulesResponse) export const getRotationInfoMessage = ( data: PAM.IPAMGenericUidRequest