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
745 changes: 745 additions & 0 deletions KEEPER_DRIVE_COMMANDS.md

Large diffs are not rendered by default.

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__ = '17.2.16'
__version__ = '18.0.0'
2 changes: 1 addition & 1 deletion keepercommander/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ def get_record(params, record_uid):
try:
rec = Record(record_uid)
data = json.loads(cached_rec['data_unencrypted'])
extra = json.loads(cached_rec['extra_unencrypted']) if 'extra_unencrypted' in cached_rec else None
extra = json.loads(cached_rec['extra_unencrypted']) if cached_rec.get('extra_unencrypted') else None
rec.load(data, version=version, revision=cached_rec['revision'], extra=extra)
if not resolve_record_view_path(params, record_uid):
rec.mask_password()
Expand Down
7 changes: 6 additions & 1 deletion keepercommander/autocomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
from .commands.folder import mv_parser
from .commands.base import GroupCommand, Command
from .commands.connect import ConnectCommand
from .command_categories import COMMAND_CATEGORIES
from .commands import commands, enterprise_commands, msp_commands

_KEEPER_DRIVE_COMMANDS = COMMAND_CATEGORIES.get('KeeperDrive Commands', set())
from .subfolder import try_resolve_path as sf_try_resolve_path


Expand Down Expand Up @@ -132,7 +135,9 @@ def get_completions(self, document, complete_event):
if document.is_cursor_at_the_end:
pos = document.text.find(' ')
if pos == -1:
cmds = [x for x in commands if x.startswith(document.text)]
hide_kd = self.params.is_feature_disallowed('keeper_drive')
cmds = [x for x in commands
if x.startswith(document.text) and not (hide_kd and x in _KEEPER_DRIVE_COMMANDS)]
if self.aliases:
al_cmds = [x[0] for x in self.aliases.items() if type(x[1]) == tuple and x[0].startswith(document.text)]
cmds.extend(al_cmds)
Expand Down
22 changes: 18 additions & 4 deletions keepercommander/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,13 @@
from .commands.utils import LoginCommand
from .commands import msp
from .constants import OS_WHICH_CMD, KEEPER_PUBLIC_HOSTS, KEEPER_SERVERS
from .command_categories import COMMAND_CATEGORIES
from .error import CommandError, Error
from .params import KeeperParams
from .subfolder import BaseFolderNode

KEEPER_DRIVE_COMMANDS = COMMAND_CATEGORIES.get('KeeperDrive Commands', set())

current_command = None # type: Union[None, CliCommand]
stack = []
register_commands(commands, aliases, command_info)
Expand All @@ -68,7 +71,7 @@
logging.getLogger('asyncio').setLevel(logging.WARNING)


def display_command_help(show_enterprise=False, show_shell=False, show_legacy=False):
def display_command_help(show_enterprise=False, show_shell=False, show_legacy=False, show_keeper_drive=True):
from .command_categories import get_command_category, get_category_order
from .display import bcolors
from colorama import Fore, Style
Expand Down Expand Up @@ -141,6 +144,8 @@ def clean_description(desc):
continue
if category == 'Legacy Commands' and not show_legacy:
continue
if category == 'KeeperDrive Commands' and not show_keeper_drive:
continue

if category == 'KeeperPAM Commands':
for cmd_display, description in sorted(pam_subcommands):
Expand Down Expand Up @@ -380,7 +385,9 @@ def is_msp(params_local):
else:
cmd = ali

if cmd in commands or cmd in enterprise_commands or cmd in msp_commands:
is_kd_hidden = cmd in KEEPER_DRIVE_COMMANDS and params.is_feature_disallowed('keeper_drive')

if not is_kd_hidden and (cmd in commands or cmd in enterprise_commands or cmd in msp_commands):
command = commands.get(cmd) or enterprise_commands.get(cmd) or msp_commands.get(cmd)
global current_command
current_command = command
Expand Down Expand Up @@ -431,7 +438,10 @@ def is_msp(params_local):
else:
if not params.session_token and utils.is_email(orig_cmd):
return LoginCommand().execute(params, email=orig_cmd, new_login=False)
display_command_help(show_enterprise=(params.enterprise is not None))
display_command_help(
show_enterprise=(params.enterprise is not None),
show_keeper_drive=not params.is_feature_disallowed('keeper_drive')
)


def runcommands(params, commands=None, command_delay=0, quiet=False):
Expand Down Expand Up @@ -856,7 +866,11 @@ def get_prompt(params):
break

if f.parent_uid is not None:
f = params.folder_cache[f.parent_uid]
if f.parent_uid in params.folder_cache:
f = params.folder_cache[f.parent_uid]
else:
# Parent UID not in folder_cache (e.g., KD folders with special root UID)
f = params.root_folder
else:
if f.type == BaseFolderNode.SharedFolderFolderType:
f = params.folder_cache[f.shared_folder_uid]
Expand Down
9 changes: 9 additions & 0 deletions keepercommander/command_categories.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@
'epm'
},

