Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release #1174

Merged
merged 10 commits into from
Jan 16, 2024
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: [email protected]
#

__version__ = '16.10.0'
__version__ = '16.10.1'
2 changes: 2 additions & 0 deletions keepercommander/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ def get_default_path():
params.commands.extend(params.config['commands'])
if 'plugins' in params.config:
params.plugins = params.config['plugins']
if params.config.get('debug') is True:
params.debug = True
except loader.SecureStorageException as sse:
logging.error('Unable to load configuration from secure storage:\n%s',
'\033[1m' + str(sse) + '\033[0m')
Expand Down
4 changes: 1 addition & 3 deletions keepercommander/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ def loop(params): # type: (KeeperParams) -> int
error_no = 0
suppress_errno = False

logging.getLogger().setLevel(logging.DEBUG if params.debug else logging.WARNING if params.batch_mode else logging.INFO)
enforcement_checked = set()
prompt_session = None
if not params.batch_mode:
Expand All @@ -345,9 +346,6 @@ def loop(params): # type: (KeeperParams) -> int
display.welcome()
versioning.welcome_print_version(params)

else:
logging.getLogger().setLevel(logging.DEBUG if params.debug else logging.WARNING)

if not params.batch_mode:
if params.user:
try:
Expand Down
41 changes: 39 additions & 2 deletions keepercommander/commands/automator.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#

