From 358da0c6ac47ecbf0d6c3081b3a52709caeac666 Mon Sep 17 00:00:00 2001 From: Craig Date: Fri, 3 Jul 2026 13:35:24 -0700 Subject: [PATCH 1/4] fix: Fall back to manual token entry when SSO clipboard is empty or inaccessible When using Commander over SSH, pyperclip cannot access the local browser's clipboard. Selecting 'p' now prompts for manual token input if the clipboard is unreadable or returns content too short to be a valid SSO token. Co-Authored-By: Claude Sonnet 4.6 --- keepercommander/auth/console_ui.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/keepercommander/auth/console_ui.py b/keepercommander/auth/console_ui.py index eca6bf3b4..0af2ccf58 100644 --- a/keepercommander/auth/console_ui.py +++ b/keepercommander/auth/console_ui.py @@ -387,7 +387,11 @@ def on_sso_redirect(self, step): token = pyperclip.paste() except: token = '' - logging.warning('Failed to paste from clipboard') + if len(token) < 10: + try: + token = input(f'{Fore.GREEN}Paste SSO Token: {Fore.RESET}').strip() + except KeyboardInterrupt: + token = '' else: if len(token) < 10: logging.warning(f'Unsupported menu option: {token}') From d2a71e20c25c66a244ef2ac8203a62528258c15c Mon Sep 17 00:00:00 2001 From: sshrushanth-ks Date: Tue, 7 Jul 2026 19:55:56 +0530 Subject: [PATCH 2/4] Fix pam config list dropping seconds from 6-part Quartz CRON (#2191) (#2192) --- keepercommander/vault.py | 3 +-- unit-tests/test_importer.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/keepercommander/vault.py b/keepercommander/vault.py index 0fa854f82..52a16489f 100644 --- a/keepercommander/vault.py +++ b/keepercommander/vault.py @@ -475,8 +475,7 @@ def export_schedule_field(value): # type: (dict) -> Optional[str] cron = value.get('cron') if isinstance(cron, str): comps = [x for x in cron.split(' ') if x] - if len(comps) >= 6: - comps = comps[1:6] + if comps: return ' '.join(comps) return '' diff --git a/unit-tests/test_importer.py b/unit-tests/test_importer.py index 19217776a..aaa203611 100644 --- a/unit-tests/test_importer.py +++ b/unit-tests/test_importer.py @@ -230,3 +230,13 @@ def test_schedule_parser(self): self.assertEqual(sc.get('type'), 'MONTHLY_BY_WEEKDAY') self.assertEqual(sc.get('weekday'), 'WEDNESDAY') self.assertEqual(sc.get('occurrence'), 'SECOND') + + def test_export_schedule_field_preserves_quartz_cron_seconds(self): + schedule = {"type": "CRON", "cron": "1 5 1 * * ?", "tz": "Etc/UTC"} + self.assertEqual(vault.TypedField.export_schedule_field(schedule), '1 5 1 * * ?') + + schedule = {"type": "CRON", "cron": "0 5 1 * * ?"} + self.assertEqual(vault.TypedField.export_schedule_field(schedule), '0 5 1 * * ?') + + schedule = {"type": "CRON", "cron": "5 1 * * ?"} + self.assertEqual(vault.TypedField.export_schedule_field(schedule), '5 1 * * ?') From 68bbfd7491c3e469e457ee73ee0e6520b85cf1fd Mon Sep 17 00:00:00 2001 From: Sergey Kolupaev Date: Tue, 7 Jul 2026 16:56:56 -0700 Subject: [PATCH 3/4] Release 18.0.11 --- keepercommander/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keepercommander/__init__.py b/keepercommander/__init__.py index ded16103e..0c9cce431 100644 --- a/keepercommander/__init__.py +++ b/keepercommander/__init__.py @@ -10,4 +10,4 @@ # Contact: commander@keepersecurity.com # -__version__ = '18.0.10' +__version__ = '18.0.11' From 257b4757020e4b3c7ccfe4ced7dcfe5cd32a8cba Mon Sep 17 00:00:00 2001 From: Sergey Kolupaev Date: Tue, 7 Jul 2026 10:18:55 -0700 Subject: [PATCH 4/4] Fix import command/only update if same folder --- keepercommander/importer/imp_exp.py | 43 ++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/keepercommander/importer/imp_exp.py b/keepercommander/importer/imp_exp.py index 38703a9dc..48b29176f 100644 --- a/keepercommander/importer/imp_exp.py +++ b/keepercommander/importer/imp_exp.py @@ -1796,7 +1796,7 @@ def value_to_token(value): # type: (any) -> str return str(value) -def tokenize_record_key(record): # type: (ImportRecord) -> Iterator[str] +def tokenize_record_key(record, folder): # type: (ImportRecord, str) -> Iterator[str] """ Turn a record-to-import into an iterable of str's for hashing. This is really about import --update. @@ -1807,6 +1807,7 @@ def tokenize_record_key(record): # type: (ImportRecord) -> Iterator[str] yield f'$title:{record.title or ""}' yield f'$login:{record.login or ""}' yield f'$url:{record.login_url or ""}' + yield f'$folder:{folder}' if record_type in {'', 'login'}: return @@ -2071,7 +2072,7 @@ def prepare_record_add_or_update(update_flag, params, records): If a 100% match is found for a record, then just skip requesting anything; it doesn't need to be changed. Otherwise import the record, risking creating an almost-duplicate. If update_flag is True: - if a unique field match (on title, login, and url) is found, then request a change in password only. + if a unique field match (on title, login, url, and folder) is found, then request a change in password only. """ preexisting_entire_record_hash = {} preexisting_partial_record_hash = {} @@ -2081,8 +2082,13 @@ def prepare_record_add_or_update(update_flag, params, records): record_hash = build_record_hash(tokenize_full_import_record(import_record)) preexisting_entire_record_hash[record_hash] = record_uid if update_flag: - record_hash = build_record_hash(tokenize_record_key(import_record)) - preexisting_partial_record_hash[record_hash] = record_uid + folders = [get_folder_path(params, x) for x in find_folders(params, record_uid)] + folders = [x for x in folders if x] + if len(folders) == 0: + folders.append(record_uid) + for folder in folders: + record_hash = build_record_hash(tokenize_record_key(import_record, folder)) + preexisting_partial_record_hash[record_hash] = record_uid else: pass @@ -2128,15 +2134,26 @@ def prepare_record_add_or_update(update_flag, params, records): record_to_import.append(import_record) continue elif update_flag: - record_hash = build_record_hash(tokenize_record_key(import_record)) - if record_hash in preexisting_partial_record_hash: - record_uid = preexisting_partial_record_hash[record_hash] - if import_record.uid: - external_lookup[import_record.uid] = record_uid - if record_uid not in record_uid_to_update: - record_uid_to_update.add(record_uid) - import_record.uid = record_uid - record_to_import.append(import_record) + folders = [] + if import_record.folders: + folders.extend([x.get_folder_path() for x in import_record.folders]) + folders = [x for x in folders if x] + if len(folders) == 0: + folders.append('') + found = False + for folder in folders: + record_hash = build_record_hash(tokenize_record_key(import_record, folder)) + if record_hash in preexisting_partial_record_hash: + record_uid = preexisting_partial_record_hash[record_hash] + if import_record.uid: + external_lookup[import_record.uid] = record_uid + if record_uid not in record_uid_to_update: + record_uid_to_update.add(record_uid) + import_record.uid = record_uid + record_to_import.append(import_record) + found = True + break + if found: continue record_uid = utils.generate_uid()