# KeeperDrive Commands
'KeeperDrive Commands': {
'kd-mkdir', 'kd-record-add', 'kd-record-update', 'kd-rndir', 'kd-list',
'kd-share-folder', 'kd-record-details', 'kd-share-record',
'kd-record-permission', 'kd-transfer-record',
'kd-ln', 'kd-rm', 'kd-rmdir', 'kd-shortcut', 'kd-get'
},

# Legacy Commands
'Legacy Commands': {
'rotate', 'connect', 'ssh', 'ssh-agent', 'rdp', 'rsync', 'set', 'echo',
Expand Down Expand Up @@ -145,5 +153,6 @@ def get_category_order():
'Miscellaneous Commands',
'KeeperPAM Commands',
'EPM Commands',
'KeeperDrive Commands',
'Legacy Commands'
]
4 changes: 4 additions & 0 deletions keepercommander/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ def register_commands(commands, aliases, command_info):
device_management.register_commands(commands)
device_management.register_command_info(aliases, command_info)

from . import keeper_drive
keeper_drive.register_commands(commands)
keeper_drive.register_command_info(aliases, command_info)

if sys.version_info.major == 3 and sys.version_info.minor >= 10 and (utils.is_windows_11() or sys.platform == 'darwin'):
from ..biometric import BiometricCommand
commands['biometric'] = BiometricCommand()
Expand Down
117 changes: 80 additions & 37 deletions keepercommander/commands/discoveryrotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
import requests
from keeper_secrets_manager_core.utils import url_safe_str_to_bytes

