Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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__ = '18.0.15'
__version__ = '18.1.0'
2 changes: 1 addition & 1 deletion keepercommander/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1530,7 +1530,7 @@ def login_and_get_mc_params_login_v3(params: KeeperParams, mc_id):
mc_params.rsa_key = params.rsa_key
mc_params.rsa_key2 = params.rsa_key2
mc_params.ecc_key = params.ecc_key
mc_params.forbid_rsa = params.forbid_rsa
mc_params.forbid_rsa = resp.forbidKeyType2

mc_params.session_token = loginv3.CommonHelperMethods.bytes_to_url_safe_str(resp.encryptedSessionToken)
mc_params.msp_tree_key = params.enterprise['unencrypted_tree_key']
Expand Down
2 changes: 1 addition & 1 deletion keepercommander/command_categories.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
'Service Mode REST API': {
'service-create', 'service-add-config', 'service-start', 'service-stop', 'service-status',
'service-config-add', 'service-docker-setup', 'slack-app-setup', 'teams-app-setup',
'sailpoint-app-setup'
'sailpoint-app-setup', 'gchat-app-setup'
},

# Email Configuration Commands
Expand Down
3 changes: 2 additions & 1 deletion keepercommander/commands/aram.py
Original file line number Diff line number Diff line change
Expand Up @@ -1212,7 +1212,8 @@ def __init__(self):

def get_sox_data(self, params):
if not self.sox_data and is_compliance_reporting_enabled(params):
self.sox_data = get_compliance_data(params, 0, 0,False, min_updated=0, no_cache=False)
min_updated = int(datetime.datetime.now().timestamp()) if self.allow_sox_data_fetch else 0
self.sox_data = get_compliance_data(params, 0, 0, False, min_updated=min_updated, no_cache=False)
return self.sox_data

def get_value(self, params, field, event):
Expand Down
15 changes: 6 additions & 9 deletions keepercommander/commands/connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,15 +194,12 @@ def get_parameter_value(params, record, parameter, temp_files, **kwargs):
@staticmethod
def get_command_string(params, record, template, temp_files, **kwargs):
# type: (KeeperParams, KeeperRecord, str, list, ...) -> str or None
command = template
while True:
m = endpoint_parameter_pattern.search(command)
if not m:
break
p = m.group(1)
pv = BaseConnectCommand.get_parameter_value(params, record, p, temp_files, **kwargs)
command = command[:m.start()] + (pv or '') + command[m.end():]
return command
# Single-pass substitution: re.sub never re-scans replacement text, so
# a field value containing ${…} cannot trigger a second expansion round.
def _replace(m):
pv = BaseConnectCommand.get_parameter_value(params, record, m.group(1), temp_files, **kwargs)
return pv or ''
return endpoint_parameter_pattern.sub(_replace, template)


class ConnectSshCommand(BaseConnectCommand):
Expand Down
36 changes: 36 additions & 0 deletions keepercommander/commands/connect_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@

_DANGEROUS_ENV_PREFIXES = ('LD_', 'DYLD_')

# SSH -o options that cause local code execution via the SSH client itself.
_DANGEROUS_SSH_OPTIONS = frozenset({
'proxycommand',
'localcommand',
'permitlocalcommand',
})

