Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0e14601
Import Command - Match Shared Folder by uid (revised) (#2363)
lthievenaz-keeper Sep 10, 2026
1283fbd
KC-1435: Prevent delegated admin privilege escalation by validating u…
sshrushanth-ks Sep 10, 2026
6015f25
KC-935, KC-1123: Fix Service Mode background mode and tunneling on Wi…
amangalampalli-ks Sep 11, 2026
6da2c8c
KC-1456: Enforce allow_pam_gateway policy on pam gateway new/remove (…
sshrushanth-ks Sep 11, 2026
68d9bcb
DR-1368 add hashicorp pam config updated pr (#2357)
mfordkeeper Sep 14, 2026
6ee615c
Add Nested Shared Folder (NSF) support to CyberArk PAM import
pvagare-ks Sep 15, 2026
52f4d34
Add KeeperRDP Proxy support and generic --keeper-proxy tunnel edit op…
idimov-keeper Sep 15, 2026
4bb5917
allow renaming root node
IliaTheKeeper Sep 16, 2026
9bdfbf0
allow to use updated root node name as displayName
IliaTheKeeper Sep 16, 2026
3be455f
generate command: support AES key generation
sk-keeper Sep 15, 2026
27141e6
KC-1449: Mask sensitive record data in Docker logs during record-add …
sshrushanth-ks Sep 17, 2026
4899899
DR-1381 Add a toggle to prevent keeper-dag from deactivating similar …
jwalstra-keeper Sep 17, 2026
724a0ff
KC-1458: Restrict app-setup command lists to their own allowlist (#2368)
amangalampalli-ks Sep 18, 2026
c214042
KC-1453: Fix Gateway Name not displayed in pam rotation info output (…
sshrushanth-ks Sep 18, 2026
98d24ef
handle root node visibility flag correctly
IliaTheKeeper Sep 18, 2026
20b43e9
KC-1457: Block Service Mode from ever accessing its own config record…
amangalampalli-ks Sep 18, 2026
3ebacec
Fix PAM tunnel diagnose proxy support
craiglurey Sep 18, 2026
1a2ef26
KC-1470: Protect Integration config records from Service Mode API acc…
amangalampalli-ks Sep 21, 2026
dda0399
Fix epm approval list truncating application_info/account_info in JSO…
sshrushanth-ks Sep 24, 2026
bdcf52a
KC-1468,1473,1474: Protect SailPoint config record from Service Mode …
amangalampalli-ks Sep 24, 2026
036862a
Fix false 'Supported columns' warning in enterprise-info when request…
sshrushanth-ks Sep 24, 2026
15db75c
Refresh rotation cache before pam rotation edit to avoid stale revisi…
idimov-keeper Sep 25, 2026
f2c3b9e
Merge proxy-tunnel-ephemeral-jit onto release: KeeperSSH/RDP Proxy, e…
idimov-keeper Sep 25, 2026
2ffd0a2
Release 18.1.6
sk-keeper Sep 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions docs/cyberark-pam-import.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,13 @@ Keeper Vault/
| PaloAltoNetworks, CiscoIOS, CiscoASA, JuniperJunos, F5BigIP, CheckPointGAIA | pamMachine | ssh | 22 |
| CyberArk (internal) | pamMachine | ssh | 22 |
| BusinessWebsite | login | — | — |
| (empty platformId) | pamMachine | ssh | 22 |
| (unknown platformId) | pamMachine | ssh | 22 |
| Custom/renamed platform resolved by CyberArk metadata or keyword matching | inferred PAM type | inferred | inferred |
| Empty/unknown platform with no metadata or keyword match | login | — | — |

For an unresolved platform, fields not already represented by the login's title,
login, password, URL, or CyberArk notes are preserved as text custom fields.
Nested account metadata is flattened using dotted field names. Raw secret-bearing
payload keys are excluded from custom fields.

### Field Mapping

Expand Down Expand Up @@ -170,6 +175,9 @@ pam project cyberark-import pvwa.company.com --dry-run --output import.json --in
# Filter specific safes
pam project cyberark-import pvwa.company.com --safes "Production,Staging" --exclude-safes "Archive*"

# Import into Nested Share Folders (folders, records, rotation, PAM config)
pam project cyberark-import pvwa.company.com --name "CyberArk Migration" --gateway "My Gateway" --nsf

# Extend existing project
pam project cyberark-import pvwa.company.com --config <pam-config-uid>

Expand Down Expand Up @@ -199,7 +207,8 @@ pam project cyberark-cleanup --name "CyberArk Migration" --dry-run
| `--name`, `-n` | Project name |
| `--config`, `-c` | Extend existing PAM config UID |
| `--gateway`, `-g` | Gateway name or UID |
| `--folder-mode` | flat, exact, ksm (default) |
| `--folder-mode` | flat, exact, ksm, safe (default: safe) |
| `--nsf` | Create project folders/records/PAM config in Nested Share Folders |
| `--safes` | Include only these safes (comma/glob) |
| `--exclude-safes` | Exclude safes (comma/glob) |
| `--list-safes` | List safes and exit |
Expand Down Expand Up @@ -273,5 +282,3 @@ Before building the import JSON, the importer warns about:
- Rate limit handling: automatic retry on HTTP 429 with exponential backoff
- Pagination cap: MAX_FETCH_RECORDS (50,000) prevents OOM attacks
---


2 changes: 1 addition & 1 deletion keepercommander/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
# Contact: commander@keepersecurity.com
#

__version__ = '18.1.5'
__version__ = '18.1.6'
8 changes: 8 additions & 0 deletions keepercommander/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,14 @@ def main(from_package=False):
if from_package:
sys.excepthook = handle_exceptions

# Internal: background service mode for a frozen (PyInstaller) executable - see service_app.py.
is_frozen = getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS')
if is_frozen:
from .service.core.service_app import SERVICE_MODE_FLAG, run_background_service
if len(sys.argv) > 1 and sys.argv[1] == SERVICE_MODE_FLAG:
run_background_service()
return

sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
opts, flags = parser.parse_known_args(sys.argv[1:])

Expand Down
7 changes: 5 additions & 2 deletions keepercommander/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from .sync_down import sync_down
from .team import Team
from .ttk import TTK
from .sanitization import sanitize_protobuf_json

current_milli_time = lambda: int(round(time.time() * 1000))

Expand Down Expand Up @@ -881,7 +882,8 @@ def communicate_rest(params, request, endpoint, *, rs_type=None, payload_version
if request:
if logging.getLogger().level <= logging.DEBUG:
js = google.protobuf.json_format.MessageToJson(request)
logging.debug('>>> [RQ] %s: %s', endpoint, js)
sanitized_js = sanitize_protobuf_json(js)
logging.debug('>>> [RQ] %s: %s', endpoint, sanitized_js)
api_request_payload.payload = request.SerializeToString()
if isinstance(payload_version, int):
api_request_payload.apiVersion = payload_version
Expand All @@ -894,7 +896,8 @@ def communicate_rest(params, request, endpoint, *, rs_type=None, payload_version
proto_rs.ParseFromString(rs)
if logging.getLogger().level <= logging.DEBUG:
js = google.protobuf.json_format.MessageToJson(proto_rs)
logging.debug('>>> [RS] %s: %s', endpoint, js)
sanitized_js = sanitize_protobuf_json(js)
logging.debug('>>> [RS] %s: %s', endpoint, sanitized_js)
return proto_rs
else:
return rs
Expand Down
124 changes: 119 additions & 5 deletions keepercommander/commands/discoveryrotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,25 @@ class PAMCreateRecordRotationCommand(Command):
def get_parser(self):
return PAMCreateRecordRotationCommand.parser

def execute_args(self, params, args, **kwargs):
# Reached only via normal command dispatch (interactive shell, a one-shot
# `keeper pam rotation edit ...` invocation, or a run-batch script line) -
# never by callers that instantiate this class directly and call execute()
# themselves (bulk PAM import/extend), who keep trusting their own
# pre-warmed record_rotation_cache for speed.
kwargs.setdefault('trust_cache', False)
# Only a literal False opts into the forced-resync path; any other value
# (True, None, or anything else a caller might pass) is treated as True.
trust_cache = kwargs['trust_cache'] is not False
kwargs['trust_cache'] = trust_cache
try:
return super().execute_args(params, args, **kwargs)
finally:
if not trust_cache:
# Make sure the shell picks up the change (or the fresh sync we just
# forced) via the standard "sync after command" mechanism in do_command().
params.sync_data = True

def execute(self, params, **kwargs):
"""Configure rotation settings for one or multiple PAM records.

Expand All @@ -577,6 +596,9 @@ def execute(self, params, **kwargs):
resource linkage and then submits rotation requests to the Keeper
PAM router service.
"""
# Only a literal False opts into the forced-resync path; any other value
# (True, missing/defaulted True, None, or anything else) is treated as True.
trust_cache = kwargs.pop('trust_cache', True) is not False

def config_resource(_dag, target_record, target_config_uid, silent=None):
if not _dag.linking_dag.has_graph:
Expand Down Expand Up @@ -1497,6 +1519,15 @@ def config_user(_dag, target_record, target_resource_uid, target_config_uid=None
if not kwargs.get('silent'):
logging.info('Selected %d PAM record(s) for rotation', len(pam_records))

if not trust_cache:
# Refresh the local rotation cache before reading any cached revision below.
# A stale rotation revision here would collide with a real rotation change made
# elsewhere (another session, a scheduled rotation) since this cache was
# last warmed, and the router rejects the update with an error like
# mismatched_revision_blocking_update. Bulk callers that pre-warm record_rotation_cache
# themselves (pam project import/extend) pass trust_cache=True and skip this.
api.sync_down(params)

schedule_config = kwargs.get('schedule_config') is True
if schedule_config:
refresh_vault_for_schedule_config(params)
Expand Down Expand Up @@ -2429,7 +2460,7 @@ def print_root_rotation_setting(params, is_verbose=False, format_type='table'):
for c in configurations: # type: vault.TypedRecord
if c.record_type in ('pamAwsConfiguration', 'pamAzureConfiguration', 'pamGcpConfiguration',
'pamDomainConfiguration', 'pamNetworkConfiguration', 'pamOciConfiguration',
'pamGitHubConfiguration'):
'pamGitHubConfiguration', 'pamHashiCorpConfiguration'):
facade.record = c
folder_info = resolve_pam_config_folder_info(
params, facade, c.record_uid)
Expand Down Expand Up @@ -2489,7 +2520,7 @@ def print_root_rotation_setting(params, is_verbose=False, format_type='table'):

common_parser = argparse.ArgumentParser(add_help=False)
common_parser.add_argument('--environment', '-env', dest='config_type', action='store',
choices=['local', 'aws', 'azure', 'gcp', 'domain', 'oci', 'github'], help='PAM Configuration Type')
choices=['local', 'aws', 'azure', 'gcp', 'domain', 'oci', 'github', 'hashicorp'], help='PAM Configuration Type')
common_parser.add_argument('--title', '-t', dest='title', action='store', help='Title of the PAM Configuration')
common_parser.add_argument('--gateway', '-g', dest='gateway_uid', action='store', help='Gateway UID or Name')
common_parser.add_argument('--shared-folder', '-sf', dest='shared_folder_uid', action='store',
Expand Down Expand Up @@ -2555,12 +2586,23 @@ def print_root_rotation_setting(params, is_verbose=False, format_type='table'):
github_group.add_argument('--github-base-url', dest='github_base_url', action='store',
help='GitHub Base URL')

hashicorp_group = common_parser.add_argument_group('hashicorp', 'HashiCorp Vault configuration')
hashicorp_group.add_argument('--hashicorp-id', dest='hashicorp_id', action='store', help='HashiCorp Id')
hashicorp_group.add_argument('--vault-base-url', dest='vault_base_url', action='store',
help='Vault Base URL (e.g., https://vault.company.com:8200)')
hashicorp_group.add_argument('--vault-token', dest='vault_token', action='store',
help='Vault Token (optional; syncIdentity takes precedence)')
hashicorp_group.add_argument('--vault-namespace', dest='vault_namespace', action='store',
help='Vault Namespace (optional; leave blank for Community Edition)')
hashicorp_group.add_argument('--vault-mount-path', dest='vault_mount_path', action='store',
help='Vault KV Mount Path (optional; defaults to "secret")')

class PamConfigurationEditMixin(RecordEditMixin):
pam_record_types = None
PAM_CONFIG_RECORD_TYPES = frozenset({
'pamAwsConfiguration', 'pamAzureConfiguration', 'pamGcpConfiguration',
'pamDomainConfiguration', 'pamNetworkConfiguration', 'pamOciConfiguration',
'pamGitHubConfiguration',
'pamGitHubConfiguration', 'pamHashiCorpConfiguration',
})
PAM_RESOURCE_RECORD_TYPES = frozenset({
'pamDatabase', 'pamDirectory', 'pamMachine', 'pamRemoteBrowser',
Expand Down Expand Up @@ -2859,6 +2901,22 @@ def parse_properties(self, params, record, **kwargs): # type: (KeeperParams, va
oci_region = kwargs.get('oci_region')
if oci_region:
extra_properties.append(f'text.regionOci={oci_region}')
elif record.record_type == 'pamHashiCorpConfiguration':
hashicorp_id = kwargs.get('hashicorp_id')
if hashicorp_id:
extra_properties.append(f'text.pamHashiCorpId={hashicorp_id}')
vault_base_url = kwargs.get('vault_base_url')
if vault_base_url:
extra_properties.append(f'text.pamHashiCorpVaultBaseUrl={vault_base_url}')
vault_token = kwargs.get('vault_token')
if vault_token:
extra_properties.append(f'secret.pamHashiCorpVaultToken={vault_token}')
vault_namespace = kwargs.get('vault_namespace')
if vault_namespace:
extra_properties.append(f'text.pamHashiCorpVaultNamespace={vault_namespace}')
vault_mount_path = kwargs.get('vault_mount_path')
if vault_mount_path:
extra_properties.append(f'text.pamHashiCorpVaultMountPath={vault_mount_path}')
if extra_properties:
self.assign_typed_fields(record, [RecordEditMixin.parse_field(x) for x in extra_properties])

Expand Down Expand Up @@ -2936,9 +2994,11 @@ def execute(self, params, **kwargs):
record_type = 'pamDomainConfiguration'
elif config_type == 'oci':
record_type = 'pamOciConfiguration'
elif config_type == 'hashicorp':
record_type = 'pamHashiCorpConfiguration'
else:
raise CommandError('pam-config-new', f'--environment {config_type} is not supported'
' - supported options: local, aws, azure, gcp, domain, oci, github')
' - supported options: local, aws, azure, gcp, domain, oci, github, hashicorp')

title = kwargs.get('title')
if not title:
Expand Down Expand Up @@ -3265,7 +3325,34 @@ def execute(self, params, **kwargs):
if rri_status_name == 'RRS_ONLINE':

configuration_uid = utils.base64_url_encode(rri.configurationUid)
gateway_name = rri.controllerName if rri.controllerName else '-'
gateway_name = rri.controllerName
if not gateway_name and rri.controllerUid:
def _normalize_uid(uid):
if uid is None:
return None
if isinstance(uid, (bytes, bytearray)):
return utils.base64_url_encode(uid)
return str(uid)

target_uid = _normalize_uid(rri.controllerUid)
if target_uid is None:
gateway_name = None
else:
try:
all_gateways = gateway_helper.get_all_gateways(params) or []
except (Exception,) as ex:
logging.debug(f"Failed to retrieve gateway list for name resolution: {ex}")
all_gateways = []

if all_gateways:
matched = next((g for g in all_gateways
if _normalize_uid(getattr(g, 'controllerUid', None)) == target_uid), None)
if matched:
gateway_name = getattr(matched, 'controllerName', None)
if gateway_name:
logging.debug(f"Resolved gateway name from controllerUid {target_uid} -> {gateway_name}")

gateway_name = gateway_name if gateway_name else '-'
gateway_uid = utils.base64_url_encode(rri.controllerUid) if rri.controllerUid else '-'

def is_resource_ok(resource_id, params, configuration_uid):
Expand Down Expand Up @@ -3754,6 +3841,28 @@ def _is_rotation_allowed_by_enforcement(params):
return True


def ensure_gateway_management_allowed(params):
# type: (KeeperParams) -> bool
"""Per-user enforcement gate on the 'allow_pam_gateway' role enforcement
(confirmed via live account_summary payload). Prints an error and returns
False when the user's enterprise enforcement disallows Gateway management.
Shared by both PAMCreateGatewayCommand and PAMGatewayRemoveCommand (and
their legacy discoveryrotation_v1 counterparts) to avoid duplicating the
check at every call site.
"""
try:
from .workflow.helpers import is_pam_action_allowed_by_enforcement
except ImportError as _e:
logging.debug('workflow.helpers not available; skipping gateway enforcement check: %s', _e)
return True

if not is_pam_action_allowed_by_enforcement(params, 'allow_pam_gateway'):
print(f"{bcolors.FAIL}Gateway management is not allowed by your enterprise "
f"enforcement (allow_pam_gateway).{bcolors.ENDC}")
return False
return True


class PAMGatewayActionRotateCommand(Command):
parser = argparse.ArgumentParser(prog='pam action rotate')
parser.add_argument('--record-uid', '-r', dest='record_uid', action='store',
Expand Down Expand Up @@ -4393,6 +4502,9 @@ def get_parser(self):
return PAMGatewayRemoveCommand.dr_remove_controller_parser

def execute(self, params, **kwargs):
if not ensure_gateway_management_allowed(params):
return

gateway_name = kwargs.get('gateway')
gateways = gateway_helper.get_all_gateways(params)

Expand Down Expand Up @@ -4461,6 +4573,8 @@ def get_parser(self):
return PAMCreateGatewayCommand.dr_create_controller_parser

def execute(self, params, **kwargs):
if not ensure_gateway_management_allowed(params):
return

gateway_name = kwargs.get('gateway_name')
ksm_app = kwargs.get('ksm_app')
Expand Down
7 changes: 6 additions & 1 deletion keepercommander/commands/discoveryrotation_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from keeper_secrets_manager_core.utils import url_safe_str_to_bytes

from .base import Command, GroupCommand, user_choice, dump_report_data, report_output_parser, json_output_parser, field_to_title, FolderMixin
from .discoveryrotation import PAMLegacyCommand
from .discoveryrotation import PAMLegacyCommand, ensure_gateway_management_allowed
from .folder import FolderMoveCommand
from .ksm import KSMCommand
from .pam import gateway_helper, router_helper
Expand Down Expand Up @@ -1710,6 +1710,9 @@ def get_parser(self):
return PAMGatewayRemoveCommand.dr_remove_controller_parser

def execute(self, params, **kwargs):
if not ensure_gateway_management_allowed(params):
return

gateway_name = kwargs.get('gateway')
gateways = gateway_helper.get_all_gateways(params)

Expand Down Expand Up @@ -1745,6 +1748,8 @@ def get_parser(self):
return PAMCreateGatewayCommand.dr_create_controller_parser

def execute(self, params, **kwargs):
if not ensure_gateway_management_allowed(params):
return

gateway_name = kwargs.get('gateway_name')
ksm_app = kwargs.get('ksm_app')
Expand Down
Loading
Loading