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

breachwatch report + compliance * fixes/improvements (part of KC-790) #1267

Merged
merged 1 commit into from
Jul 10, 2024
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
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
Loading