_INTERPRETER_DASH_C_FLAGS = (
'-c', '-lc', '-ic', '-Command', '-EncodedCommand',
'-e', '-E', '/c', '/C',
Expand Down Expand Up @@ -102,6 +109,29 @@ def _is_dangerous_env_name(name: Optional[str]) -> bool:
return upper in _DANGEROUS_ENV_NAMES or upper.startswith(_DANGEROUS_ENV_PREFIXES)


def _dangerous_ssh_option(argv: List[str]) -> Optional[str]:
"""Return the canonical option name if argv contains an SSH flag that causes
local code execution (ProxyCommand, LocalCommand, PermitLocalCommand).
Handles both merged (-oProxyCommand=…) and split (-o ProxyCommand=…) forms.
Returns None if no dangerous option is found.
"""
i = 0
while i < len(argv):
token = argv[i]
# Merged form: -oProxyCommand=value or -OLocalCommand=value
if len(token) > 2 and token[0] == '-' and token[1] in ('o', 'O'):
opt_name = token[2:].split('=', 1)[0]
if opt_name.lower() in _DANGEROUS_SSH_OPTIONS:
return opt_name
# Split form: -o ProxyCommand=value
elif token in ('-o', '-O') and i + 1 < len(argv):
opt_name = argv[i + 1].split('=', 1)[0]
if opt_name.lower() in _DANGEROUS_SSH_OPTIONS:
return opt_name
i += 1
return None


def _looks_multi_statement(arg: str) -> bool:
return (
len(arg) > _LONG_ARG_THRESHOLD
Expand Down Expand Up @@ -243,6 +273,12 @@ def confirm_argv(stage: str, argv: List[str], record: Any = None) -> bool:
warning_lines.append(
'argv[0] is a shell/scripting interpreter - it can run any code.'
)
dangerous_opt = _dangerous_ssh_option(argv)
if dangerous_opt is not None:
warning_lines.append(
f'{_sanitize(dangerous_opt)} is set — SSH will execute a local command'
' on this machine with your privileges.'
)
_emit_warning(warning_lines)
_emit_section('Command to execute:', _argv_block(argv))
sys.stderr.write('\n')
Expand Down
21 changes: 17 additions & 4 deletions keepercommander/commands/enterprise.py
Original file line number Diff line number Diff line change
Expand Up @@ -797,17 +797,24 @@ def tree_node(node):
elif column == 'transfer_status':
row.append(user_status_dict['acct_transfer_status'])
elif column == 'node':
node_path = self.get_node_path(params, u['node_id'])
if is_verbose:
row.append(str(u['node_id']))
row.append({
'node_id': str(u['node_id']),
'node_name': node_path,
})
else:
row.append(self.get_node_path(params, u['node_id']))
row.append(node_path)
elif column == 'team_count':
row.append(len([1 for t in teams.values() if t['users'] and user_id in t['users']]))
elif column == 'teams':
user_team_list = [t for t in teams.values()
if t['users'] and user_id in t['users']]
if is_verbose:
row.append([t['id'] for t in user_team_list])
row.append([
{'team_uid': t['id'], 'team_name': t['name']}
for t in user_team_list
])
else:
row.append([t['name'] for t in user_team_list])
elif column == 'role_count' or column == 'roles':
Expand All @@ -822,7 +829,13 @@ def tree_node(node):
row.append(len(role_ids))
else:
if is_verbose:
row.append([str(role_id) for role_id in role_ids if role_id in roles])
row.append([
{
'role_id': str(role_id),
'role_name': roles[role_id]['name'],
}
for role_id in role_ids if role_id in roles
])
else:
role_names = [roles[role_id]['name'] for role_id in role_ids if role_id in roles]
row.append(role_names)
Expand Down
9 changes: 7 additions & 2 deletions keepercommander/commands/start_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
from ..service.commands.handle_service import StartService, StopService, ServiceStatus
from ..service.commands.service_docker_setup import ServiceDockerSetupCommand
from ..service.commands.integrations import (
SlackAppSetupCommand, TeamsAppSetupCommand, SailPointAppSetupCommand,
GChatAppSetupCommand,
SlackAppSetupCommand,
TeamsAppSetupCommand,
SailPointAppSetupCommand,
)

def register_commands(commands):
Expand All @@ -27,6 +30,7 @@ def register_commands(commands):
commands['slack-app-setup'] = SlackAppSetupCommand()
commands['teams-app-setup'] = TeamsAppSetupCommand()
commands['sailpoint-app-setup'] = SailPointAppSetupCommand()
commands['gchat-app-setup'] = GChatAppSetupCommand()

def register_command_info(aliases, command_info):
service_classes = [
Expand All @@ -39,9 +43,10 @@ def register_command_info(aliases, command_info):
SlackAppSetupCommand,
TeamsAppSetupCommand,
SailPointAppSetupCommand,
GChatAppSetupCommand,
]

for service_class in service_classes:
parser = service_class()
p = parser.get_parser()
command_info[p.prog] = p.description
command_info[p.prog] = p.description
12 changes: 8 additions & 4 deletions keepercommander/enterprise.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,14 @@ def load(self, params): # type: (KeeperParams) -> None
rq.enterprisePublicKey = rsa_public_key
rq.encryptedEnterprisePrivateKey = rsa_encrypted_private_key
rq.keyType = proto.RSA
api.communicate_rest(params, rq, 'enterprise/set_enterprise_key_pair')
self._enterprise._rsa_key = rsa_private_key
keys['rsa_public_key'] = utils.base64_url_encode(rsa_public_key)
keys['rsa_encrypted_private_key'] = utils.base64_url_encode(rsa_encrypted_private_key)
try:
api.communicate_rest(params, rq, 'enterprise/set_enterprise_key_pair')
self._enterprise._rsa_key = rsa_private_key
keys['rsa_public_key'] = utils.base64_url_encode(rsa_public_key)
keys['rsa_encrypted_private_key'] = utils.base64_url_encode(rsa_encrypted_private_key)
except:
logging.info('Failed to set enterprise RSA key')
pass

if 'ecc_encrypted_private_key' not in keys:
ec_private, ec_public = crypto.generate_ec_key()
Expand Down
8 changes: 2 additions & 6 deletions keepercommander/importer/keepass/keepass.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from typing import Dict
from xml.sax.saxutils import escape

from pykeepass import PyKeePass
from pykeepass import PyKeePass, create_database
from pykeepass.exceptions import CredentialsError
from pykeepass.attachment import Attachment as KeepassAttachment
from pykeepass.group import Group
Expand Down Expand Up @@ -316,11 +316,7 @@ def do_export(self, filename, records, file_password=None, kbdx_key_file=None, *
elif isinstance(x, SharedFolder):
sfs.append(x)

template_file = os.path.join(os.path.dirname(__file__), 'template.kdbx')

with PyKeePass(template_file, password='111111') as kdb:
kdb.password = password
kdb.keyfile = keyfile
with create_database(filename, password=password, keyfile=keyfile) as kdb:
root = kdb.root_group

for r in rs:
Expand Down
Binary file removed keepercommander/importer/keepass/template.kdbx
Binary file not shown.
2 changes: 1 addition & 1 deletion keepercommander/loginv3.py
Original file line number Diff line number Diff line change
Expand Up @@ -1155,7 +1155,7 @@ def accountSummary(params: KeeperParams):
return api.communicate_rest(params, rq, 'login/account_summary', rs_type=AccountSummary_pb2.AccountSummaryElements)

@staticmethod
def loginToMc(rest_context, session_token, mc_id):
def loginToMc(rest_context, session_token, mc_id): # type: (Any, str, int) -> enterprise_pb2.LoginToMcResponse

endpoint = 'authentication/login_to_mc'

Expand Down
5 changes: 4 additions & 1 deletion keepercommander/plugins/adpasswd/adpasswd.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ def rotate(record, new_password): # type: (KeeperRecord, str) -> bool
if not login and not user_dn:
raise ValueError(f'Rotate AD password: User login or DN is not set.')

tls = ldap3.Tls(validate=ssl.CERT_NONE)
_tls_validate_map = {'none': ssl.CERT_NONE, 'optional': ssl.CERT_OPTIONAL, 'required': ssl.CERT_REQUIRED}
tls_validate_str = (RecordMixin.get_record_field(record, 'cmdr:tls_verify') or 'required').lower()
tls_validate = _tls_validate_map.get(tls_validate_str, ssl.CERT_REQUIRED)
tls = ldap3.Tls(validate=tls_validate)
server = ldap3.Server(host=host, port=port, use_ssl=True, tls=tls, connect_timeout=5, get_info=ldap3.ALL)
with ldap3.Connection(server) as c:
c.open()
Expand Down
6 changes: 4 additions & 2 deletions keepercommander/plugins/mysql/mysql.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,13 @@ def rotate(self, record, new_password, revert=False):
is_old_version = vn < 5007006
except ValueError:
pass
escape_login = pymysql.converters.escape_string(self.login)
escape_user_host = pymysql.converters.escape_string(self.user_host)
escape_new_password = pymysql.converters.escape_string(new_password)
if is_old_version:
sql = f"set password for '{self.login}'@'{self.user_host}' = password('{escape_new_password}')"
sql = f"set password for '{escape_login}'@'{escape_user_host}' = password('{escape_new_password}')"
else:
sql = f"alter user '{self.login}'@'{self.user_host}' identified by '{escape_new_password}'"
sql = f"alter user '{escape_login}'@'{escape_user_host}' identified by '{escape_new_password}'"
cursor.execute(sql)
return True
except pymysql.err.OperationalError as e:
Expand Down
4 changes: 3 additions & 1 deletion keepercommander/plugins/oracle/oracle.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ def rotate(self, record, new_password, revert=False):
connection = oracledb.connect(**kwargs)
with connection.cursor() as cursor:
logging.debug(f'Connected to {dsn}')
sql = f'ALTER USER {user} IDENTIFIED BY "{new_password}" ACCOUNT UNLOCK'
quoted_user = '"' + user.replace('"', '""') + '"'
escaped_password = new_password.replace('"', '""')
sql = f'ALTER USER {quoted_user} IDENTIFIED BY "{escaped_password}" ACCOUNT UNLOCK'
cursor.execute(sql)
result = True
except Exception as e:
Expand Down
5 changes: 4 additions & 1 deletion keepercommander/plugins/postgresql/postgresql.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#

import psycopg2
import psycopg2.sql
import logging

"""Commander Plugin for Postgres Database Server
Expand Down Expand Up @@ -51,7 +52,9 @@ def rotate(self, record, new_password, revert=False):
database=self.db) as connection:
logging.debug(f'Connected to {self.host}')
with connection.cursor() as cursor:
sql = f'alter user {self.login} with password %s'
sql = psycopg2.sql.SQL('ALTER USER {} WITH PASSWORD %s').format(
psycopg2.sql.Identifier(self.login)
)
cursor.execute(sql, (new_password,))
return True
except Exception as e:
Expand Down
6 changes: 4 additions & 2 deletions keepercommander/rest_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from . import crypto, utils
from cryptography.hazmat.primitives.asymmetric import rsa, ec

CLIENT_VERSION = 'c18.0.0'
CLIENT_VERSION = 'c18.1.0'

SERVER_PUBLIC_KEYS = {
1: crypto.load_rsa_public_key(utils.base64_url_decode(
Expand Down Expand Up @@ -227,6 +227,8 @@ def execute_rest(context, endpoint, payload, timeout=None):
elif rs.status_code >= 400:
if content_type.startswith('application/json'):
failure = rs.json()
if isinstance(failure, list):
failure = failure[0] if failure else {}
logging.debug('<<< Response Error: [%s]', failure)
if rs.status_code == 401:
if failure.get('error') == 'key':
Expand All @@ -252,7 +254,7 @@ def execute_rest(context, endpoint, payload, timeout=None):
context.server_key_id = server_key_id
run_request = True
continue
elif rs.status_code == 403:
elif rs.status_code in (403, 429):
if failure.get('error') == 'throttled' and not context.fail_on_throttle:
throttle_retries += 1
if throttle_retries > max_throttle_retries:
Expand Down
8 changes: 6 additions & 2 deletions keepercommander/rsync/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def get_parser(self):

def execute(self, params, **kwargs):
local_path = kwargs.get('local_path')
if not local_path:
if not isinstance(local_path, str) or not local_path:
self.get_parser().print_help()
return

Expand Down Expand Up @@ -187,9 +187,13 @@ def execute(self, params, **kwargs):

if len(to_download) > 0:
logging.info('Downloading %d file(s):', len(to_download))
safe_root = os.path.realpath(local_path)
verified_folders = set()
for file in to_download:
absolute_path = os.path.join(local_path, file.path)
absolute_path = os.path.realpath(os.path.join(local_path, file.path))
if not (absolute_path == safe_root or absolute_path.startswith(safe_root + os.sep)):
logging.warning(f'Skipping entry with path outside sync root: {file.path}')
continue
logging.info(absolute_path)
folder_name = os.path.dirname(absolute_path)
if folder_name not in verified_folders:
Expand Down
Loading
Loading