Skip to content

Commit

Permalink
breachwatch report: bug-fix for inaccurate report when run in MC as…
Browse files Browse the repository at this point in the history
… MSP admin; `compliance *`: improved messaging for denied operations; minor code clean-up (part of KC-790)
  • Loading branch information
aaunario-keeper authored and sk-keeper committed Jul 10, 2024
1 parent 667aa84 commit 566ed21
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 42 deletions.
17 changes: 16 additions & 1 deletion keepercommander/breachwatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@
from urllib.parse import urlparse, urlunparse
from typing import Iterator, Tuple, Optional, List, Callable, Dict, Iterable, Union

from .commands.helpers.enterprise import user_has_privilege, is_addon_enabled
from .constants import KEEPER_PUBLIC_HOSTS
from . import api, crypto, utils, rest_api, vault
from .proto import breachwatch_pb2, client_pb2, APIRequest_pb2
from .error import KeeperApiError
from .error import KeeperApiError, CommandError
from .params import KeeperParams
from .vault import KeeperRecord

Expand Down Expand Up @@ -418,3 +419,17 @@ def scan_and_update_security_data(params, record_uid, bw_obj=None, force_update=
if set_reused_pws:
BreachWatch.save_reused_pw_count(params)
api.sync_down(params)

@staticmethod
def validate_reporting(cmd, params):
msg_no_priv = 'You do not have the required privilege to run a BreachWatch report'
msg_no_addon = ('BreachWatch is not enabled for this enterprise. '
'Please visit https://www.keepersecurity.com/breachwatch.html for more information.')

privilege = 'run_reports'
addon = 'enterprise_breach_watch'
error_msg = msg_no_priv if not user_has_privilege(params, privilege) \
else msg_no_addon if not is_addon_enabled(params, addon) \
else None
if error_msg:
raise CommandError(cmd, error_msg)
19 changes: 16 additions & 3 deletions keepercommander/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
register_commands, register_enterprise_commands, register_msp_commands,
aliases, commands, command_info, enterprise_commands, msp_commands
)
from .commands.base import dump_report_data, CliCommand
from .commands.base import dump_report_data, CliCommand, GroupCommand
from .commands import msp
from .constants import OS_WHICH_CMD, KEEPER_PUBLIC_HOSTS
from .error import CommandError, Error
Expand Down Expand Up @@ -112,6 +112,19 @@ def check_if_running_as_mc(params, args):
return params, args


def is_enterprise_command(name, command, args): # type: (str, CliCommand, str) -> bool
if name in enterprise_commands:
return True
elif isinstance(command, GroupCommand):
args = args.split(' ')
verb = next(iter(args), None)
subcommand = command.subcommands.get(verb)
from keepercommander.commands.enterprise_common import EnterpriseCommand
return isinstance(subcommand, EnterpriseCommand)
else:
return False


def command_and_args_from_cmd(command_line):
args = ''
pos = command_line.find(' ')
Expand Down Expand Up @@ -238,10 +251,10 @@ def is_msp(params_local):
logging.info('Canceled')
return

if cmd in enterprise_commands or cmd in msp_commands:
if is_enterprise_command(cmd, command, args) or cmd in msp_commands:
params, args = check_if_running_as_mc(params, args)

if cmd in enterprise_commands and not params.enterprise:
if is_enterprise_command(cmd, command, args) and not params.enterprise:
if is_executing_as_msp_admin():
logging.debug("OK to execute command: %s", cmd)
else:
Expand Down
6 changes: 4 additions & 2 deletions keepercommander/commands/breachwatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import logging
from typing import Optional, Any, Dict

from .enterprise_common import EnterpriseCommand
from .security_audit import SecurityAuditReportCommand
from .. import api, crypto, utils, vault, vault_extensions
from .base import GroupCommand, Command, dump_report_data
Expand Down Expand Up @@ -73,7 +74,7 @@ def __init__(self):
self.default_verb = 'list'

def validate(self, params): # type: (KeeperParams) -> None
if not params.breach_watch:
if not params.breach_watch and not params.msp_tree_key:
raise CommandError('breachwatch',
'BreachWatch is not active. Please visit the Web Vault at https://keepersecurity.com/vault')

Expand Down Expand Up @@ -279,10 +280,11 @@ def execute(self, params, **kwargs): # type: (KeeperParams, any) -> any
logging.info(f'{utils.base64_url_encode(status.recordUid)}: {status.status} {status.reason}')


class BreachWatchReportCommand(Command):
class BreachWatchReportCommand(EnterpriseCommand):
def get_parser(self):
return breachwatch_report_parser

def execute(self, params, **kwargs):
BreachWatch.validate_reporting('breachwatch report', params)
cmd = SecurityAuditReportCommand()
return cmd.execute(params, **{'breachwatch':True, **kwargs})
37 changes: 37 additions & 0 deletions keepercommander/commands/helpers/enterprise.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from keepercommander.params import KeeperParams


def is_addon_enabled(params, addon_name): # type: (KeeperParams, Dict[str, ]) -> Boolean
def is_enabled(addon):
return addon.get('enabled') or addon.get('included_in_product')