import argparse
import datetime
import getpass
import logging
import os
Expand Down Expand Up @@ -130,10 +131,45 @@ def dump_automator(endpoint, status=False): # type: (automator_proto.Automato
logging.info('{0:>32s}: {1}'.format('Enabled', 'Yes' if endpoint.enabled else 'No'))
if status:
logging.info('{0:>32s}: {1}'.format('Initialized', 'Yes' if endpoint.status.initialized else 'No'))
if endpoint.status.initialized:
delta = datetime.datetime.now() - datetime.datetime.fromtimestamp (endpoint.status.initializedTimestamp//1000)
days = delta.days
seconds = delta.seconds
hours = seconds // 3600
uptime = ''
if days > 0:
uptime += f'{days} days '
if hours > 0:
uptime += f'{hours} hours'
else:
minutes = (seconds % 3600) // 60
if hours > 0:
uptime += f'{hours} hours'
if minutes > 0:
uptime += f' {minutes} minutes'
else:
uptime += f' {minutes} minutes'
logging.info('{0:>32s}: {1}'.format('Uptime', uptime))

if endpoint.status.sslCertificateExpiration > 0:
dt = datetime.datetime.fromtimestamp(endpoint.status.sslCertificateExpiration//1000)
logging.info('{0:>32}: {1}'.format('Certificate Expires', dt.strftime('%x')))

if endpoint.status.numberOfDevicesApproved > 0:
logging.info('{0:>32}: {1}'.format('Approved Devices', endpoint.status.numberOfDevicesApproved))
if endpoint.status.numberOfDevicesDenied > 0:
logging.info('{0:>32}: {1}'.format('Denied Devices', endpoint.status.numberOfDevicesDenied))

if endpoint.status.numberOfTeamsApproved > 0:
logging.info('{0:>32}: {1}'.format('Approved Teams', endpoint.status.numberOfTeamsApproved))
if endpoint.status.numberOfTeamsDenied > 0:
logging.info('{0:>32}: {1}'.format('Denied Teams', endpoint.status.numberOfTeamsDenied))

if endpoint.status.numberOfTeamMembershipsApproved > 0:
logging.info('{0:>32}: {1}'.format('Approved Team Memberships', endpoint.status.numberOfTeamMembershipsApproved))
if endpoint.status.numberOfTeamMembershipsDenied > 0:
logging.info('{0:>32}: {1}'.format('Denied Team Memberships', endpoint.status.numberOfTeamMembershipsDenied))

if endpoint.status.numberOfErrors > 0:
logging.info('{0:>32}: {1}'.format('Number of Errors', endpoint.status.numberOfErrors))

Expand Down Expand Up @@ -270,6 +306,7 @@ def execute(self, params, **kwargs): # type: (KeeperParams, **any) -> any
automator = self.find_automator(params, kwargs.get('target'))
rq = automator_proto.AdminEditAutomatorRequest()
rq.automatorId = automator.automatorId
rq.enabled = automator.enabled
name = kwargs['name']
if name:
rq.name = name
Expand Down Expand Up @@ -376,13 +413,13 @@ def execute(self, params, **kwargs): # type: (KeeperParams, **any) -> any
rq = automator_proto.AdminSetupAutomatorRequest()
rq.automatorId = automator.automatorId
rq.automatorState = automator_proto.NEEDS_CRYPTO_STEP_2
automator_public_key = crypto.load_ec_public_key(rs.automatorEcPublicKey)
automator_public_key = crypto.load_ec_public_key(rs.automatorEccPublicKey)
keys = params.enterprise['keys']
if 'ecc_encrypted_private_key' in keys:
encrypted_ec_private_key = utils.base64_url_decode(keys['ecc_encrypted_private_key'])
ec_private_key = crypto.decrypt_aes_v2(encrypted_ec_private_key, params.enterprise['unencrypted_tree_key'])
encrypted_ec_private_key = crypto.encrypt_ec(ec_private_key, automator_public_key)
rq.encryptedEcEnterprisePrivateKey = encrypted_ec_private_key
rq.encryptedEccEnterprisePrivateKey = encrypted_ec_private_key

if 'rsa_encrypted_private_key' in keys:
encrypted_rsa_private_key = utils.base64_url_decode(keys['rsa_encrypted_private_key'])
Expand Down
14 changes: 9 additions & 5 deletions keepercommander/commands/compliance.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,15 @@
access_report_desc = 'Run a report showing all records a user has accessed or can access'
access_report_parser = argparse.ArgumentParser(prog='compliance record-access-report', description=access_report_desc,
parents=[compliance_parser])
access_report_parser.add_argument('user', nargs='+', metavar='USER', type=str, help='username or ID')
report_type_help = 'select type of record-access data to include in report (defaults to "history")'
user_arg_help = 'username(s) or ID(s). Set to "@all" to run report for all users'
access_report_parser.add_argument('user', nargs='+', metavar='USER', type=str, help=user_arg_help)
report_type_help = ('select type of record-access data to include in report (defaults to "history"). '
'Set to "history" to view past record-access activity, "vault" to view current vault contents')
ACCESS_REPORT_TYPES = ('history', 'vault')
access_report_parser.add_argument('--report-type', action='store', choices=ACCESS_REPORT_TYPES,
default='history', help=report_type_help)
access_report_parser.add_argument('--aging', action='store_true', help='include record-aging data')
aging_help = 'include record-aging data (last modified, created, and last password rotation dates)'
access_report_parser.add_argument('--aging', action='store_true', help=aging_help)

summary_report_desc = 'Run a summary SOX compliance report'
summary_report_parser = argparse.ArgumentParser(prog='compliance summary-report', description=summary_report_desc,
Expand Down Expand Up @@ -438,6 +441,7 @@ def compile_user_report(user, access_events):
rec_owner = sox_data.get_record_owner(uid)
event_ts = access_event.get('last_created')
access_record = {uid: {'record_title': rec_info.get('title'),
'record_type': rec_info.get('record_type'),
'record_url': rec_info.get('url', '').rstrip('/'),
'record_owner': rec_owner and rec_owner.email,
'has_attachments': sox_rec.has_attachments if sox_rec else None,
Expand Down Expand Up @@ -573,8 +577,8 @@ def compile_report_data(rec_ids):
error_msg = f'Unrecognized report-type: "{report_type}"\nValues allowed: {ACCESS_REPORT_TYPES}'
raise CommandError(self.get_parser().prog, error_msg)

default_columns = ['vault_owner', 'record_uid', 'record_title', 'record_url', 'has_attachments', 'in_trash',
'record_owner', 'ip_address', 'device', 'last_access']
default_columns = ['vault_owner', 'record_uid', 'record_title', 'record_type', 'record_url', 'has_attachments',
'in_trash', 'record_owner', 'ip_address', 'device', 'last_access']

aging_columns = ['created', 'last_modified', 'last_rotation'] if aging else []
self.report_headers = default_columns + aging_columns
Expand Down
38 changes: 28 additions & 10 deletions keepercommander/commands/discoveryrotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
router_get_connected_gateways, router_set_record_rotation_information, router_get_rotation_schedules, \
get_router_url
from .record_edit import RecordEditMixin
from .tunnel.port_forward.endpoint import establish_symmetric_key, WebRTCConnection, TunnelEntrance, READ_TIMEOUT
from .tunnel.port_forward.endpoint import establish_symmetric_key, WebRTCConnection, TunnelEntrance, READ_TIMEOUT, \
find_open_port
from .. import api, utils, vault_extensions, vault, record_management, attachment, record_facades
from ..display import bcolors
from ..error import CommandError
Expand Down Expand Up @@ -1640,7 +1641,8 @@ def clean_up_tunnel(params, convo_id):
if params.tunnel_threads_queue.get(convo_id):
del params.tunnel_threads_queue[convo_id]
else:
print(f"{bcolors.WARNING}No tunnel data found to remove for {convo_id}{bcolors.ENDC}")
if params.debug:
print(f"{bcolors.WARNING}No tunnel data found to remove for {convo_id}{bcolors.ENDC}")


class PAMTunnelStopCommand(Command):
Expand All @@ -1659,6 +1661,7 @@ def execute(self, params, **kwargs):
if not tunnel_data:
raise CommandError('tunnel stop', f"No tunnel data to remove found for {convo_id}")
clean_up_tunnel(params, convo_id)

return


Expand Down Expand Up @@ -1733,7 +1736,7 @@ def retrieve_gateway_public_key(gateway_uid, params, api, utils) -> bytes:

class PAMTunnelStartCommand(Command):
pam_cmd_parser = argparse.ArgumentParser(prog='dr-port-forward-command')
pam_cmd_parser.add_argument('--gateway', '-g', required=False, dest='gateway', action='store',
pam_cmd_parser.add_argument('--gateway', '-g', required=True, dest='gateway', action='store',
help='Used to list all tunnels for the given Gateway UID')
pam_cmd_parser.add_argument('--record', '-r', required=True, dest='record_uid', action='store',
help='The Record UID of the PAM resource record with network information to use for '
Expand Down Expand Up @@ -1820,7 +1823,7 @@ async def connect(self, params, record_uid, convo_id, gateway_uid, host, port,
try:
await pc.signal_channel('start')
except Exception as e:
CommandError('tunnel start', f"{e}")
raise CommandError('Tunnel Start', f"{e}")

logger.debug("starting private tunnel")

Expand Down Expand Up @@ -1888,8 +1891,10 @@ def custom_exception_handler(loop, context):
print(f"{bcolors.FAIL}Socket not connected exception in connection {convo_id}: {es}{bcolors.ENDC}")
except KeyboardInterrupt:
print(f"{bcolors.OKBLUE}Exiting: {convo_id}{bcolors.ENDC}")
except CommandError as ce:
print(f"{bcolors.FAIL}{ce}{bcolors.ENDC}")
except Exception as e:
print(f"{bcolors.FAIL}An exception occurred in pre_connect for connection {convo_id}: {e}{bcolors.ENDC}")
print(f"{bcolors.FAIL}An exception occurred in connection {convo_id}: {e}{bcolors.ENDC}")
finally:
if loop:
try:
Expand Down Expand Up @@ -1918,7 +1923,9 @@ def custom_exception_handler(loop, context):
logging.debug(f"{bcolors.WARNING}Exception while stopping event loop: {e}{bcolors.ENDC}")
except Exception as e:
print(f"{bcolors.FAIL}An exception occurred in pre_connect for connection {convo_id}: {e}{bcolors.ENDC}")
print(f"{bcolors.OKBLUE}Tunnel {convo_id} closed.{bcolors.ENDC}")
finally:
clean_up_tunnel(params, convo_id)
print(f"{bcolors.OKBLUE}Tunnel {convo_id} closed.{bcolors.ENDC}")

def execute(self, params, **kwargs):
# https://pypi.org/project/aiortc/
Expand All @@ -1939,6 +1946,17 @@ def execute(self, params, **kwargs):
gateway_uid = kwargs.get('gateway')
host = kwargs.get('host')
port = kwargs.get('port')
if port is not None and port > 0:
try:
port = find_open_port(tried_ports=[], preferred_port=port, host=host)
except CommandError as e:
print(f"{bcolors.FAIL}{e}{bcolors.ENDC}")
return
else:
port = find_open_port(tried_ports=[], host=host)
if port is None:
print(f"{bcolors.FAIL}Could not find open port to use for tunnel{bcolors.ENDC}")
return

gateway_public_key_bytes = retrieve_gateway_public_key(gateway_uid, params, api, utils)

Expand Down Expand Up @@ -2007,14 +2025,14 @@ def execute(self, params, **kwargs):
time.sleep(.1)

def print_fail():
fail_dynamic_length = len("| Endpoint : failed to start..") + len(convo_id)
fail_dynamic_length = len("| Endpoint ") + len(convo_id) + len(" failed to start..")

clean_up_tunnel(params, convo_id)
time.sleep(.5)
# Dashed line adjusted to the length of the middle line
fail_dashed_line = '+' + '-' * fail_dynamic_length + '+'
print(f'\n{bcolors.FAIL}{fail_dashed_line}{bcolors.ENDC}')
print(f'{bcolors.FAIL}| Endpoint {convo_id}{bcolors.ENDC} failed to start..')
print(f'{bcolors.FAIL}| Endpoint {bcolors.ENDC}{convo_id}{bcolors.FAIL} failed to start..{bcolors.ENDC}')
print(f'{bcolors.FAIL}{fail_dashed_line}{bcolors.ENDC}\n')

if entrance is not None:
Expand All @@ -2028,7 +2046,7 @@ def print_fail():
host = host + ":" if host else ''
# Total length of the dynamic parts (endpoint name, host, and port)
dynamic_length = \
(len("| Endpoint : Listening on port: ") + len(convo_id) + len(host) + len(str(entrance.port)))
(len("| Endpoint : Listening on: ") + len(convo_id) + len(host) + len(str(entrance.port)))

# Dashed line adjusted to the length of the middle line
dashed_line = '+' + '-' * dynamic_length + '+'
Expand All @@ -2037,7 +2055,7 @@ def print_fail():
print(f'\n{bcolors.OKGREEN}{dashed_line}{bcolors.ENDC}')
print(
f'{bcolors.OKGREEN}| Endpoint {bcolors.ENDC}{bcolors.OKBLUE}{convo_id}{bcolors.ENDC}'
f'{bcolors.OKGREEN}: Listening on port: {bcolors.ENDC}'
f'{bcolors.OKGREEN}: Listening on: {bcolors.ENDC}'
f'{bcolors.BOLD}{bcolors.OKBLUE}{host}{entrance.port}{bcolors.ENDC}{bcolors.OKGREEN} |{bcolors.ENDC}')
print(f'{bcolors.OKGREEN}{dashed_line}{bcolors.ENDC}')
print(
Expand Down
16 changes: 12 additions & 4 deletions keepercommander/commands/scim.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import os
import requests
from typing import Iterable, Union, Optional, Dict, List
from urllib.parse import urlparse, urlunparse
from urllib.parse import urlparse, urlunparse, parse_qsl, urlencode

from .base import user_choice, dump_report_data, report_output_parser, field_to_title, GroupCommand
from .enterprise import TeamApproveCommand, EnterpriseCommand
Expand Down Expand Up @@ -695,13 +695,21 @@ def delete_scim_resource(url, resource_id, token, dry_run=False):
@staticmethod
def get_scim_resource(url, token):
resources = []
start_index = 0
start_index = 1
count = 500
headers = {
'Authorization': f'Bearer {token}'
}
comps = urlparse(url)

while True:
headers['startIndex'] = str(start_index)
rs = requests.get(url, headers=headers)
q = parse_qsl(comps.query, keep_blank_values=True)
q.append(('startIndex', str(start_index)))
q.append(('count', str(count)))
query = urlencode(q, doseq=True)
url_comp = (comps.scheme, comps.netloc, comps.path, None, query, None)
rq_url = urlunparse(url_comp)
rs = requests.get(rq_url, headers=headers)
if rs.status_code != 200:
raise Exception(f'SCIM GET error code "{rs.status_code}"')
response = rs.json()
Expand Down
Loading
Loading