From cc79cfbc9ad6ebb36b2e043a2e210afa12f7f34b Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Thu, 10 Sep 2026 18:20:16 +0530 Subject: [PATCH 1/2] pam rotation example --- .../src/nestedShareFolders/nsfHelpers.ts | 14 +- examples/sdk_example/package.json | 8 + .../src/pam/rotation/add_script.ts | 65 +++++ .../src/pam/rotation/delete_script.ts | 58 +++++ .../src/pam/rotation/edit_rotation.ts | 245 ++++++++++++++++++ .../src/pam/rotation/edit_script.ts | 70 +++++ .../src/pam/rotation/list_rotations.ts | 55 ++++ .../src/pam/rotation/list_scripts.ts | 78 ++++++ .../src/pam/rotation/rotation_info.ts | 49 ++++ 9 files changed, 638 insertions(+), 4 deletions(-) create mode 100644 examples/sdk_example/src/pam/rotation/add_script.ts create mode 100644 examples/sdk_example/src/pam/rotation/delete_script.ts create mode 100644 examples/sdk_example/src/pam/rotation/edit_rotation.ts create mode 100644 examples/sdk_example/src/pam/rotation/edit_script.ts create mode 100644 examples/sdk_example/src/pam/rotation/list_rotations.ts create mode 100644 examples/sdk_example/src/pam/rotation/list_scripts.ts create mode 100644 examples/sdk_example/src/pam/rotation/rotation_info.ts diff --git a/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts b/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts index 54d0c8d1..bd229a09 100644 --- a/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts +++ b/KeeperSdk/src/nestedShareFolders/nsfHelpers.ts @@ -876,10 +876,16 @@ export async function fetchLiveRecordAccessEntries( }[] > { try { - const [response, shareUsers] = await Promise.all([ - auth.executeRest(getRecordAccessMessage({ recordUids: [normal64Bytes(recordUid)] })), - loadShareUserMap(auth, storage), - ]) + let response: record.v3.details.IRecordAccessResponse + try { + response = await auth.executeRest(getRecordAccessMessage({ recordUids: [normal64Bytes(recordUid)] })) + } catch (restErr) { + if (restErr instanceof Error && restErr.message.includes('decrypt')) { + return [] + } + throw restErr + } + const shareUsers = await loadShareUserMap(auth, storage) return (response.recordAccesses ?? []) .filter((entry) => { diff --git a/examples/sdk_example/package.json b/examples/sdk_example/package.json index 05cfc408..aba405bf 100644 --- a/examples/sdk_example/package.json +++ b/examples/sdk_example/package.json @@ -78,6 +78,14 @@ "pam:config:new": "ts-node src/pam/config/create_config.ts", "pam:config:edit": "ts-node src/pam/config/edit_config.ts", "pam:config:remove": "ts-node src/pam/config/remove_config.ts", + "pam:rotation:list": "ts-node src/pam/rotation/list_rotations.ts", + "pam:rotation:info": "ts-node src/pam/rotation/rotation_info.ts", + "pam:rotation:edit": "ts-node src/pam/rotation/edit_rotation.ts", + "pam:rotation:delete": "ts-node src/pam/rotation/delete_rotation.ts", + "pam:rotation:list-script": "ts-node src/pam/rotation/list_scripts.ts", + "pam:rotation:add-script": "ts-node src/pam/rotation/add_script.ts", + "pam:rotation:edit-script": "ts-node src/pam/rotation/edit_script.ts", + "pam:rotation:delete-script": "ts-node src/pam/rotation/delete_script.ts", "link-local": "cd ../../KeeperSdk && npm link ../keeperapi && cd ../examples/sdk_example && npm link ../../keeperapi", "types": "tsc --watch", "types:ci": "tsc" diff --git a/examples/sdk_example/src/pam/rotation/add_script.ts b/examples/sdk_example/src/pam/rotation/add_script.ts new file mode 100644 index 00000000..da19b107 --- /dev/null +++ b/examples/sdk_example/src/pam/rotation/add_script.ts @@ -0,0 +1,65 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' + +async function addRotationScriptExample() { + const vault = await login() + + try { + const record = (await prompt('Target PAM record UID or name: ')).trim() + if (!record) { + logger.error('Record is required.') + return + } + + const scriptPath = (await prompt('Script file path: ')).trim() + if (!scriptPath) { + logger.error('Script path is required.') + return + } + + const scriptCommand = (await prompt('Script command (optional): ')).trim() || undefined + + const credentialStr = (await prompt('Credential UIDs (comma-separated, optional): ')).trim() + const credentialUids = credentialStr + ? credentialStr.split(',').map((s) => s.trim()) + : undefined + + logger.info('\nAdding rotation script...\n') + + const result = await vault.addRotationScript({ + record, + scriptPath, + scriptCommand, + credentialUids, + }) + + if (!result.success) { + logger.error(`Error: ${result.message}`) + process.exitCode = 1 + return + } + + logger.info(`✓ ${result.message}`) + logger.info(`Script File UID: ${result.scriptFileUid}`) + + if (result.warnings.length > 0) { + logger.warn('\nWarnings:') + result.warnings.forEach((w) => logger.warn(` - ${w}`)) + } + + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(addRotationScriptExample) diff --git a/examples/sdk_example/src/pam/rotation/delete_script.ts b/examples/sdk_example/src/pam/rotation/delete_script.ts new file mode 100644 index 00000000..3a0a4f90 --- /dev/null +++ b/examples/sdk_example/src/pam/rotation/delete_script.ts @@ -0,0 +1,58 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, +} from '@keeper-security/keeper-sdk-javascript' +import { isYes } from '../../utils/promptCommands' +import { runExample } from '../../utils/runner' + +async function deleteRotationScriptExample() { + const vault = await login() + + try { + const record = (await prompt('Target PAM record UID or name: ')).trim() + if (!record) { + logger.error('Record is required.') + return + } + + const script = (await prompt('Script UID or name (optional - leaves empty to delete first script): ')).trim() + + const confirm = await prompt('Are you sure you want to delete this script? (yes/no): ') + if (!isYes(confirm)) { + logger.info('Operation cancelled.') + return + } + + logger.info('\nDeleting rotation script...\n') + + const result = await vault.deleteRotationScript({ + record, + script, + }) + + if (!result.success) { + logger.error(`Error: ${result.message}`) + process.exitCode = 1 + return + } + + logger.info(`✓ ${result.message}`) + + if (result.warnings.length > 0) { + logger.warn('\nWarnings:') + result.warnings.forEach((w) => logger.warn(` - ${w}`)) + } + + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(deleteRotationScriptExample) diff --git a/examples/sdk_example/src/pam/rotation/edit_rotation.ts b/examples/sdk_example/src/pam/rotation/edit_rotation.ts new file mode 100644 index 00000000..b3fc1646 --- /dev/null +++ b/examples/sdk_example/src/pam/rotation/edit_rotation.ts @@ -0,0 +1,245 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, + RotationProfile, + PasswordComplexityInput, + ScheduleData, + suppressLogs, +} from "@keeper-security/keeper-sdk-javascript"; +import { runExample } from "../../utils/runner"; +import { isYes } from "../../utils/format"; + +async function editRotationExample() { + const vault = await login(); + + try { + // Get record UID or NSF record title + const recordUidInput = await prompt( + "Record UID or NSF record title to edit (-r): ", + ); + const recordUid = recordUidInput.trim(); + if (!recordUid) { + logger.info("Record UID or NSF record title is required."); + process.exitCode = 1; + return; + } + + // Get optional parameters + const configUidInput = await prompt( + "PAM Configuration UID or NSF record title (-c): ", + ); + const configUid = configUidInput.trim() || undefined; + + const resourceUidInput = await prompt( + "PAM Resource UID or NSF record title (-rs): ", + ); + const resourceUid = resourceUidInput.trim() || undefined; + + const iamAadConfigUidInput = await prompt( + "IAM/AAD Configuration UID (-iac): ", + ); + const iamAadConfigUid = iamAadConfigUidInput.trim() || undefined; + + const saasConfigUidInput = await prompt("SaaS Configuration UID: "); + const saasConfigUid = saasConfigUidInput.trim() || undefined; + + const adminUserUidInput = await prompt("Admin User UID (-a): "); + const adminUserUid = adminUserUidInput.trim() || undefined; + + // Get rotation profile + let rotationProfile: RotationProfile | undefined; + const profileInputRaw = await prompt( + "Rotation Profile [general|iam_user|scripts_only|saas] (optional): ", + ); + const profileInput = profileInputRaw.trim().toLowerCase(); + if ( + profileInput && + ["general", "iam_user", "scripts_only", "saas"].includes(profileInput) + ) { + rotationProfile = profileInput as RotationProfile; + } + + // Get schedule options + const scheduleOptionsRaw = await prompt( + "Schedule type [on-demand|json|cron|config] (optional): ", + ); + const scheduleOptions = scheduleOptionsRaw.trim().toLowerCase(); + + let onDemand = false; + let scheduleJson: ScheduleData[] | undefined; + let scheduleCron: string | undefined; + let scheduleConfig = false; + + if (scheduleOptions === "on-demand") { + onDemand = true; + } else if (scheduleOptions === "json") { + const jsonStrInput = await prompt("Schedule JSON: "); + const jsonStr = jsonStrInput.trim(); + if (jsonStr) { + try { + const parsed = JSON.parse(jsonStr); + // Wrap single object in array if needed + scheduleJson = Array.isArray(parsed) ? parsed : [parsed]; + } catch (e) { + logger.error("Invalid schedule JSON."); + process.exitCode = 1; + return; + } + } + } else if (scheduleOptions === "cron") { + const cronInput = await prompt( + "Cron expression (6 fields, e.g. 0 0 4 * * ?): ", + ); + scheduleCron = cronInput.trim() || undefined; + } else if (scheduleOptions === "config") { + scheduleConfig = true; + } + + // Get password complexity + let passwordComplexity: PasswordComplexityInput | undefined; + const setPwdInput = await prompt("Set password complexity? [y/N]: "); + if (isYes(setPwdInput)) { + const lengthInput = await prompt("Password length: "); + const length = parseInt(lengthInput) || 32; + + const capsInput = await prompt("Capital letters: "); + const caps = parseInt(capsInput) || 5; + + const lowercaseInput = await prompt("Lowercase letters: "); + const lowercase = parseInt(lowercaseInput) || 5; + + const digitsInput = await prompt("Digits: "); + const digits = parseInt(digitsInput) || 5; + + const specialInput = await prompt("Special characters: "); + const special = parseInt(specialInput) || 5; + + const specialCharsInput = await prompt( + "Special characters set (optional): ", + ); + const specialChars = specialCharsInput.trim() || undefined; + + passwordComplexity = { + length, + caps, + lowercase, + digits, + special, + specialChars, + }; + } + + // Get enable/disable state + const enableInput = await prompt("Enable rotation? [y/N]: "); + const enable = isYes(enableInput); + + const disableInput = await prompt("Disable rotation? [y/N]: "); + const disable = !enable && isYes(disableInput); + + const scheduleOnlyInput = await prompt("Update schedule only? [y/N]: "); + const scheduleOnly = isYes(scheduleOnlyInput); + + const forceInput = await prompt("Skip confirmation? (-f/--force) [y/N]: "); + const force = isYes(forceInput); + + const input = { + recordUid, + configUid, + resourceUid, + iamAadConfigUid, + saasConfigUid, + adminUserUid, + rotationProfile, + onDemand, + scheduleJson, + scheduleCron, + scheduleConfig, + passwordComplexity, + enable, + disable, + scheduleOnly, + force, + }; + + // Show confirmation before updating (unless force flag is set) + if (!force) { + logger.info(""); + logger.info("=== The following record will be updated ==="); + logger.info(` Record UID: ${recordUid}`); + if (configUid) logger.info(` Config UID: ${configUid}`); + if (resourceUid) logger.info(` Resource UID: ${resourceUid}`); + if (input.scheduleJson || input.scheduleCron || input.onDemand) { + logger.info(` Schedule: ${input.onDemand ? "On-Demand" : "Custom"}`); + } + if (passwordComplexity) { + logger.info(` Complexity: ${JSON.stringify(passwordComplexity)}`); + } + if (enable || disable) { + logger.info(` Enabled: ${enable === true}`); + } + logger.info(""); + + // Ask for confirmation + const confirmInput = await prompt( + "Do you want to update rotation? [Y/n]: ", + ); + if (confirmInput.trim().toLowerCase().startsWith("n")) { + logger.info("Update cancelled."); + process.exitCode = 0; + return; + } + } + + let result; + const restore = suppressLogs(); + try { + result = await vault.editRotation(input); + } finally { + restore(); + } + + logger.info(""); + logger.info( + `Operation Result: ${result.successful ? "SUCCESS" : "FAILED"}`, + ); + logger.info(`Updated ${result.validRecords.length} record(s)`); + if (result.skippedRecords.length > 0) { + logger.info(`Skipped ${result.skippedRecords.length} record(s)`); + } + logger.info(result.message || ""); + logger.info(""); + + if (result.validRecords.length > 0) { + logger.info("Updated Records:"); + result.validRecords.forEach((record) => { + logger.info(` - ${record.recordTitle} (${record.recordUid})`); + logger.info(` Enabled: ${record.enabled}`); + logger.info(` Config UID: ${record.configUid || "N/A"}`); + logger.info(` Resource UID: ${record.resourceUid || "N/A"}`); + logger.info(` Schedule: ${record.schedule}`); + if (record.complexity) + logger.info(` Complexity: ${record.complexity}`); + }); + } + + if (result.skippedRecords.length > 0) { + logger.info(""); + logger.info("Skipped Records:"); + result.skippedRecords.forEach((record) => { + logger.info(` - ${record.recordTitle} (${record.recordUid})`); + logger.info(` Problem: ${record.problem}`); + logger.info(` Description: ${record.description}`); + }); + } + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`); + process.exitCode = 1; + } finally { + cleanup(vault); + } +} + +runExample(editRotationExample); diff --git a/examples/sdk_example/src/pam/rotation/edit_script.ts b/examples/sdk_example/src/pam/rotation/edit_script.ts new file mode 100644 index 00000000..031c100e --- /dev/null +++ b/examples/sdk_example/src/pam/rotation/edit_script.ts @@ -0,0 +1,70 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' + +async function editRotationScriptExample() { + const vault = await login() + + try { + const record = (await prompt('Target PAM record UID or name: ')).trim() + if (!record) { + logger.error('Record is required.') + return + } + + const script = (await prompt('Script UID or name: ')).trim() + if (!script) { + logger.error('Script is required.') + return + } + + const scriptCommand = (await prompt('New script command (optional): ')).trim() || undefined + + const addCredStr = (await prompt('Credentials to add (comma-separated, optional): ')).trim() + const addCredentials = addCredStr + ? addCredStr.split(',').map((s) => s.trim()) + : undefined + + const removeCredStr = (await prompt('Credentials to remove (comma-separated, optional): ')).trim() + const removeCredentials = removeCredStr + ? removeCredStr.split(',').map((s) => s.trim()) + : undefined + + logger.info('\nEditing rotation script...\n') + + const result = await vault.editRotationScript({ + record, + script, + scriptCommand, + addCredentials, + removeCredentials, + }) + + if (!result.success) { + logger.error(`Error: ${result.message}`) + process.exitCode = 1 + return + } + + logger.info(`✓ ${result.message}`) + + if (result.warnings.length > 0) { + logger.warn('\nWarnings:') + result.warnings.forEach((w) => logger.warn(` - ${w}`)) + } + + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(editRotationScriptExample) diff --git a/examples/sdk_example/src/pam/rotation/list_rotations.ts b/examples/sdk_example/src/pam/rotation/list_rotations.ts new file mode 100644 index 00000000..699d20f1 --- /dev/null +++ b/examples/sdk_example/src/pam/rotation/list_rotations.ts @@ -0,0 +1,55 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, + RotationListFormat, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' +import { isYes } from '../../utils/format' + +async function listRotationSchedulesExample() { + const vault = await login() + + try { + const verbose = isYes(await prompt('Verbose output? [y/N]: ')) + const asJson = isYes(await prompt('Output as JSON? [y/N]: ')) + + const options = { + verbose, + format: asJson ? RotationListFormat.Json : RotationListFormat.Table, + } + + let result + const restore = suppressLogs() + try { + result = await vault.listRotationSchedules(options) + } finally { + restore() + } + + if (result.rotations.length === 0) { + logger.info(result.message || 'No PAM User rotation schedules found.') + return + } + + const rotations = [...result.rotations].sort((a, b) => { + const aMissingConfig = a.pamConfigDisplay === '[No Config Found]' ? 1 : 0 + const bMissingConfig = b.pamConfigDisplay === '[No Config Found]' ? 1 : 0 + return aMissingConfig - bMissingConfig || a.recordTitle.localeCompare(b.recordTitle) + }) + + logger.info('') + logger.info(vault.formatRotationSchedulesOutput({ ...result, rotations }, options)) + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(listRotationSchedulesExample) diff --git a/examples/sdk_example/src/pam/rotation/list_scripts.ts b/examples/sdk_example/src/pam/rotation/list_scripts.ts new file mode 100644 index 00000000..16a2e714 --- /dev/null +++ b/examples/sdk_example/src/pam/rotation/list_scripts.ts @@ -0,0 +1,78 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' + +async function listRotationScriptsExample() { + const vault = await login() + + try { + const pattern = (await prompt('Search pattern (optional): ')).trim() || undefined + + logger.info('\nFetching rotation scripts...\n') + + const result = await vault.listRotationScripts({ + pattern, + }) + + if (!result.success) { + logger.error(`Error: ${result.message}`) + process.exitCode = 1 + return + } + + if (result.scripts.length === 0) { + logger.info(result.message || 'No rotation scripts found.') + return + } + + // Format as table + const table = vault.formatRotationScriptsTable(result) + if (table.length > 0) { + const headers = table[0] + const rows = table.slice(1) + + console.log('\n' + formatTable(headers, rows) + '\n') + } + + logger.info(`Total: ${result.scripts.length} script(s)\n`) + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +/** + * Format table for console display (simple format without borders) + */ +function formatTable(headers: string[], rows: string[][]): string { + const colWidths = headers.map((h, i) => { + const maxRowWidth = Math.max(...rows.map((r) => (r[i] || '').length)) + return Math.max(h.length, maxRowWidth) + }) + + const headerRow = headers + .map((h, i) => h.padEnd(colWidths[i])) + .join(' ') + + const separator = colWidths.map((w) => '-'.repeat(w)).join(' ') + + const dataRows = rows + .map( + (row) => + row + .map((cell, i) => (cell || '').padEnd(colWidths[i])) + .join(' ') + ) + .join('\n') + + return [headerRow, separator, dataRows].join('\n') +} + +runExample(listRotationScriptsExample) diff --git a/examples/sdk_example/src/pam/rotation/rotation_info.ts b/examples/sdk_example/src/pam/rotation/rotation_info.ts new file mode 100644 index 00000000..163282b2 --- /dev/null +++ b/examples/sdk_example/src/pam/rotation/rotation_info.ts @@ -0,0 +1,49 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, + RotationListFormat, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' +import { isYes } from '../../utils/format' + +async function rotationInfoExample() { + const vault = await login() + + try { + const recordUid = (await prompt('Record UID (-r): ')).trim() + if (!recordUid) { + logger.info('Record UID is required.') + process.exitCode = 1 + return + } + + const asJson = isYes(await prompt('Output as JSON? [y/N]: ')) + const input = { + recordUid, + format: asJson ? RotationListFormat.Json : RotationListFormat.Table, + } + + let result + const restore = suppressLogs() + try { + result = await vault.getRotationInfo(input) + } finally { + restore() + } + + logger.info('') + logger.info(vault.formatRotationInfoOutput(result, input)) + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(rotationInfoExample) From fe65314162530c2a99e6843acec01299749d075d Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Thu, 10 Sep 2026 18:39:24 +0530 Subject: [PATCH 2/2] CodeQL error fix --- examples/sdk_example/src/pam/rotation/edit_rotation.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/sdk_example/src/pam/rotation/edit_rotation.ts b/examples/sdk_example/src/pam/rotation/edit_rotation.ts index b3fc1646..560aaaaa 100644 --- a/examples/sdk_example/src/pam/rotation/edit_rotation.ts +++ b/examples/sdk_example/src/pam/rotation/edit_rotation.ts @@ -175,7 +175,7 @@ async function editRotationExample() { logger.info(` Schedule: ${input.onDemand ? "On-Demand" : "Custom"}`); } if (passwordComplexity) { - logger.info(` Complexity: ${JSON.stringify(passwordComplexity)}`); + logger.info(" Complexity: configured"); } if (enable || disable) { logger.info(` Enabled: ${enable === true}`); @@ -220,8 +220,7 @@ async function editRotationExample() { logger.info(` Config UID: ${record.configUid || "N/A"}`); logger.info(` Resource UID: ${record.resourceUid || "N/A"}`); logger.info(` Schedule: ${record.schedule}`); - if (record.complexity) - logger.info(` Complexity: ${record.complexity}`); + if (record.complexity) logger.info(" Complexity: configured"); }); }