Skip to content
Merged

Release #2165

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ab6267f
Support empty user/ports and other improvements (#2122)
lthievenaz-keeper Jun 13, 2026
242f85f
Thycotic secret ids arg (#2149)
sk-keeper Jun 13, 2026
c5307db
Enforce GENERATED_PASSWORD_COMPLEXITY and RESTRICT_RECORD_TYPES on ns…
sshrushanth-ks Jun 15, 2026
49399a1
KEPM: system collection
sk-keeper Jun 16, 2026
2a81177
KC-1307, KC-1308, KC-1309 & KC-1310: Fix nsf share expiration updates…
sshrushanth-ks Jun 16, 2026
8eff5c5
Fix SQL injection in MSSQL password rotation (#2151)
amangalampalli-ks Jun 16, 2026
a2e3ef8
Fix IAM user link permission-check rotation request shape. (#2159)
idimov-keeper Jun 17, 2026
63de90e
Add --online filter to pam gateway list command (#2158)
idimov-keeper Jun 17, 2026
2a94cbb
Add secret-ids arg in Thycotic import (#2150)
lthievenaz-keeper Jun 17, 2026
dc889d0
Add pam connection ai command for KeeperAI settings on PAM resources.…
idimov-keeper Jun 18, 2026
c2e4c66
DR-1280 Fix SaaS profile for `pam rotation edit` command.
jwalstra-keeper Jun 18, 2026
731a00d
Transfer enterprise to another MSP. KC-1024 (#2027)
sk-keeper Jun 19, 2026
0fe763e
Release 18.0.9
sk-keeper Jun 21, 2026
d6e1ca1
Add AI enable toggles to pam connection ai and fix default reset.
idimov-keeper Jun 18, 2026
9dfca9a
Update pedm protobuf
sk-keeper Jun 22, 2026
4505850
KC-1314: Add vault-style passphrase generation to Commander CLI (#2164)
sshrushanth-ks Jun 22, 2026
de79fcd
Strictly validate passphrase word count (5–9), separator, and true/fa…
sshrushanth-ks Jun 23, 2026
995a782
updated test file
sshrushanth-ks Jun 23, 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
11 changes: 11 additions & 0 deletions examples/pam-kcm-import/KCM_mappings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
{
"ports":{
"rdp":3389,
"ssh":22,
"vnc":5900,
"telnet":23,
"kubernetes":8080,
"mysql":3306,
"postgresql":5432,
"sql-server":1433
},
"users":{
"username":"login",
"password":"password",
Expand All @@ -21,6 +31,7 @@
"disable-copy": "pam_settings.connection.disable_copy",
"disable-paste": "pam_settings.connection.disable_paste",
"force-lossless": null,
"allow-file-uploads":null,
"read-only": null,
"backspace": null,
"url": "url",
Expand Down
132 changes: 77 additions & 55 deletions examples/pam-kcm-import/kcm_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,9 @@ def collect_db_config(self):
'(2) I have hardcoded them in the Python script'
])
if handle_prompt({'1':'file','2':'code'}) == 'file':
display('## Please upload your docker-compose file', 'cyan')
self.docker_compose = validate_file_upload('yaml')
display('## Please upload your docker-compose file\n(blank for /etc/kcm-setup/docker-compose.yml)', 'cyan')
docker_file = input('File path: ') or '/etc/kcm-setup/docker-compose.yml'
self.docker_compose = validate_file_upload('yaml',docker_file)

port={'MYSQL':3306,'POSTGRES':5432}
custom_port = None
Expand Down Expand Up @@ -340,9 +341,9 @@ def generate_data(self):
templ = validate_file_upload('json')
templ_resources = templ.get('pam_data',{}).get('resources',[])
if len(templ_resources)>1:
self.template_rs = templ_resources[1]
self.template_rs = deepcopy(templ_resources[1])
if self.template_rs.get('users',[]):
self.template_usr = self.template_rs['users'][0]
self.template_usr = deepcopy(self.template_rs['users'][0])
print()

self.group_paths = {}
Expand Down Expand Up @@ -373,6 +374,55 @@ def resolve_path(group_id):
self.users = {}
self.shared_folders = []

def handle_mapping(mapping, name, arg, value, dir):
if mapping == 'ignore':
debug(f'Mapping {arg} ignored', self.debug)
return dir
if mapping == 'log':
if name not in self.logged_records:
self.logged_records[name] = {'name':name}
if 'parameters' not in self.logged_records:
self.logged_records[name]['parameters'] = []
self.logged_records[name]['parameters'].append(arg)
return dir
if mapping is None:
debug(f'Mapping {arg} recognized but not supported', self.debug)
return dir
if '=' in mapping:
mapping, value = mapping.split('=', 1)
keys = mapping.split('.')
set_nested(dir[id], keys, value)
return dir

def handle_arg(id,name,arg,value,resource,user):

if value.startswith('${KEEPER_') and id not in self.dynamic_tokens:
debug('Dynamic token detected',self.debug)
self.dynamic_tokens.append(id)
if name not in self.logged_records:
self.logged_records[name] = {'name':name, 'dynamic_token':True}
else:
self.logged_records[name]['dynamic_token'] = True
elif value and arg.startswith('totp-'):
if 'oneTimeCode' not in user:
user['oneTimeCode'] = {
"totp-algorithm": '',
"totp-digits": "",
"totp-period": "",
"totp-secret": ""
}
user['oneTimeCode'][arg] = value
elif value and arg == 'hostname':
resource['host'] = value
elif value and arg == 'port':
resource['pam_settings']['connection']['port'] = value
elif value and arg in self.mappings['users']:
self.users = handle_mapping(self.mappings['users'][arg],name,arg,value,self.users)
elif arg in self.mappings['resources']:
self.connections = handle_mapping(self.mappings['resources'][arg],name,arg,value,self.connections)
else:
display(f'Error: Unknown parameter detected: {arg}. Add it to KCM_mappings.json to resolve this error','bold red')

for connection in self.connection_data:
id = connection['connection_id']
name = connection["name"]
Expand Down Expand Up @@ -445,50 +495,7 @@ def resolve_path(group_id):
resource['pam_settings']['connection']['launch_credentials'] = f'KCM User - {name}'
self.connections[id] = resource

def handle_arg(id,name,arg,value,resource,user):
def handle_mapping(mapping, value, dir):
if mapping == 'ignore':
debug(f'Mapping {arg} ignored', self.debug)
return dir
if mapping == 'log':
record = self.logged_records.setdefault(name, {'name': name})
record[arg] = value
return dir
if mapping is None:
debug(f'Mapping {arg} recognized but not supported', self.debug)
return dir
if '=' in mapping:
mapping, value = mapping.split('=', 1)
keys = mapping.split('.')
set_nested(dir[id], keys, value)
return dir

if value.startswith('${KEEPER_') and id not in self.dynamic_tokens:
debug('Dynamic token detected',self.debug)
self.dynamic_tokens.append(id)
if name not in self.logged_records:
self.logged_records[name] = {'name':name, 'dynamic_token':True}
else:
self.logged_records[name]['dynamic_token'] = True
elif value and arg.startswith('totp-'):
if 'oneTimeCode' not in user:
user['oneTimeCode'] = {
"totp-algorithm": '',
"totp-digits": "",
"totp-period": "",
"totp-secret": ""
}
user['oneTimeCode'][arg] = value
elif value and arg == 'hostname':
resource['host'] = value
elif value and arg == 'port':
resource['pam_settings']['connection']['port'] = value
elif value and arg in self.mappings['users']:
self.users = handle_mapping(self.mappings['users'][arg],value,self.users)
elif arg in self.mappings['resources']:
self.connections = handle_mapping(self.mappings['resources'][arg],value,self.connections)
else:
display(f'Error: Unknown parameter detected: {arg}. Add it to KCM_mappings.json to resolve this error','bold red')


# Handle args
if connection['parameter_name']:
Expand All @@ -497,8 +504,7 @@ def handle_mapping(mapping, value, dir):
if connection['attribute_name']:
handle_arg(id,connection['name'],connection['attribute_name'],connection['attribute_value'],self.connections[id],self.users[id])


self.user_records = list(user for user in self.users.values())
self.user_records = list(user for user in self.users.values() if user.get('login',None))
self.resource_records = list(conn for conn in self.connections.values())

# Sanitize totp
Expand All @@ -511,10 +517,25 @@ def handle_mapping(mapping, value, dir):
stripped_secret = ''.join([x for x in secret if x.isnumeric()])
user['otp'] = f'otpauth://totp/{TOTP_ACCOUNT}?secret={stripped_secret}&issuer=&algorithm={alg}&digits={dig}&period={period}'

# Handle SFTP records
usernames = [x['title'] for x in self.user_records]
for resource in self.resource_records:
# Replace launch_credentials with autofill_credentials on http
if resource['pam_settings']['connection']['protocol']=='http':
resource['pam_settings']['connection']['autofill_credentials'] = resource['pam_settings']['connection']['launch_credentials']
del resource['pam_settings']['connection']['launch_credentials']
else:
# Remove non-existent users and add to logs
if resource['pam_settings']['connection']['launch_credentials'] not in usernames:
if resource['title'] not in self.logged_records:
self.logged_records[resource['title']] = {'name':resource['title']}
self.logged_records[resource['title']]['empty_credentials'] = True
resource['pam_settings']['connection']['launch_credentials'] = None
# Handle empty ports
if not resource['pam_settings']['connection'].get('port',None):
resource['pam_settings']['connection']['port'] = self.mappings['ports'].get(resource['pam_settings']['connection']['protocol'],'')
# Handle SFTP records
if 'sftp' in resource['pam_settings']['connection']:
sftp_settings = resource['pam_settings']['connection']['sftp']
sftp_settings = deepcopy(resource['pam_settings']['connection']['sftp'])
# Create resource for SFTP
sftp_resource = {
'folder_path':resource['folder_path']+'/SFTP Resources',
Expand Down Expand Up @@ -556,12 +577,13 @@ def handle_mapping(mapping, value, dir):

if self.dynamic_tokens:
display(f'- {len(self.dynamic_tokens)} dynamic tokens detected, they will be added to the JSON file.','yellow')
if len(self.logged_records)-len(self.dynamic_tokens)>0:
display(f'- {len(self.logged_records)-len(self.dynamic_tokens)} records logged, they will be added to the JSON file.','yellow')
logged_records_count = len([x for x in self.logged_records if self.logged_records[x].get('parameters',None)])
if logged_records_count:
display(f'- {logged_records_count} records logged, they will be added to the JSON file.','yellow')

logged_records = []
if self.logged_records:
logged_records = (list(record for record in self.logged_records.values()))
logged_records = list(record for record in self.logged_records.values())

shared_folders = []
for folder in self.shared_folders:
Expand Down
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.0.8'
__version__ = '18.0.9'
17 changes: 16 additions & 1 deletion keepercommander/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,8 @@ def communicate_rest(params, request, endpoint, *, rs_type=None, payload_version
return rs
elif isinstance(rs, dict):
kae = KeeperApiError(rs['error'], rs['message'])
kae.additional_info = rs.get('additional_info')

if kae.result_code == 'session_token_expired':
params.session_token = None
raise kae
Expand Down Expand Up @@ -1507,7 +1509,20 @@ def login_and_get_mc_params_login_v3(params: KeeperParams, mc_id):

mc_params.session_token = loginv3.CommonHelperMethods.bytes_to_url_safe_str(resp.encryptedSessionToken)
mc_params.msp_tree_key = params.enterprise['unencrypted_tree_key']
tree_key = crypto.decrypt_aes_v2(utils.base64_url_decode(resp.encryptedTreeKey), mc_params.msp_tree_key)
encrypted_tree_key = utils.base64_url_decode(resp.encryptedTreeKey)
key_type = resp.keyTypeId
if key_type == 1 or len(encrypted_tree_key) > 200: # RSA
private_key_bytes = utils.base64_url_decode(params.enterprise['keys']['rsa_encrypted_private_key'])
private_key_bytes = crypto.decrypt_aes_v2(private_key_bytes, mc_params.msp_tree_key)
private_key = crypto.load_rsa_private_key(private_key_bytes)
tree_key = crypto.decrypt_rsa(encrypted_tree_key, private_key)
elif key_type == 4 or len(encrypted_tree_key) == 125: # EC
private_key_bytes = utils.base64_url_decode(params.enterprise['keys']['ecc_encrypted_private_key'])
private_key_bytes = crypto.decrypt_aes_v2(private_key_bytes, mc_params.msp_tree_key)
private_key = crypto.load_ec_private_key(private_key_bytes)
tree_key = crypto.decrypt_ec(encrypted_tree_key, private_key)
else:
tree_key = crypto.decrypt_aes_v2(encrypted_tree_key, mc_params.msp_tree_key)

sync_down(mc_params)
query_enterprise(mc_params, True, tree_key=tree_key)
Expand Down
4 changes: 3 additions & 1 deletion keepercommander/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,12 @@ def register_enterprise_commands(commands, aliases, command_info):
commands['risk-management'] = RiskManagementReportCommand()
command_info['risk-management'] = 'Risk Management Reports'
aliases['rmd'] = 'risk-management'

from . import device_management
device_management.register_enterprise_commands(commands)
device_management.register_enterprise_command_info(aliases, command_info)
from . import mc_transfer
commands['mc-transfer'] = mc_transfer.McTransferCommand()
command_info['mc-transfer'] = 'Managed Company Transfer'

from . import sso_cloud
sso_cloud.register_commands(commands)
Expand Down
Loading
Loading