Skip to content
Merged

Release #1989

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5d3a828
fixed cyberark portal authentication (#1959)
pvagare-ks Apr 15, 2026
6a25293
added old legacy support
pvagare-ks Apr 15, 2026
10a7c5e
Service mode parser response and security improvements (#1945)
amangalampalli-ks Apr 13, 2026
fa07701
Remove sync-down from service-mode
amangalampalli-ks Apr 14, 2026
70640e5
pam launch mysql fix (#1963)
idimov-keeper Apr 17, 2026
422ba0d
added missing record field (#1964)
pvagare-ks Apr 17, 2026
6183151
added folder import in cyberark_portal
pvagare-ks Apr 17, 2026
28fca0e
KC- 625 Added --purge flag to rm command to differentiate remove vs.…
sshrushanth-ks Apr 21, 2026
897f653
spelling change
sshrushanth-ks Apr 21, 2026
c91c5af
Fixed ambiguous title match check applying to all record lookup paths
sshrushanth-ks Apr 21, 2026
7e60329
Remove other developer's PAM workflow implementation
amangalampalli-ks Apr 16, 2026
dddf20c
Implementation of PAM Workflow Commands (#1830)
amangalampalli-ks Apr 21, 2026
29ad8e7
Fix IIS pool text for PAM debug commands.
jwalstra-keeper Apr 21, 2026
3e5f83e
Added details to json verbose response in pam config list for single …
adeshmukh-ks Apr 22, 2026
e0f2329
Fix: EPM policy can not be created without name and mandatory fields.…
sshrushanth-ks Apr 16, 2026
2cd1eca
fix(epm): allow --machine-filter to accept UIDs not in local collecti…
sshrushanth-ks Apr 22, 2026
efdf580
Fixed keeper server hostname parsing (#1980)
idimov-keeper Apr 22, 2026
e856deb
perf(pam launch): cache TunnelDAG across the launch flow (#1981)
idimov-keeper Apr 22, 2026
e9bfce1
perf(pam launch): shrink tunnel-open sleeps and parallelize WebSocket…
idimov-keeper Apr 22, 2026
48ab33f
Improve kepm scim command by supporting Kerberos. KC-1228
sk-keeper Apr 22, 2026
c9c034e
perf(pam launch): batch ICE candidate flush and skip unneeded waits (…
idimov-keeper Apr 23, 2026
0b9aa0e
perf(pam launch): cross-launch DAG cache + polish (rust-log grace, We…
idimov-keeper Apr 23, 2026
1cfa99d
Pam Remote Browser Get JSON response data added
adeshmukh-ks Apr 24, 2026
a9ae219
Add a check for conflicting Automators in Automator Create Command
lthievenaz-keeper Apr 24, 2026
ccaf00a
Release 17.2.14
sk-keeper Apr 24, 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
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__ = '17.2.13'
__version__ = '17.2.14'
1 change: 1 addition & 0 deletions keepercommander/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
('pam rotation', 'Manage Rotations'),
('pam split', 'Split credentials from legacy PAM Machine'),
('pam tunnel', 'Manage Tunnels'),
('pam workflow', 'Manage PAM Workflows'),
]
domain_subcommands = [
('domain list (dl)', 'List all reserved domains for the enterprise'),
Expand Down Expand Up @@ -277,15 +278,15 @@
# Look up server in KEEPER_SERVERS (case insensitive)
server_upper = server.upper()
if server_upper in KEEPER_SERVERS:
params.server = KEEPER_SERVERS[server_upper]

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (secret)
as clear text.
logging.info('Keeper region is set to %s', server_upper)
else:
# Check if it matches a valid hostname directly
server_lower = server.lower()
if server_lower in KEEPER_SERVERS.values():
params.server = server_lower

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (secret)
as clear text.
logging.info('Keeper server is set to %s', params.server)
else:

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (secret)
as clear text.
logging.error('Invalid region: %s', server)
print(f'Valid regions: {", ".join(sorted(KEEPER_SERVERS.keys()))}')
else:
Expand Down Expand Up @@ -400,7 +401,7 @@
params, args = check_if_running_as_mc(params, args)

if is_enterprise_command(cmd, command, args) and not params.enterprise:
if is_executing_as_msp_admin():

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (secret)
as clear text.
logging.debug("OK to execute command: %s", cmd)
else:
logging.error('This command is restricted to Keeper Enterprise administrators.')
Expand Down
17 changes: 11 additions & 6 deletions keepercommander/commands/automator.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import logging
import os
import re
import json

from typing import Optional
from cryptography.hazmat.primitives.serialization import pkcs12
Expand Down Expand Up @@ -286,11 +287,15 @@ def execute(self, params, **kwargs):
return
matched_node_id = nodes[0]['node_id']
self.ensure_loaded(params, False)
# if params.automators: # type: list[automator_proto.AutomatorInfo]
# n = next((True for x in params.automators if x.nodeId == matched_node_id), None)
# if n:
# logging.warning('Automator for node \"%s\" already exists', node)
# return

automators = json.loads(self.dump_automators(params,fmt='json'))
conflict_automators = [automator['name'] for automator in automators if automator['node_id']==1067368092533492 and automator['enabled']]
if conflict_automators:
logging.warning('\n- '.join(
['Enabled Automator(s) have been found in this node. Unless disabled, they will take precedence for handling automator tasks.'] +
conflict_automators)
)
if input('Continue (y/n)? ').lower() != 'y': return

rq = automator_proto.AdminCreateAutomatorRequest()
rq.nodeId = matched_node_id
Expand Down Expand Up @@ -551,4 +556,4 @@ def print_cert(title, c):
def is_authorised(self):
return False



4 changes: 2 additions & 2 deletions keepercommander/commands/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def register_command_info(aliases, command_info):

convert_parser = argparse.ArgumentParser(prog='convert', description='Convert record(s) to use record types')
convert_parser.add_argument(
'-t', '--record-type', '--record_type', dest='record_type', action='store', help='Convert to record type'
'-t', '--record-type', dest='record_type', action='store', help='Convert to record type (default: login)'
)
convert_parser.add_argument(
'-q', '--quiet', dest='quiet', action='store_true', help="Don't display info about records matched and converted"
Expand Down Expand Up @@ -79,7 +79,7 @@ def register_command_info(aliases, command_info):
description='Convert all legacy General records in the vault to a typed record format'
)
convert_all_parser.add_argument(
'-t', '--record-type', '--record_type', dest='record_type', action='store',
'-t', '--record-type', dest='record_type', action='store',
help='Target record type (default: login)'
)
convert_all_parser.add_argument(
Expand Down
78 changes: 71 additions & 7 deletions keepercommander/commands/discoveryrotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
from .email_commands import find_email_config_record, load_email_config_from_record, update_oauth_tokens_in_record
from .enterprise_common import EnterpriseCommand
from ..email_service import EmailSender, build_onboarding_email
from .tunnel.port_forward.TunnelGraph import TunnelDAG
from .tunnel.port_forward.TunnelGraph import TunnelDAG, get_vertex_content
from .tunnel.port_forward.tunnel_helpers import get_config_uid, get_keeper_tokens
from .. import api, utils, vault_extensions, crypto, vault, record_management, attachment, record_facades
from ..display import bcolors
Expand All @@ -76,9 +76,9 @@
from .pam_debug.rotation_setting import PAMDebugRotationSettingsCommand
from .pam_debug.vertex import PAMDebugVertexCommand
from .pam_import.commands import PAMProjectCommand
from keepercommander.commands.pam_cloud.pam_privileged_workflow import PAMPrivilegedWorkflowCommand
from keepercommander.commands.pam_cloud.pam_privileged_access import PAMPrivilegedAccessCommand
from .pam_launch.launch import PAMLaunchCommand
from .workflow import PAMWorkflowCommand
from .pam_service.list import PAMActionServiceListCommand
from .pam_service.add import PAMActionServiceAddCommand
from .pam_service.remove import PAMActionServiceRemoveCommand
Expand Down Expand Up @@ -192,8 +192,7 @@ def __init__(self):
self.register_command('rbi', PAMRbiCommand(), 'Manage Remote Browser Isolation', 'b')
self.register_command('project', PAMProjectCommand(), 'PAM Project Import/Export', 'p')
self.register_command('launch', PAMLaunchCommand(), 'Launch a connection to a PAM resource', 'l')
self.register_command('workflow', PAMPrivilegedWorkflowCommand(),
'Manage workflow access operations', 'wf')
self.register_command('workflow', PAMWorkflowCommand(), 'Manage PAM Workflows', 'w')
self.register_command('access', PAMPrivilegedAccessCommand(),
'Manage privileged cloud access operations', 'ac')

Expand Down Expand Up @@ -1759,6 +1758,53 @@ def execute(self, params, **kwargs):
is_config=True, transmission_key=transmission_key)
tmp_dag.print_tunneling_config(pam_configuration_uid, None)

@staticmethod
def _allowed_settings_dag_to_json(allowed):
# type: (dict) -> dict
"""Maps PAM graph allowedSettings to JSON keys matching pam config edit/new flags."""
if not allowed:
allowed = {}
return {
'connections': allowed.get('connections'),
'tunneling': allowed.get('portForwards'),
'rotation': allowed.get('rotation'),
'remote_browser_isolation': allowed.get('remoteBrowserIsolation'),
'connections_recording': allowed.get('sessionRecording'),
'typescript_recording': allowed.get('typescriptRecording'),
'ai_threat_detection': allowed.get('aiEnabled'),
'ai_terminate_session_on_detection': allowed.get('aiSessionTerminate'),
}

@staticmethod
def _domain_administrative_credential_uid(configuration):
# type: (vault.KeeperRecord) -> Optional[str]
if not isinstance(configuration, vault.TypedRecord) or \
configuration.record_type != 'pamDomainConfiguration':
return None
prf = configuration.get_typed_field('pamResources')
if not prf or not prf.value or not isinstance(prf.value[0], dict):
return None
return prf.value[0].get('adminCredentialRef') or None

@staticmethod
def _pam_config_allowed_settings_json(params, config_uid):
try:
encrypted_session_token, encrypted_transmission_key, transmission_key = get_keeper_tokens(params)
tmp_dag = TunnelDAG(
params, encrypted_session_token, encrypted_transmission_key, config_uid,
is_config=True, transmission_key=transmission_key,
)
tmp_dag.linking_dag.load()
vertex = tmp_dag.linking_dag.get_vertex(config_uid)
content = get_vertex_content(vertex) if vertex else None
a = (content or {}).get('allowedSettings')
if a is None:
a = {}
return PAMConfigurationListCommand._allowed_settings_dag_to_json(a)
except Exception as e:
logging.getLogger(__name__).debug('PAM config allowedSettings: %s', e)
return PAMConfigurationListCommand._allowed_settings_dag_to_json({})

@staticmethod
def print_pam_configuration_details(params, config_uid, is_verbose=False, format_type='table'):
configuration = vault.KeeperRecord.load(params, config_uid)
Expand Down Expand Up @@ -1796,6 +1842,7 @@ def print_pam_configuration_details(params, config_uid, is_verbose=False, format
"uid": sf.shared_folder_uid if sf else None
} if sf else None,
"gateway_uid": facade.controller_uid,
"gateway_name": facade.title,
"resource_record_uids": facade.resource_ref,
"fields": {}
}
Expand All @@ -1806,12 +1853,20 @@ def print_pam_configuration_details(params, config_uid, is_verbose=False, format
values = list(field.get_external_value())
if not values:
continue
field_name = field.get_field_name()
field_name = field.label if field.label else field.type
if field.type == 'schedule':
field_name = 'Default Schedule'

config_data["fields"][field_name] = values

if configuration.record_type == 'pamDomainConfiguration':
config_data['domain_administrative_credential'] = (
PAMConfigurationListCommand._domain_administrative_credential_uid(configuration))

if is_verbose:
config_data['allowed_settings'] = PAMConfigurationListCommand._pam_config_allowed_settings_json(
params, configuration.record_uid)

return json.dumps(config_data, indent=2)
else:
table = []
Expand Down Expand Up @@ -2391,6 +2446,11 @@ class PAMConfigurationEditCommand(Command, PamConfigurationEditMixin):
help='Set recording connections permissions for the resource')
parser.add_argument('--typescript-recording', '-tr', dest='typescriptrecording', choices=choices,
help='Set TypeScript recording permissions for the resource')
parser.add_argument('--ai-threat-detection', dest='ai_threat_detection', choices=choices,
help='Set AI threat detection permissions')
parser.add_argument('--ai-terminate-session-on-detection', dest='ai_terminate_session_on_detection',
choices=choices,
help='Set AI session termination on threat detection permissions')

def __init__(self):
super(PAMConfigurationEditCommand, self).__init__()
Expand Down Expand Up @@ -2492,13 +2552,17 @@ def execute(self, params, **kwargs):
_rbi = kwargs.get('remotebrowserisolation', None)
_recording = kwargs.get('recording', None)
_typescript_recording = kwargs.get('typescriptrecording', None)
_ai_threat = kwargs.get('ai_threat_detection', None)
_ai_terminate = kwargs.get('ai_terminate_session_on_detection', None)

if (_connections is not None or _tunneling is not None or _rotation is not None or _rbi is not None or
_recording is not None or _typescript_recording is not None or orig_admin_cred_ref != admin_cred_ref):
_recording is not None or _typescript_recording is not None or _ai_threat is not None or
_ai_terminate is not None or orig_admin_cred_ref != admin_cred_ref):
encrypted_session_token, encrypted_transmission_key, transmission_key = get_keeper_tokens(params)
tmp_dag = TunnelDAG(params, encrypted_session_token, encrypted_transmission_key,
configuration.record_uid, is_config=True, transmission_key=transmission_key)
tmp_dag.edit_tunneling_config(_connections, _tunneling, _rotation, _recording, _typescript_recording, _rbi)
tmp_dag.edit_tunneling_config(_connections, _tunneling, _rotation, _recording, _typescript_recording, _rbi,
_ai_threat, _ai_terminate)
if orig_admin_cred_ref != admin_cred_ref:
if orig_admin_cred_ref: # just drop is_admin from old Domain
tmp_dag.link_user_to_config_with_options(orig_admin_cred_ref, is_admin='default')
Expand Down
Loading
Loading