from .base import (Command, GroupCommand, user_choice, dump_report_data, report_output_parser, field_to_title,
from .base import (Command, GroupCommand, user_choice, dump_report_data, report_output_parser,
json_output_parser, field_to_title,
FolderMixin, RecordMixin, toggle_pam_legacy_commands)
from .folder import FolderMoveCommand
from .ksm import KSMCommand
Expand Down Expand Up @@ -2610,7 +2611,8 @@ def execute(self, params, **kwargs):


class PAMRouterGetRotationInfo(Command):
parser = argparse.ArgumentParser(prog='dr-router-get-rotation-info-parser')
parser = argparse.ArgumentParser(prog='dr-router-get-rotation-info-parser',
parents=[json_output_parser])
parser.add_argument('--record-uid', '-r', required=True, dest='record_uid', action='store',
help='Record UID to rotate')

Expand All @@ -2620,76 +2622,115 @@ def get_parser(self):
def execute(self, params, **kwargs):

record_uid = kwargs.get('record_uid')
format_type = kwargs.get('format', 'table')
record_uid_bytes = url_safe_str_to_bytes(record_uid)

rri = record_rotation_get(params, record_uid_bytes)
rri_status_name = router_pb2.RouterRotationStatus.Name(rri.status)
if rri_status_name == 'RRS_ONLINE':

print(f'Rotation Status: {bcolors.OKBLUE}Ready to rotate ({rri_status_name}){bcolors.ENDC}')
configuration_uid = utils.base64_url_encode(rri.configurationUid)
print(f'PAM Config UID: {bcolors.OKBLUE}{configuration_uid}{bcolors.ENDC}')
print(f'Node ID: {bcolors.OKBLUE}{rri.nodeId}{bcolors.ENDC}')

print(
f"Gateway Name where the rotation will be performed: {bcolors.OKBLUE}{(rri.controllerName if rri.controllerName else '-')}{bcolors.ENDC}")
print(
f"Gateway Uid: {bcolors.OKBLUE}{(utils.base64_url_encode(rri.controllerUid) if rri.controllerUid else '-')} {bcolors.ENDC}")
gateway_name = rri.controllerName if rri.controllerName else '-'
gateway_uid = utils.base64_url_encode(rri.controllerUid) if rri.controllerUid else '-'

def is_resource_ok(resource_id, params, configuration_uid):
if resource_id not in params.record_cache:
return False

configuration = vault.KeeperRecord.load(params, configuration_uid)
if not isinstance(configuration, vault.TypedRecord):
return False

field = configuration.get_typed_field('pamResources')
if not (field and isinstance(field.value, list) and len(field.value) == 1):
return False

rv = field.value[0]
if not isinstance(rv, dict):
return False

resources = rv.get('resourceRef')
return isinstance(resources, list) and resource_id in resources

admin_resource_uid = None
if rri.resourceUid:
resource_id = utils.base64_url_encode(rri.resourceUid)
resource_ok = is_resource_ok(resource_id, params, configuration_uid)
print(f"Admin Resource Uid: {bcolors.OKBLUE if resource_ok else bcolors.FAIL}{resource_id}"
f"{bcolors.ENDC}")
admin_resource_uid = utils.base64_url_encode(rri.resourceUid)

# print(f"Router Cookie: {bcolors.OKBLUE}{(rri.cookie if rri.cookie else '-')}{bcolors.ENDC}")
# print(f"scriptName: {bcolors.OKGREEN}{rri.scriptName}{bcolors.ENDC}")
if rri.pwdComplexity:
print(f"Password Complexity: {bcolors.OKGREEN}{rri.pwdComplexity}{bcolors.ENDC}")
# Password complexity
pwd_complexity_raw = rri.pwdComplexity if rri.pwdComplexity else None
pwd_complexity_detail = None
if pwd_complexity_raw:
try:
record = params.record_cache.get(record_uid)
if record:
complexity = crypto.decrypt_aes_v2(utils.base64_url_decode(rri.pwdComplexity),
complexity = crypto.decrypt_aes_v2(utils.base64_url_decode(pwd_complexity_raw),
record['record_key_unencrypted'])
c = json.loads(complexity.decode())
print(f"Password Complexity Data: {bcolors.OKBLUE}"
f"Length: {c.get('length')}; Lowercase: {c.get('lowercase')}; "
f"Uppercase: {c.get('caps')}; "
f"Digits: {c.get('digits')}; "
f"Symbols: {c.get('special')}; "
f"Symbols Chars: {c.get('specialChars')} {bcolors.ENDC}")
except:
pass
pwd_complexity_detail = json.loads(complexity.decode())
except Exception:
pwd_complexity_detail = None

# Schedule information
schedule_type = None
schedule_data = None
rq = pam_pb2.PAMGenericUidsRequest()
schedules_proto = router_get_rotation_schedules(params, rq)
if schedules_proto:
for s in schedules_proto.schedules:
if s.recordUid == record_uid_bytes:
if s.noSchedule is True:
schedule_type = 'manual'
else:
schedule_type = 'scheduled'
schedule_data = s.scheduleData if s.scheduleData else None
break

if format_type == 'json':
result = {
'status': rri_status_name,
'ready_to_rotate': True,
'pam_config_uid': configuration_uid,
'node_id': rri.nodeId,
'gateway_name': gateway_name,
'gateway_uid': gateway_uid,
'admin_resource_uid': admin_resource_uid,
'password_complexity': pwd_complexity_raw,
'password_complexity_detail': pwd_complexity_detail,
'schedule_type': schedule_type,
'schedule_data': schedule_data,
'disabled': rri.disabled,
'script_name': rri.scriptName if rri.scriptName else None,
}
return json.dumps(result, indent=2)

# --- table output (original behaviour preserved) ---
print(f'Rotation Status: {bcolors.OKBLUE}Ready to rotate ({rri_status_name}){bcolors.ENDC}')
print(f'PAM Config UID: {bcolors.OKBLUE}{configuration_uid}{bcolors.ENDC}')
print(f'Node ID: {bcolors.OKBLUE}{rri.nodeId}{bcolors.ENDC}')
print(
f"Gateway Name where the rotation will be performed: {bcolors.OKBLUE}{gateway_name}{bcolors.ENDC}")
print(
f"Gateway Uid: {bcolors.OKBLUE}{gateway_uid} {bcolors.ENDC}")

if admin_resource_uid:
resource_ok = is_resource_ok(admin_resource_uid, params, configuration_uid)
print(f"Admin Resource Uid: {bcolors.OKBLUE if resource_ok else bcolors.FAIL}{admin_resource_uid}"
f"{bcolors.ENDC}")

# print(f"Router Cookie: {bcolors.OKBLUE}{(rri.cookie if rri.cookie else '-')}{bcolors.ENDC}")
# print(f"scriptName: {bcolors.OKGREEN}{rri.scriptName}{bcolors.ENDC}")
if pwd_complexity_raw:
print(f"Password Complexity: {bcolors.OKGREEN}{pwd_complexity_raw}{bcolors.ENDC}")
if pwd_complexity_detail:
c = pwd_complexity_detail
print(f"Password Complexity Data: {bcolors.OKBLUE}"
f"Length: {c.get('length')}; Lowercase: {c.get('lowercase')}; "
f"Uppercase: {c.get('caps')}; "
f"Digits: {c.get('digits')}; "
f"Symbols: {c.get('special')}; "
f"Symbols Chars: {c.get('specialChars')} {bcolors.ENDC}")
else:
print(f"Password Complexity: {bcolors.OKGREEN}[not set]{bcolors.ENDC}")

print(f"Is Rotation Disabled: {bcolors.OKGREEN}{rri.disabled}{bcolors.ENDC}")

# Get schedule information
rq = pam_pb2.PAMGenericUidsRequest()
schedules_proto = router_get_rotation_schedules(params, rq)
if schedules_proto:
schedules = list(schedules_proto.schedules)
for s in schedules:
for s in schedules_proto.schedules:
if s.recordUid == record_uid_bytes:
if s.noSchedule is True:
print(f"Schedule Type: {bcolors.OKBLUE}Manual Rotation{bcolors.ENDC}")
Expand All @@ -2707,6 +2748,8 @@ def is_resource_ok(resource_id, params, configuration_uid):

print(f"\nCommand to manually rotate: {bcolors.OKGREEN}pam action rotate -r {record_uid}{bcolors.ENDC}")
else:
if format_type == 'json':
return json.dumps({'status': rri_status_name, 'ready_to_rotate': False})
print(f'{bcolors.WARNING}Rotation Status: Not ready to rotate ({rri_status_name}){bcolors.ENDC}')


Expand Down
Loading
Loading