enterprise = params.enterprise or {}
licenses = enterprise.get('licenses')
if not isinstance(licenses, list):
return False
if next(iter(licenses), {}).get('lic_status') == 'business_trial':
return True
addons = [a for l in licenses for a in l.get('add_ons', []) if a.get('name') == addon_name]
return any(a for a in addons if is_enabled(a))


def user_has_privilege(params, privilege): # type: (KeeperParams, str) -> bool
# Running as MSP admin, user has all available privileges in this context
if params.msp_tree_key:
return True

enterprise = params.enterprise

# Not an admin account (user has no admin privileges)
if not enterprise:
return False

# Check role-derived privileges
username = params.user
users = enterprise.get('users')
e_user_id = next(iter([u.get('enterprise_user_id') for u in users if u.get('username') == username]))
role_users = enterprise.get('role_users')
r_ids = [ru.get('role_id') for ru in role_users if ru.get('enterprise_user_id') == e_user_id]
r_privileges = enterprise.get('role_privileges')
p_key = 'privilege'
return any(rp for rp in r_privileges if rp.get('role_id') in r_ids and rp.get(p_key) == privilege)
9 changes: 4 additions & 5 deletions keepercommander/commands/security_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey

from keepercommander import api, crypto, utils
from keepercommander.breachwatch import BreachWatch
from keepercommander.commands.base import GroupCommand, raise_parse_exception, suppress_exit, field_to_title, \
dump_report_data
from keepercommander.commands.enterprise_common import EnterpriseCommand
Expand Down Expand Up @@ -163,10 +164,9 @@ def execute(self, params, **kwargs):
logging.info(security_audit_report_description)
return

if kwargs.get('breachwatch') and not params.breach_watch:
msg = ('Ignoring "--breachwatch" option because BreachWatch is not active. '
'Please visit the Web Vault at https://keepersecurity.com/vault')
logging.warning(msg)
show_breachwatch = kwargs.get('breachwatch')
if show_breachwatch:
BreachWatch.validate_reporting('security-audit-report', params)

def get_node_id(name_or_id):
nodes = params.enterprise.get('nodes') or []
Expand Down Expand Up @@ -308,7 +308,6 @@ def update_vault_errors(username, error):
if save_report:
self.save_updated_security_reports(params, updated_security_reports)

show_breachwatch = kwargs.get('breachwatch') and params.breach_watch
fields = ('email', 'name', 'at_risk', 'passed', 'ignored') if show_breachwatch else \
('email', 'name', 'weak', 'medium', 'strong', 'reused', 'unique', 'securityScore', 'twoFactorChannel',
'node')
Expand Down
45 changes: 14 additions & 31 deletions keepercommander/sox/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import Dict, Tuple

from .. import api, crypto, utils
from ..commands.helpers.enterprise import user_has_privilege, is_addon_enabled
from ..error import CommandError, Error
from ..params import KeeperParams
from ..proto import enterprise_pb2
Expand All @@ -18,40 +19,22 @@


def validate_data_access(params, cmd=''):
if not is_compliance_reporting_enabled(params):
msg = 'Compliance reports add-on required to perform this action. ' \
'Please contact your administrator to enable this feature.'
raise CommandError(cmd, msg)
privilege = 'run_compliance_reports'
addon = 'compliance_report'
msg_no_priv = 'You do not have the required privilege to run a Compliance Report.'
msg_no_addon = ('Compliance reports add-on is required to perform this action. '
'Please contact your administrator to enable this feature.')
error_msg = msg_no_priv if not user_has_privilege(params, privilege) \
else msg_no_addon if not is_addon_enabled(params, addon) \
else None
if error_msg:
raise CommandError(cmd, error_msg)


def is_compliance_reporting_enabled(params):
enterprise = params.enterprise
if not enterprise:
return False
e_licenses = enterprise.get('licenses')
if not isinstance(e_licenses, list):
return False
if len(e_licenses) == 0:
return False
if e_licenses[0].get('lic_status') == 'business_trial':
return True
addon = next((a for l in e_licenses for a in l.get('add_ons', [])
if a.get('name') == 'compliance_report' and (a.get('enabled') or a.get('included_in_product'))), None)
if addon is None:
return False

if not params.msp_tree_key:
role_privilege = 'run_compliance_reports'
username = params.user
users = enterprise.get('users')
e_user_id = next(iter([u.get('enterprise_user_id') for u in users if u.get('username') == username]))
role_users = enterprise.get('role_users')
r_ids = [ru.get('role_id') for ru in role_users if ru.get('enterprise_user_id') == e_user_id]
r_privileges = enterprise.get('role_privileges')
p_key = 'privilege'
return any([rp for rp in r_privileges if rp.get('role_id') in r_ids and rp.get(p_key) == role_privilege])
else:
return True
privilege = 'run_compliance_reports'
addon = 'compliance_report'
return user_has_privilege(params, privilege) and is_addon_enabled(params, addon)


def encrypt_data(params, data): # type: (KeeperParams, str) -> bytes
Expand Down
1 change: 1 addition & 0 deletions keepercommander/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ def size_to_str(size): # type: (int) -> str
size = size / 1024
return f'{size:,.2f} Gb'


def parse_totp_uri(uri): # type: (str) -> Dict[str, Union[str, int, None]]
def parse_int(val):
return val and int(val)
Expand Down

0 comments on commit 566ed21

Please sign in to comment.