diff --git a/keepercommander/commands/record.py b/keepercommander/commands/record.py index 99355974c..7977d0ab5 100644 --- a/keepercommander/commands/record.py +++ b/keepercommander/commands/record.py @@ -635,6 +635,41 @@ def _format_expiration(expiration_value): 'value': r.notes, } fields.append(field) + # custom fields (v2 type in c['type']; v3 encode type as "type:label" in c['name']) + # Keep _MASKED_FIELD_TYPES in sync with _MASKED_TYPES in record.py. + # 'note' = Secured Note — sensitive, masked by design. + # 'passkey' omitted: early-exit handler renders only non-sensitive sub-fields. + _MASKED_FIELD_TYPES = frozenset({ + 'secret', 'pinCode', 'note', 'json', 'oneTimeCode', + 'paymentCard', 'bankAccount', 'keyPair', 'securityQuestion', + }) + unmask = kwargs.get('unmask') is True + for cf in (r.custom_fields or []): + cf_value = cf.get('value') + if not cf_value and cf_value != 0: + continue + cf_name = str(cf.get('name') or cf.get('type') or '') + is_sensitive = (cf.get('type') in _MASKED_FIELD_TYPES or + cf_name.split(':')[0] in _MASKED_FIELD_TYPES) + is_sq = (cf.get('type') == 'securityQuestion' or + cf_name == 'securityQuestion' or + cf_name.startswith('securityQuestion:')) + if is_sq: + val = cf_value + entry = val[0] if (isinstance(val, list) and val) else val + if isinstance(entry, dict): + display_val = { + 'question': entry.get('question') or '', + 'answer': (entry.get('answer') or '') if unmask else '********', + } + else: + display_val = cf_value if unmask else '********' + else: + display_val = cf_value if (unmask or not is_sensitive) else '********' + fields.append({ + 'name': cf_name, + 'value': display_val, + }) print(json.dumps(fields, indent=2)) else: diff --git a/keepercommander/commands/record_edit.py b/keepercommander/commands/record_edit.py index 286b0527c..9781bf431 100644 --- a/keepercommander/commands/record_edit.py +++ b/keepercommander/commands/record_edit.py @@ -1035,7 +1035,7 @@ def _sync_password_to_pam(self, params: KeeperParams, record: vault.TypedRecord, if plugin_name == 'azureadpwd': # Import Azure AD plugin - from ...plugins.azureadpwd import azureadpwd + from ..plugins.azureadpwd import azureadpwd # Call the rotate function with PAM config record success = azureadpwd.rotate(pam_record, password) @@ -1047,7 +1047,7 @@ def _sync_password_to_pam(self, params: KeeperParams, record: vault.TypedRecord, elif plugin_name == 'awspswd': # Import AWS plugin and common rotator - from ...plugins.awspswd import aws_passwd + from ..plugins.awspswd import aws_passwd # Extract AWS credentials from PAM config aws_access_key = None @@ -1078,7 +1078,6 @@ def _sync_password_to_pam(self, params: KeeperParams, record: vault.TypedRecord, # Set AWS credentials in environment or use profile if aws_access_key and aws_secret_key: - import os original_access_key = os.environ.get('AWS_ACCESS_KEY_ID') original_secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY') @@ -1145,7 +1144,6 @@ def _send_onboarding_email( # Load email configuration from .email_commands import find_email_config_record, load_email_config_from_record from ..email_service import EmailSender, build_onboarding_email - from .helpers.timeout import parse_timeout config_uid = find_email_config_record(params, email_config_name) if not config_uid: @@ -1170,7 +1168,7 @@ def _send_onboarding_email( else: # minutes minutes = expire_seconds // 60 expiration_text = f"{minutes} minute{'s' if minutes > 1 else ''}" - except: + except Exception: expiration_text = expiration # fallback to original if parsing fails html_body = build_onboarding_email( diff --git a/keepercommander/record.py b/keepercommander/record.py index 897180003..189e3ccf0 100644 --- a/keepercommander/record.py +++ b/keepercommander/record.py @@ -275,6 +275,15 @@ def display(self, unmask=False): # Strip type prefixes from field names (e.g., "text:Sign-In Address" -> "Sign-In Address") field_type_prefixes = ('text:', 'multiline:', 'url:', 'phone:', 'email:', 'secret:', 'date:', 'name:', 'host:', 'address:') display_name = field_name + # v2 records carry the real type in c['type']; v3 encode it as "type:label" in the name. + # Keep this list in sync with _MASKED_FIELD_TYPES in commands/record.py. + # 'note' = Secured Note — sensitive, masked by design. + # 'passkey' omitted: early-exit handler above renders only non-sensitive sub-fields. + _MASKED_TYPES = frozenset({ + 'secret', 'pinCode', 'note', 'json', 'oneTimeCode', + 'paymentCard', 'bankAccount', 'keyPair', 'securityQuestion', + }) + is_secret_field = c.get('type') in _MASKED_TYPES for prefix in field_type_prefixes: if field_name.lower().startswith(prefix): display_name = field_name[len(prefix):] @@ -293,8 +302,26 @@ def display(self, unmask=False): 'address:': 'Address', } display_name = type_friendly_names.get(prefix, prefix.rstrip(':').title()) + if prefix.rstrip(':') in _MASKED_TYPES: + is_secret_field = True break - print('{0:>20s}: {1:20s}: {1:20s}: {1: