Example #1
0
def list_role_rollbacks(dynamo_table, message):
    role_id = dynamo.find_role_in_cache(dynamo_table, message.account,
                                        message.role_name)

    if not role_id:
        return ResponderReturn(
            successful=False,
            return_message="Unable to find role {} in account {}".format(
                message.role_name, message.account),
        )
    else:
        role_data = dynamo.get_role_data(dynamo_table,
                                         role_id,
                                         fields=["Policies"])
        return_val = "Restorable versions for role {} in account {}\n".format(
            message.role_name, message.account)
        for index, policy_version in enumerate(role_data["Policies"]):
            total_permissions, _ = roledata._get_permissions_in_policy(
                policy_version["Policy"])
            return_val += "({:>3}):  {:<5}     {:<15}  {}\n".format(
                index,
                len(total_permissions),
                policy_version["Discovered"],
                policy_version["Source"],
            )
        return ResponderReturn(successful=True, return_message=return_val)
Example #2
0
def cancel_scheduled_repo(account_number, dynamo_table, role_name=None, is_all=None):
    """
    Cancel scheduled repo for a role in an account
    """
    if not is_all and not role_name:
        LOGGER.error('Either a specific role to cancel or all must be provided')
        return

    if is_all:
        roles = Roles([Role(get_role_data(dynamo_table, roleID))
                      for roleID in role_ids_for_account(dynamo_table, account_number)])

        # filter to show only roles that are scheduled
        roles = [role for role in roles if (role.repo_scheduled)]

        for role in roles:
            set_role_data(dynamo_table, role.role_id, {'RepoScheduled': 0, 'ScheduledPerms': []})

        LOGGER.info('Canceled scheduled repo for roles: {}'.format(', '.join([role.role_name for role in roles])))
        return

    role_id = find_role_in_cache(dynamo_table, account_number, role_name)
    if not role_id:
        LOGGER.warn('Could not find role with name {} in account {}'.format(role_name, account_number))
        return

    role = Role(get_role_data(dynamo_table, role_id))

    if not role.repo_scheduled:
        LOGGER.warn('Repo was not scheduled for role {} in account {}'.format(role.role_name, account_number))
        return

    set_role_data(dynamo_table, role.role_id, {'RepoScheduled': 0, 'ScheduledPerms': []})
    LOGGER.info('Successfully cancelled scheduled repo for role {} in account {}'.format(role.role_name,
                role.account))
Example #3
0
def list_repoable_services(dynamo_table, message):
    role_id = dynamo.find_role_in_cache(dynamo_table, message.account,
                                        message.role_name)

    if not role_id:
        return ResponderReturn(
            successful=False,
            return_message='Unable to find role {} in account {}'.format(
                message.role_name, message.account))
    else:
        role_data = dynamo.get_role_data(dynamo_table,
                                         role_id,
                                         fields=['RepoableServices'])

        (repoable_permissions, repoable_services
         ) = roledata._convert_repoed_service_to_sorted_perms_and_services(
             role_data['RepoableServices'])

        repoable_services = role_data['RepoableServices']
        return ResponderReturn(
            successful=True,
            return_message=(
                'Role {} in account {} has:\n    Repoable Services: \n{}'
                '\n\n    Repoable Permissions: \n{}'.format(
                    message.role_name, message.account,
                    '\n'.join([service for service in repoable_services]),
                    '\n'.join([perm for perm in repoable_permissions]))))
Example #4
0
def _repo_role(
    account_number: str,
    role_name: str,
    config: RepokidConfig,
    hooks: RepokidHooks,
    commit: bool = False,
    scheduled: bool = False,
) -> List[str]:
    """
    Calculate what repoing can be done for a role and then actually do it if commit is set
      1) Check that a role exists, it isn't being disqualified by a filter, and that is has fresh AA data
      2) Get the role's current permissions, repoable permissions, and the new policy if it will change
      3) Make the changes if commit is set
    Args:
        account_number (string)
        role_name (string)
        commit (bool)

    Returns:
        None
    """
    role_id = find_role_in_cache(role_name, account_number)
    # only load partial data that we need to determine if we should keep going
    role = Role(role_id=role_id, config=config)
    role.fetch()
    return role.repo(hooks, commit=commit, scheduled=scheduled)
Example #5
0
def list_repoable_services(message: Message) -> ResponderReturn:
    role_id = find_role_in_cache(message.role_name, message.account)

    if not role_id:
        return ResponderReturn(
            successful=False,
            return_message="Unable to find role {} in account {}".format(
                message.role_name, message.account),
        )
    else:
        role = Role(role_id=role_id)
        role.fetch(fields=["RepoableServices"])

        (
            repoable_permissions,
            repoable_services,
        ) = get_services_and_permissions_from_repoable(role.repoable_services)

        return ResponderReturn(
            successful=True,
            return_message=
            ("Role {} in account {} has:\n    Repoable Services: \n{}\n\n    Repoable Permissions: \n{}"
             .format(
                 message.role_name,
                 message.account,
                 "\n".join([service for service in repoable_services]),
                 "\n".join([perm for perm in repoable_permissions]),
             )),
        )
Example #6
0
def remove_opt_out(message: Message) -> ResponderReturn:
    role_id = find_role_in_cache(message.role_name, message.account)

    if not role_id:
        return ResponderReturn(
            successful=False,
            return_message="Unable to find role {} in account {}".format(
                message.role_name, message.account),
        )

    role = Role(role_id=role_id)
    role.fetch(fields=["OptOut"])

    if not role.opt_out:
        return ResponderReturn(
            successful=False,
            return_message="Role {} in account {} wasn't opted out".format(
                message.role_name, message.account),
        )
    else:
        role.opt_out = {}
        role.store(fields=["opt_out"])
        return ResponderReturn(
            successful=True,
            return_message="Cancelled opt-out for role {} in account {}".
            format(message.role_name, message.account),
        )
Example #7
0
def list_role_rollbacks(message: Message) -> ResponderReturn:
    role_id = find_role_in_cache(message.role_name, message.account)

    if not role_id:
        return ResponderReturn(
            successful=False,
            return_message="Unable to find role {} in account {}".format(
                message.role_name, message.account
            ),
        )

    role = Role(role_id=role_id)
    role.fetch(fields=["Policies"])
    return_val = "Restorable versions for role {} in account {}\n".format(
        message.role_name, message.account
    )
    for index, policy_version in enumerate(role.policies):
        total_permissions, _ = get_permissions_in_policy(policy_version["Policy"])
        return_val += "({:>3}):  {:<5}     {:<15}  {}\n".format(
            index,
            len(total_permissions),
            policy_version["Discovered"],
            policy_version["Source"],
        )
    return ResponderReturn(successful=True, return_message=return_val)
Example #8
0
def remove_opt_out(dynamo_table, message):
    role_id = dynamo.find_role_in_cache(dynamo_table, message.account,
                                        message.role_name)

    if not role_id:
        return ResponderReturn(
            successful=False,
            return_message="Unable to find role {} in account {}".format(
                message.role_name, message.account),
        )

    role_data = dynamo.get_role_data(dynamo_table, role_id, fields=["OptOut"])

    if "OptOut" not in role_data or not role_data["OptOut"]:
        return ResponderReturn(
            successful=False,
            return_message="Role {} in account {} wasn't opted out".format(
                message.role_name, message.account),
        )
    else:
        dynamo.set_role_data(dynamo_table, role_id, {"OptOut": {}})
        return ResponderReturn(
            successful=True,
            return_message="Cancelled opt-out for role {} in account {}".
            format(message.role_name, message.account),
        )
Example #9
0
def _cancel_scheduled_repo(account_number: str,
                           role_name: str = "",
                           is_all: bool = False) -> None:
    """
    Cancel scheduled repo for a role in an account
    """
    if not is_all and not role_name:
        LOGGER.error(
            "Either a specific role to cancel or all must be provided")
        return

    if is_all:
        role_ids = get_all_role_ids_for_account(account_number)
        roles = RoleList.from_ids(role_ids)

        # filter to show only roles that are scheduled
        roles = roles.get_scheduled()

        for role in roles:
            role.repo_scheduled = 0
            role.scheduled_perms = []
            try:
                role.store(["repo_scheduled", "scheduled_perms"])
            except RoleStoreError:
                LOGGER.exception("failed to store role", exc_info=True)

        LOGGER.info("Canceled scheduled repo for roles: {}".format(", ".join(
            [role.role_name for role in roles])))
        return

    role_id = find_role_in_cache(role_name, account_number)
    if not role_id:
        LOGGER.warning(
            f"Could not find role with name {role_name} in account {account_number}"
        )
        return

    role = Role(role_id=role_id)
    role.fetch()

    if not role.repo_scheduled:
        LOGGER.warning(
            "Repo was not scheduled for role {} in account {}".format(
                role.role_name, account_number))
        return

    role.repo_scheduled = 0
    role.scheduled_perms = []
    try:
        role.store(["repo_scheduled", "scheduled_perms"])
    except RoleStoreError:
        LOGGER.exception("failed to store role", exc_info=True)
        raise

    LOGGER.info(
        "Successfully cancelled scheduled repo for role {} in account {}".
        format(role.role_name, role.account))
Example #10
0
def opt_out(dynamo_table, message):
    if CONFIG:
        opt_out_period = CONFIG.get("opt_out_period_days", 90)
    else:
        opt_out_period = 90

    if not message.reason or not message.requestor:
        return ResponderReturn(
            successful=False,
            return_message="Reason and requestor must be specified")

    role_id = dynamo.find_role_in_cache(dynamo_table, message.account,
                                        message.role_name)

    if not role_id:
        return ResponderReturn(
            successful=False,
            return_message="Unable to find role {} in account {}".format(
                message.role_name, message.account),
        )

    role_data = dynamo.get_role_data(dynamo_table, role_id, fields=["OptOut"])
    if "OptOut" in role_data and role_data["OptOut"]:

        timestr = time.strftime("%m/%d/%y",
                                time.localtime(role_data["OptOut"]["expire"]))
        return ResponderReturn(
            successful=False,
            return_message=
            ("Role {} in account {} is already opted out by {} for reason {} "
             "until {}".format(
                 message.role_name,
                 message.account,
                 role_data["OptOut"]["owner"],
                 role_data["OptOut"]["reason"],
                 timestr,
             )),
        )

    else:
        current_dt = datetime.datetime.fromtimestamp(time.time())
        expire_dt = current_dt + datetime.timedelta(opt_out_period)
        expire_epoch = int(
            (expire_dt - datetime.datetime(1970, 1, 1)).total_seconds())
        new_opt_out = {
            "owner": message.requestor,
            "reason": message.reason,
            "expire": expire_epoch,
        }
        dynamo.set_role_data(dynamo_table, role_id, {"OptOut": new_opt_out})
        return ResponderReturn(
            successful=True,
            return_message="Role {} in account {} opted-out until {}".format(
                message.role_name, message.account,
                expire_dt.strftime("%m/%d/%y")),
        )
Example #11
0
def opt_out(dynamo_table, message):
    if CONFIG:
        opt_out_period = CONFIG.get('opt_out_period_days', 90)
    else:
        opt_out_period = 90

    if not message.reason or not message.requestor:
        return ResponderReturn(
            successful=False,
            return_message='Reason and requestor must be specified')

    role_id = dynamo.find_role_in_cache(dynamo_table, message.account,
                                        message.role_name)

    if not role_id:
        return ResponderReturn(
            successful=False,
            return_message='Unable to find role {} in account {}'.format(
                message.role_name, message.account))

    role_data = dynamo.get_role_data(dynamo_table, role_id, fields=['OptOut'])
    if 'OptOut' in role_data and role_data['OptOut']:

        timestr = time.strftime('%m/%d/%y',
                                time.localtime(role_data['OptOut']['expire']))
        return ResponderReturn(
            successful=False,
            return_message=
            ('Role {} in account {} is already opted out by {} for reason {} '
             'until {}'.format(message.role_name, message.account,
                               role_data['OptOut']['owner'],
                               role_data['OptOut']['reason'], timestr)))

    else:
        current_dt = datetime.datetime.fromtimestamp(time.time())
        expire_dt = current_dt + datetime.timedelta(opt_out_period)
        expire_epoch = int(
            (expire_dt - datetime.datetime(1970, 1, 1)).total_seconds())
        new_opt_out = {
            'owner': message.requestor,
            'reason': message.reason,
            'expire': expire_epoch
        }
        dynamo.set_role_data(dynamo_table, role_id, {'OptOut': new_opt_out})
        return ResponderReturn(
            successful=True,
            return_message='Role {} in account {} opted-out until {}'.format(
                message.role_name, message.account,
                expire_dt.strftime('%m/%d/%y')))
Example #12
0
def cancel_scheduled_repo(account_number, role_name, dynamo_table):
    """
    Cancel scheduled repo for a role in an account
    """
    role_id = find_role_in_cache(dynamo_table, account_number, role_name)
    if not role_id:
        LOGGER.warn('Could not find role with name {} in account {}'.format(role_name, account_number))
        return

    role = Role(get_role_data(dynamo_table, role_id))

    if not role.repo_scheduled:
        LOGGER.warn('Repo was not scheduled for role {} in account {}'.format(role.role_name, account_number))
        return

    set_role_data(dynamo_table, role.role_id, {'RepoScheduled': 0})
    LOGGER.info('Successfully cancelled scheduled repo for role {} in account {}'.format(role.role_name,
                role.account))
Example #13
0
def list_repoable_services(dynamo_table, message):
    role_id = dynamo.find_role_in_cache(dynamo_table, message.account,
                                        message.role_name)

    if not role_id:
        return ResponderReturn(
            successful=False,
            return_message='Unable to find role {} in account {}'.format(
                message.role_name, message.account))
    else:
        role_data = dynamo.get_role_data(dynamo_table,
                                         role_id,
                                         fields=['RepoableServices'])
        repoable_services = role_data['RepoableServices']
        return ResponderReturn(
            successful=True,
            return_message='Repoable services from role {} in account {}: {}'.
            format(message.role_name, message.account, repoable_services))
Example #14
0
def _remove_permissions_from_roles(permissions,
                                   role_filename,
                                   dynamo_table,
                                   config,
                                   hooks,
                                   commit=False):
    """Loads roles specified in file and calls _remove_permissions_from_role() for each one.

    Args:
        permissions (list<string>)
        role_filename (string)
        commit (bool)

    Returns:
        None
    """
    roles = list()
    with open(role_filename, "r") as fd:
        roles = json.load(fd)

    for role_arn in tqdm(roles):
        arn = ARN(role_arn)
        if arn.error:
            LOGGER.error("INVALID ARN: {arn}".format(arn=role_arn))
            return

        account_number = arn.account_number
        role_name = arn.name.split("/")[-1]

        role_id = find_role_in_cache(dynamo_table, account_number, role_name)
        role = Role.parse_obj(get_role_data(dynamo_table, role_id))

        remove_permissions_from_role(
            account_number,
            permissions,
            role,
            role_id,
            dynamo_table,
            config,
            hooks,
            commit=commit,
        )

        repokid.hooks.call_hooks(hooks, "AFTER_REPO", {"role": role})
Example #15
0
def list_role_rollbacks(dynamo_table, message):
    role_id = dynamo.find_role_in_cache(dynamo_table, message.account,
                                        message.role_name)

    if not role_id:
        return ResponderReturn(
            successful=False,
            return_message='Unable to find role {} in account {}'.format(
                message.role_name, message.account))
    else:
        role_data = dynamo.get_role_data(dynamo_table,
                                         role_id,
                                         fields=['Policies'])
        return_val = 'Restorable versions for role {} in account {}\n'.format(
            message.role_name, message.account)
        for index, policy_version in enumerate(role_data['Policies']):
            return_val += '({:>3}):  {:<5}     {:<15}  {}\n'.format(
                index, len(str(policy_version['Policy'])),
                policy_version['Discovered'], policy_version['Source'])
        return ResponderReturn(successful=True, return_message=return_val)
Example #16
0
def _remove_permissions_from_roles(
    permissions: List[str],
    role_filename: str,
    config: RepokidConfig,
    hooks: RepokidHooks,
    commit: bool = False,
) -> None:
    """Loads roles specified in file and calls _remove_permissions_from_role() for each one.

    Args:
        permissions (list<string>)
        role_filename (string)
        commit (bool)

    Returns:
        None
    """
    with open(role_filename, "r") as fd:
        roles = json.load(fd)

    for role_arn in tqdm(roles):
        arn = ARN(role_arn)
        if arn.error:
            LOGGER.error("INVALID ARN: {arn}".format(arn=role_arn))
            return

        account_number = arn.account_number
        role_name = arn.name.split("/")[-1]

        role_id = find_role_in_cache(role_name, account_number)
        role = Role(role_id=role_id)
        role.fetch()

        remove_permissions_from_role(account_number,
                                     permissions,
                                     role,
                                     config,
                                     hooks,
                                     commit=commit)

        repokid.hooks.call_hooks(hooks, "AFTER_REPO", {"role": role})
Example #17
0
def remove_opt_out(dynamo_table, message):
    role_id = dynamo.find_role_in_cache(dynamo_table, message.account,
                                        message.role_name)

    if not role_id:
        return ResponderReturn(
            successful=False,
            return_message='Unable to find role {} in account {}'.format(
                message.role_name, message.account))

    role_data = dynamo.get_role_data(dynamo_table, role_id, fields=['OptOut'])

    if 'OptOut' not in role_data or not role_data['OptOut']:
        return ResponderReturn(
            successful=False,
            return_message='Role {} in account {} wasn\'t opted out'.format(
                message.role_name, message.account))
    else:
        dynamo.set_role_data(dynamo_table, role_id, {'OptOut': {}})
        return ResponderReturn(
            successful=True,
            return_message='Cancelled opt-out for role {} in account {}'.
            format(message.role_name, message.account))
Example #18
0
def display_role(account_number, role_name, dynamo_table, config, hooks):
    """
    Displays data about a role in a given account:
      1) Name, which filters are disqualifying it from repo, if it's repoable, total/repoable permissions,
         when it was last repoed, which services can be repoed
      2) The policy history: how discovered (repo, scan, etc), the length of the policy, and start of the contents
      3) Captured stats entry for the role
      4) A list of all services/actions currently allowed and whether they are repoable
      5) What the new policy would look like after repoing (if it is repoable)

    Args:
        account_number (string)
        role_name (string)

    Returns:
        None
    """
    role_id = find_role_in_cache(dynamo_table, account_number, role_name)
    if not role_id:
        LOGGER.warn('Could not find role with name {}'.format(role_name))
        return

    role = Role(get_role_data(dynamo_table, role_id))

    print "\n\nRole repo data:"
    headers = ['Name', 'Refreshed', 'Disqualified By', 'Can be repoed', 'Permissions', 'Repoable', 'Repoed', 'Services']
    rows = [[role.role_name,
             role.refreshed,
             role.disqualified_by,
             len(role.disqualified_by) == 0,
             role.total_permissions,
             role.repoable_permissions,
             role.repoed,
             role.repoable_services]]
    print tabulate(rows, headers=headers) + '\n\n'

    print "Policy history:"
    headers = ['Number', 'Source', 'Discovered', 'Permissions', 'Services']
    rows = []
    for index, policies_version in enumerate(role.policies):
        policy_permissions = roledata._get_permissions_in_policy(policies_version['Policy'])
        rows.append([index,
                     policies_version['Source'],
                     policies_version['Discovered'],
                     len(policy_permissions),
                     roledata._get_services_in_permissions(policy_permissions)])
    print tabulate(rows, headers=headers) + '\n\n'

    print "Stats:"
    headers = ['Date', 'Event Type', 'Permissions Count', 'Disqualified By']
    rows = []
    for stats_entry in role.stats:
        rows.append([stats_entry['Date'],
                     stats_entry['Source'],
                     stats_entry['PermissionsCount'],
                     stats_entry.get('DisqualifiedBy', [])])
    print tabulate(rows, headers=headers) + '\n\n'

    # can't do anymore if we don't have AA data
    if not role.aa_data:
        LOGGER.warn('ARN not found in Access Advisor: {}'.format(role.arn))
        return

    warn_unknown_permissions = config.get('warnings', {}).get('unknown_permissions', False)
    repoable_permissions = set([])

    permissions = roledata._get_role_permissions(role, warn_unknown_perms=warn_unknown_permissions)
    if len(role.disqualified_by) == 0:
        repoable_permissions = roledata._get_repoable_permissions(account_number, role.role_name, permissions,
                                                                  role.aa_data, role.no_repo_permissions,
                                                                  config['filter_config']['AgeFilter']['minimum_age'],
                                                                  hooks)

    print "Repoable services and permissions"
    headers = ['Service', 'Action', 'Repoable']
    rows = []
    for permission in permissions:
        service = permission.split(':')[0]
        action = permission.split(':')[1]
        repoable = permission in repoable_permissions
        rows.append([service, action, repoable])

    rows = sorted(rows, key=lambda x: (x[2], x[0], x[1]))
    print tabulate(rows, headers=headers) + '\n\n'

    repoed_policies, _ = roledata._get_repoed_policy(role.policies[-1]['Policy'], repoable_permissions)

    if repoed_policies:
        print('Repo\'d Policies: \n{}'.format(json.dumps(repoed_policies, indent=2, sort_keys=True)))
    else:
        print('All Policies Removed')

    # need to check if all policies would be too large
    if len(json.dumps(repoed_policies)) > MAX_AWS_POLICY_SIZE:
        LOGGER.warning("Policies would exceed the AWS size limit after repo for role: {}.  "
                       "Please manually minify.".format(role_name))
Example #19
0
def opt_out(message: Message) -> ResponderReturn:
    if CONFIG:
        opt_out_period = CONFIG.get("opt_out_period_days", 90)
    else:
        opt_out_period = 90

    if not message.reason or not message.requestor:
        return ResponderReturn(
            successful=False, return_message="Reason and requestor must be specified"
        )

    role_id = find_role_in_cache(message.role_name, message.account)

    if not role_id:
        return ResponderReturn(
            successful=False,
            return_message="Unable to find role {} in account {}".format(
                message.role_name, message.account
            ),
        )

    role = Role(role_id=role_id)
    role.fetch(fields=["OptOut"])
    if role.opt_out:
        timestr = time.strftime("%m/%d/%y", time.localtime(role.opt_out["expire"]))
        return ResponderReturn(
            successful=False,
            return_message=(
                "Role {} in account {} is already opted out by {} for reason {} "
                "until {}".format(
                    message.role_name,
                    message.account,
                    role.opt_out["owner"],
                    role.opt_out["reason"],
                    timestr,
                )
            ),
        )
    else:
        current_dt = datetime.datetime.fromtimestamp(time.time())
        expire_dt = current_dt + datetime.timedelta(opt_out_period)
        expire_epoch = int((expire_dt - datetime.datetime(1970, 1, 1)).total_seconds())
        new_opt_out = {
            "owner": message.requestor,
            "reason": message.reason,
            "expire": expire_epoch,
        }
        role.opt_out = new_opt_out
        try:
            role.store(fields=["opt_out"])
        except RoleStoreError:
            return ResponderReturn(
                successful=False,
                return_message=f"Failed to opt out role {message.role_name} in account {message.account}",
            )
        return ResponderReturn(
            successful=True,
            return_message="Role {} in account {} opted-out until {}".format(
                message.role_name, message.account, expire_dt.strftime("%m/%d/%y")
            ),
        )
Example #20
0
def _repo_role(
    account_number,
    role_name,
    dynamo_table,
    config,
    hooks,
    include_managed_policies=True,
    commit=False,
    scheduled=False,
):
    """
    Calculate what repoing can be done for a role and then actually do it if commit is set
      1) Check that a role exists, it isn't being disqualified by a filter, and that is has fresh AA data
      2) Get the role's current permissions, repoable permissions, and the new policy if it will change
      3) If include_managed_policies is set, get the role's current managed permissions, repoable managed permissions,
         and the new policy if it will change
      4) Make the changes if commit is set
    Args:
        account_number (string)
        role_name (string)
        commit (bool)

    Returns:
        None
    """
    errors = []

    role_id = find_role_in_cache(dynamo_table, account_number, role_name)
    # only load partial data that we need to determine if we should keep going
    role_data = get_role_data(
        dynamo_table,
        role_id,
        fields=[
            "DisqualifiedBy", "AAData", "RepoablePermissions",
            "RepoableManagedPermissions", "RoleName"
        ],
    )
    if not role_data:
        LOGGER.warn("Could not find role with name {}".format(role_name))
        return
    else:
        role = Role(role_data)

    continuing = True

    if len(role.disqualified_by) > 0:
        LOGGER.info(
            "Cannot repo role {} in account {} because it is being disqualified by: {}"
            .format(role_name, account_number, role.disqualified_by))
        continuing = False

    if not role.aa_data:
        LOGGER.warning("ARN not found in Access Advisor: {}".format(role.arn))
        continuing = False

    permissionsToDo = []
    if not role.repoable_permissions:
        LOGGER.info("No permissions to repo for role {} in account {}".format(
            role_name, account_number))
    else:
        permissionsToDo.append("inline")

    if include_managed_policies:
        if not role.repoable_managed_permissions:
            LOGGER.info(
                "No managed permissions to repo for role {} in account {}".
                format(role_name, account_number))
        else:
            permissionsToDo.append("managed")

    if not permissionsToDo:
        continuing = False

    # if we've gotten to this point, load the rest of the role
    role = Role(get_role_data(dynamo_table, role_id))

    fiveDaysAgo = datetime.datetime.now() - datetime.timedelta(
        days=config["repo_requirements"]["oldest_aa_data_days"])
    old_aa_data_services = []
    for aa_service in role.aa_data:
        if datetime.datetime.strptime(
                aa_service["lastUpdated"],
                "%a, %d %b %Y %H:%M:%S %Z") < fiveDaysAgo:
            old_aa_data_services.append(aa_service["serviceName"])

    if old_aa_data_services:
        LOGGER.error(
            "AAData older than threshold for these services: {} (role: {}, account {})"
            .format(old_aa_data_services, role_name, account_number),
            exc_info=True,
        )
        continuing = False

    if "inline" in permissionsToDo:
        errors.append(
            _deal_with_policies(role, account_number, config, hooks, scheduled,
                                role_name, dynamo_table, commit, continuing))

    if "managed" in permissionsToDo:
        errors.append(
            _deal_with_managed_policies(role, account_number, config, hooks,
                                        scheduled, role_name, dynamo_table,
                                        commit, continuing))
    return errors
Example #21
0
def _display_role(account_number, role_name, dynamo_table, config, hooks):
    """
    Displays data about a role in a given account:
      1) Name, which filters are disqualifying it from repo, if it's repoable, total/repoable permissions,
         when it was last repoed, which services can be repoed
      2) The policy history: how discovered (repo, scan, etc), the length of the policy, and start of the contents
      3) Captured stats entry for the role
      4) A list of all services/actions currently allowed and whether they are repoable
      5) What the new policy would look like after repoing (if it is repoable)

    Args:
        account_number (string)
        role_name (string)

    Returns:
        None
    """
    role_id = find_role_in_cache(dynamo_table, account_number, role_name)
    if not role_id:
        LOGGER.warn("Could not find role with name {}".format(role_name))
        return

    role = Role(get_role_data(dynamo_table, role_id))

    print("\n\nRole repo data:")
    headers = [
        "Name",
        "Refreshed",
        "Disqualified By",
        "Can be repoed",
        "Permissions",
        "Repoable",
        "Repoed",
        "Services",
    ]
    rows = [
        [
            role.role_name,
            role.refreshed,
            role.disqualified_by,
            len(role.disqualified_by) == 0,
            role.total_permissions,
            role.repoable_permissions,
            role.repoed,
            role.repoable_services,
        ]
    ]
    print(tabulate(rows, headers=headers) + "\n\n")

    print("Policy history:")
    headers = ["Number", "Source", "Discovered", "Permissions", "Services"]
    rows = []
    for index, policies_version in enumerate(role.policies):
        policy_permissions, _ = roledata._get_permissions_in_policy(
            policies_version["Policy"]
        )
        rows.append(
            [
                index,
                policies_version["Source"],
                policies_version["Discovered"],
                len(policy_permissions),
                roledata._get_services_in_permissions(policy_permissions),
            ]
        )
    print(tabulate(rows, headers=headers) + "\n\n")

    print("Stats:")
    headers = ["Date", "Event Type", "Permissions Count", "Disqualified By"]
    rows = []
    for stats_entry in role.stats:
        rows.append(
            [
                stats_entry["Date"],
                stats_entry["Source"],
                stats_entry["PermissionsCount"],
                stats_entry.get("DisqualifiedBy", []),
            ]
        )
    print(tabulate(rows, headers=headers) + "\n\n")

    # can't do anymore if we don't have AA data
    if not role.aa_data:
        LOGGER.warn("ARN not found in Access Advisor: {}".format(role.arn))
        return

    warn_unknown_permissions = config.get("warnings", {}).get(
        "unknown_permissions", False
    )
    repoable_permissions = set([])

    permissions, eligible_permissions = roledata._get_role_permissions(
        role, warn_unknown_perms=warn_unknown_permissions
    )
    if len(role.disqualified_by) == 0:
        repoable_permissions = roledata._get_repoable_permissions(
            account_number,
            role.role_name,
            eligible_permissions,
            role.aa_data,
            role.no_repo_permissions,
            config["filter_config"]["AgeFilter"]["minimum_age"],
            hooks,
        )

    print("Repoable services and permissions")
    headers = ["Service", "Action", "Repoable"]
    rows = []
    for permission in permissions:
        service = permission.split(":")[0]
        action = permission.split(":")[1]
        repoable = permission in repoable_permissions
        rows.append([service, action, repoable])

    rows = sorted(rows, key=lambda x: (x[2], x[0], x[1]))
    print(tabulate(rows, headers=headers) + "\n\n")

    repoed_policies, _ = roledata._get_repoed_policy(
        role.policies[-1]["Policy"], repoable_permissions
    )

    if repoed_policies:
        print(
            "Repo'd Policies: \n{}".format(
                json.dumps(repoed_policies, indent=2, sort_keys=True)
            )
        )
    else:
        print("All Policies Removed")

    # need to check if all policies would be too large
    if inline_policies_size_exceeds_maximum(repoed_policies):
        LOGGER.warning(
            "Policies would exceed the AWS size limit after repo for role: {}.  "
            "Please manually minify.".format(role_name)
        )
Example #22
0
def rollback_role(account_number, role_name, dynamo_table, config, hooks, selection=None, commit=None):
    """
    Display the historical policy versions for a roll as a numbered list.  Restore to a specific version if selected.
    Indicate changes that will be made and then actually make them if commit is selected.

    Args:
        account_number (string)
        role_name (string)
        selection (int): which policy version in the list to rollback to
        commit (bool): actually make the change

    Returns:
        errors (list): if any
    """
    errors = []

    role_id = find_role_in_cache(dynamo_table, account_number, role_name)
    if not role_id:
        message = 'Could not find role with name {} in account {}'.format(role_name, account_number)
        errors.append(message)
        LOGGER.warning(message)
        return errors
    else:
        role = Role(get_role_data(dynamo_table, role_id))

    # no option selected, display a table of options
    if not selection:
        headers = ['Number', 'Source', 'Discovered', 'Permissions', 'Services']
        rows = []
        for index, policies_version in enumerate(role.policies):
            policy_permissions = roledata._get_permissions_in_policy(policies_version['Policy'])
            rows.append([index, policies_version['Source'], policies_version['Discovered'],
                        len(policy_permissions),
                        roledata._get_services_in_permissions(policy_permissions)])
        print tabulate(rows, headers=headers)
        return

    from cloudaux import CloudAux
    conn = config['connection_iam']
    conn['account_number'] = account_number
    ca = CloudAux(**conn)

    current_policies = get_role_inline_policies(role.as_dict(), **conn)

    if selection:
        pp = pprint.PrettyPrinter()

        print "Will restore the following policies:"
        pp.pprint(role.policies[int(selection)]['Policy'])

        print "Current policies:"
        pp.pprint(current_policies)

        current_permissions = roledata._get_permissions_in_policy(role.policies[-1]['Policy'])
        selected_permissions = roledata._get_permissions_in_policy(role.policies[int(selection)]['Policy'])
        restored_permissions = selected_permissions - current_permissions

        print "\nResore will return these permissions:"
        print '\n'.join([perm for perm in sorted(restored_permissions)])

    if not commit:
        return False

    # if we're restoring from a version with fewer policies than we have now, we need to remove them to
    # complete the restore.  To do so we'll store all the policy names we currently have and remove them
    # from the list as we update.  Any policy names left need to be manually removed
    policies_to_remove = current_policies.keys()

    for policy_name, policy in role.policies[int(selection)]['Policy'].items():
        try:
            LOGGER.info("Pushing cached policy: {} (role: {} account {})".format(
                policy_name,
                role.role_name,
                account_number))

            ca.call('iam.client.put_role_policy', RoleName=role.role_name, PolicyName=policy_name,
                    PolicyDocument=json.dumps(policy, indent=2, sort_keys=True))

        except botocore.exceptions.ClientError as e:
            message = "Unable to push policy {}.  Error: {} (role: {} account {})".format(
                policy_name,
                e.message,
                role.role_name,
                account_number)
            LOGGER.error(message)
            errors.append(message)

        else:
            # remove the policy name if it's in the list
            try:
                policies_to_remove.remove(policy_name)
            except Exception:
                pass

    if policies_to_remove:
        for policy_name in policies_to_remove:
            try:
                ca.call('iam.client.delete_role_policy', RoleName=role.role_name, PolicyName=policy_name)

            except botocore.excpetions.ClientError as e:
                message = "Unable to delete policy {}.  Error: {} (role: {} account {})".format(
                    policy_name,
                    e.message,
                    role.role_name,
                    account_number)
                LOGGER.error(message)
                errors.append(message)

    _update_role_data(role, dynamo_table, account_number, config, conn, hooks, source='Restore', add_no_repo=False)

    if not errors:
        LOGGER.info('Successfully restored selected version of role policies (role: {} account: {})'.format(
            role.role_name,
            account_number))
    return errors
Example #23
0
def _repo_role(
    account_number: str,
    role_name: str,
    config: RepokidConfig,
    hooks: RepokidHooks,
    commit: bool = False,
    scheduled: bool = False,
) -> List[str]:
    """
    Calculate what repoing can be done for a role and then actually do it if commit is set
      1) Check that a role exists, it isn't being disqualified by a filter, and that is has fresh AA data
      2) Get the role's current permissions, repoable permissions, and the new policy if it will change
      3) Make the changes if commit is set
    Args:
        account_number (string)
        role_name (string)
        commit (bool)

    Returns:
        None
    """
    errors: List[str] = []

    role_id = find_role_in_cache(role_name, account_number)
    # only load partial data that we need to determine if we should keep going
    role = Role(role_id=role_id)
    role.fetch()

    continuing = True

    eligible, reason = role.is_eligible_for_repo()
    if not eligible:
        errors.append(f"Role {role_name} not eligible for repo: {reason}")
        return errors

    role.calculate_repo_scores(
        config["filter_config"]["AgeFilter"]["minimum_age"], hooks)
    repoed_policies, deleted_policy_names = role.get_repoed_policy(
        scheduled=scheduled)

    if inline_policies_size_exceeds_maximum(repoed_policies):
        error = (
            "Policies would exceed the AWS size limit after repo for role: {} in account {}.  "
            "Please manually minify.".format(role_name, account_number))
        LOGGER.error(error)
        errors.append(error)
        continuing = False

    # if we aren't repoing for some reason, unschedule the role
    if not continuing:
        role.repo_scheduled = 0
        role.scheduled_perms = []
        role.store(["repo_scheduled", "scheduled_perms"])
        return errors

    if not commit:
        log_deleted_and_repoed_policies(deleted_policy_names, repoed_policies,
                                        role_name, account_number)
        return errors

    conn = config["connection_iam"]
    conn["account_number"] = account_number

    for name in deleted_policy_names:
        try:
            delete_policy(name, role, account_number, conn)
        except IAMError as e:
            LOGGER.error(e)
            errors.append(str(e))

    if repoed_policies:
        try:
            replace_policies(repoed_policies, role, account_number, conn)
        except IAMError as e:
            LOGGER.error(e)
            errors.append(str(e))

    current_policies = get_role_inline_policies(role.dict(by_alias=True), **
                                                conn) or {}
    role.add_policy_version(current_policies, source="Repo")

    # regardless of whether we're successful we want to unschedule the repo
    role.repo_scheduled = 0
    role.scheduled_perms = []

    repokid.hooks.call_hooks(hooks, "AFTER_REPO", {
        "role": role,
        "errors": errors
    })

    if not errors:
        # repos will stay scheduled until they are successful
        role.repoed = datetime.datetime.now(
            tz=datetime.timezone.utc).isoformat()
        update_repoed_description(role.role_name, conn)
        role.gather_role_data(current_policies,
                              hooks,
                              source="Repo",
                              add_no_repo=False)
        LOGGER.info("Successfully repoed role: {} in account {}".format(
            role.role_name, account_number))
    role.store()
    return errors
Example #24
0
def repo_role(account_number, role_name, dynamo_table, config, hooks, commit=False, scheduled=False):
    """
    Calculate what repoing can be done for a role and then actually do it if commit is set
      1) Check that a role exists, it isn't being disqualified by a filter, and that is has fresh AA data
      2) Get the role's current permissions, repoable permissions, and the new policy if it will change
      3) Make the changes if commit is set
    Args:
        account_number (string)
        role_name (string)
        commit (bool)

    Returns:
        None
    """
    errors = []

    role_id = find_role_in_cache(dynamo_table, account_number, role_name)
    # only load partial data that we need to determine if we should keep going
    role_data = get_role_data(dynamo_table, role_id, fields=['DisqualifiedBy', 'AAData', 'RepoablePermissions',
                                                             'RoleName'])
    if not role_data:
        LOGGER.warn('Could not find role with name {}'.format(role_name))
        return
    else:
        role = Role(role_data)

    if len(role.disqualified_by) > 0:
        LOGGER.info('Cannot repo role {} in account {} because it is being disqualified by: {}'.format(
            role_name,
            account_number,
            role.disqualified_by))
        return

    if not role.aa_data:
        LOGGER.warning('ARN not found in Access Advisor: {}'.format(role.arn))
        return

    if not role.repoable_permissions:
        LOGGER.info('No permissions to repo for role {} in account {}'.format(role_name, account_number))
        return

    # if we've gotten to this point, load the rest of the role
    role = Role(get_role_data(dynamo_table, role_id))

    old_aa_data_services = []
    for aa_service in role.aa_data:
        if(datetime.datetime.strptime(aa_service['lastUpdated'], '%a, %d %b %Y %H:%M:%S %Z') <
           datetime.datetime.now() - datetime.timedelta(days=config['repo_requirements']['oldest_aa_data_days'])):
            old_aa_data_services.append(aa_service['serviceName'])

    if old_aa_data_services:
        LOGGER.error('AAData older than threshold for these services: {} (role: {}, account {})'.format(
            old_aa_data_services,
            role_name,
            account_number))
        return

    permissions = roledata._get_role_permissions(role)
    repoable_permissions = roledata._get_repoable_permissions(account_number, role.role_name, permissions, role.aa_data,
                                                              role.no_repo_permissions,
                                                              config['filter_config']['AgeFilter']['minimum_age'],
                                                              hooks)

    # if this is a scheduled repo we need to filter out permissions that weren't previously scheduled
    if scheduled:
        repoable_permissions = roledata._filter_scheduled_repoable_perms(repoable_permissions, role.scheduled_perms)

    repoed_policies, deleted_policy_names = roledata._get_repoed_policy(role.policies[-1]['Policy'],
                                                                        repoable_permissions)

    policies_length = len(json.dumps(repoed_policies))

    if policies_length > MAX_AWS_POLICY_SIZE:
        error = ("Policies would exceed the AWS size limit after repo for role: {} in account {}.  "
                 "Please manually minify.".format(role_name, account_number))
        LOGGER.error(error)
        errors.append(error)
        return

    if not commit:
        for name in deleted_policy_names:
            LOGGER.info('Would delete policy from {} with name {} in account {}'.format(
                role_name,
                name,
                account_number))
        if repoed_policies:
            LOGGER.info('Would replace policies for role {} with: \n{} in account {}'.format(
                role_name,
                json.dumps(repoed_policies, indent=2, sort_keys=True),
                account_number))
        return

    conn = config['connection_iam']
    conn['account_number'] = account_number

    for name in deleted_policy_names:
        LOGGER.info('Deleting policy with name {} from {} in account {}'.format(name, role.role_name, account_number))
        try:
            delete_role_policy(RoleName=role.role_name, PolicyName=name, **conn)
        except botocore.exceptions.ClientError as e:
            error = 'Error deleting policy: {} from role: {} in account {}.  Exception: {}'.format(
                name,
                role.role_name,
                account_number,
                e)
            LOGGER.error(error)
            errors.append(error)

    if repoed_policies:
        LOGGER.info('Replacing Policies With: \n{} (role: {} account: {})'.format(
            json.dumps(repoed_policies, indent=2, sort_keys=True),
            role.role_name,
            account_number))

        for policy_name, policy in repoed_policies.items():
            try:
                put_role_policy(RoleName=role.role_name, PolicyName=policy_name,
                                PolicyDocument=json.dumps(policy, indent=2, sort_keys=True),
                                **conn)

            except botocore.exceptions.ClientError as e:
                error = 'Exception calling PutRolePolicy on {role}/{policy} in account {account}\n{e}\n'.format(
                    role=role.role_name, policy=policy_name, account=account_number, e=str(e))
                LOGGER.error(error)
                errors.append(error)

    current_policies = get_role_inline_policies(role.as_dict(), **conn) or {}
    roledata.add_new_policy_version(dynamo_table, role, current_policies, 'Repo')

    # regardless of whether we're successful we want to unschedule the repo
    set_role_data(dynamo_table, role.role_id, {'RepoScheduled': 0, 'ScheduledPerms': []})

    repokid.hooks.call_hooks(hooks, 'AFTER_REPO', {'role': role})

    if not errors:
        # repos will stay scheduled until they are successful
        set_role_data(dynamo_table, role.role_id, {'Repoed': datetime.datetime.utcnow().isoformat()})
        _update_repoed_description(role.role_name, **conn)
        _update_role_data(role, dynamo_table, account_number, config, conn, hooks, source='Repo', add_no_repo=False)
        LOGGER.info('Successfully repoed role: {} in account {}'.format(role.role_name, account_number))
    return errors
Example #25
0
def _repo_role(
    account_number,
    role_name,
    dynamo_table,
    config,
    hooks,
    commit=False,
    scheduled=False,
):
    """
    Calculate what repoing can be done for a role and then actually do it if commit is set
      1) Check that a role exists, it isn't being disqualified by a filter, and that is has fresh AA data
      2) Get the role's current permissions, repoable permissions, and the new policy if it will change
      3) Make the changes if commit is set
    Args:
        account_number (string)
        role_name (string)
        commit (bool)

    Returns:
        None
    """
    errors = []

    role_id = find_role_in_cache(dynamo_table, account_number, role_name)
    # only load partial data that we need to determine if we should keep going
    role_data = get_role_data(
        dynamo_table,
        role_id,
        fields=["DisqualifiedBy", "AAData", "RepoablePermissions", "RoleName"],
    )
    if not role_data:
        LOGGER.warn("Could not find role with name {}".format(role_name))
        return
    else:
        role = Role(role_data)

    continuing = True

    if len(role.disqualified_by) > 0:
        LOGGER.info(
            "Cannot repo role {} in account {} because it is being disqualified by: {}"
            .format(role_name, account_number, role.disqualified_by))
        continuing = False

    if not role.aa_data:
        LOGGER.warning("ARN not found in Access Advisor: {}".format(role.arn))
        continuing = False

    if not role.repoable_permissions:
        LOGGER.info("No permissions to repo for role {} in account {}".format(
            role_name, account_number))
        continuing = False

    # if we've gotten to this point, load the rest of the role
    role = Role(get_role_data(dynamo_table, role_id))

    old_aa_data_services = []
    for aa_service in role.aa_data:
        if datetime.datetime.strptime(
                aa_service["lastUpdated"], "%a, %d %b %Y %H:%M:%S %Z"
        ) < datetime.datetime.now() - datetime.timedelta(
                days=config["repo_requirements"]["oldest_aa_data_days"]):
            old_aa_data_services.append(aa_service["serviceName"])

    if old_aa_data_services:
        LOGGER.error(
            "AAData older than threshold for these services: {} (role: {}, account {})"
            .format(old_aa_data_services, role_name, account_number),
            exc_info=True,
        )
        continuing = False

    total_permissions, eligible_permissions = roledata._get_role_permissions(
        role)
    repoable_permissions = roledata._get_repoable_permissions(
        account_number,
        role.role_name,
        eligible_permissions,
        role.aa_data,
        role.no_repo_permissions,
        config["filter_config"]["AgeFilter"]["minimum_age"],
        hooks,
    )

    # if this is a scheduled repo we need to filter out permissions that weren't previously scheduled
    if scheduled:
        repoable_permissions = roledata._filter_scheduled_repoable_perms(
            repoable_permissions, role.scheduled_perms)

    repoed_policies, deleted_policy_names = roledata._get_repoed_policy(
        role.policies[-1]["Policy"], repoable_permissions)

    if inline_policies_size_exceeds_maximum(repoed_policies):
        error = (
            "Policies would exceed the AWS size limit after repo for role: {} in account {}.  "
            "Please manually minify.".format(role_name, account_number))
        LOGGER.error(error)
        errors.append(error)
        continuing = False

    # if we aren't repoing for some reason, unschedule the role
    if not continuing:
        set_role_data(dynamo_table, role.role_id, {
            "RepoScheduled": 0,
            "ScheduledPerms": []
        })
        return

    if not commit:
        log_deleted_and_repoed_policies(deleted_policy_names, repoed_policies,
                                        role_name, account_number)
        return

    conn = config["connection_iam"]
    conn["account_number"] = account_number

    for name in deleted_policy_names:
        error = delete_policy(name, role, account_number, conn)
        if error:
            LOGGER.error(error)
            errors.append(error)

    if repoed_policies:
        error = replace_policies(repoed_policies, role, account_number, conn)
        if error:
            LOGGER.error(error)
            errors.append(error)

    current_policies = get_role_inline_policies(role.as_dict(), **conn) or {}
    roledata.add_new_policy_version(dynamo_table, role, current_policies,
                                    "Repo")

    # regardless of whether we're successful we want to unschedule the repo
    set_role_data(dynamo_table, role.role_id, {
        "RepoScheduled": 0,
        "ScheduledPerms": []
    })

    repokid.hooks.call_hooks(hooks, "AFTER_REPO", {
        "role": role,
        "errors": errors
    })

    if not errors:
        # repos will stay scheduled until they are successful
        set_role_data(
            dynamo_table,
            role.role_id,
            {"Repoed": datetime.datetime.utcnow().isoformat()},
        )
        update_repoed_description(role.role_name, **conn)
        partial_update_role_data(
            role,
            dynamo_table,
            account_number,
            config,
            conn,
            hooks,
            source="Repo",
            add_no_repo=False,
        )
        LOGGER.info("Successfully repoed role: {} in account {}".format(
            role.role_name, account_number))
    return errors
Example #26
0
def _rollback_role(account_number,
                   role_name,
                   dynamo_table,
                   config,
                   hooks,
                   selection=None,
                   commit=None):
    """
    Display the historical policy versions for a roll as a numbered list.  Restore to a specific version if selected.
    Indicate changes that will be made and then actually make them if commit is selected.

    Args:
        account_number (string)
        role_name (string)
        selection (int): which policy version in the list to rollback to
        commit (bool): actually make the change

    Returns:
        errors (list): if any
    """
    errors = []

    role_id = find_role_in_cache(dynamo_table, account_number, role_name)
    if not role_id:
        message = "Could not find role with name {} in account {}".format(
            role_name, account_number)
        errors.append(message)
        LOGGER.warning(message)
        return errors
    else:
        role = Role(get_role_data(dynamo_table, role_id))

    # no option selected, display a table of options
    if not selection:
        headers = ["Number", "Source", "Discovered", "Permissions", "Services"]
        rows = []
        for index, policies_version in enumerate(role.policies):
            policy_permissions, _ = roledata._get_permissions_in_policy(
                policies_version["Policy"])
            rows.append([
                index,
                policies_version["Source"],
                policies_version["Discovered"],
                len(policy_permissions),
                roledata._get_services_in_permissions(policy_permissions),
            ])
        print(tabulate(rows, headers=headers))
        return

    conn = config["connection_iam"]
    conn["account_number"] = account_number

    current_policies = get_role_inline_policies(role.as_dict(), **conn)

    if selection:
        pp = pprint.PrettyPrinter()

        print("Will restore the following policies:")
        pp.pprint(role.policies[int(selection)]["Policy"])

        print("Current policies:")
        pp.pprint(current_policies)

        current_permissions, _ = roledata._get_permissions_in_policy(
            role.policies[-1]["Policy"])
        selected_permissions, _ = roledata._get_permissions_in_policy(
            role.policies[int(selection)]["Policy"])
        restored_permissions = selected_permissions - current_permissions

        print("\nResore will return these permissions:")
        print("\n".join([perm for perm in sorted(restored_permissions)]))

    if not commit:
        return False

    # if we're restoring from a version with fewer policies than we have now, we need to remove them to
    # complete the restore.  To do so we'll store all the policy names we currently have and remove them
    # from the list as we update.  Any policy names left need to be manually removed
    policies_to_remove = current_policies.keys()

    for policy_name, policy in role.policies[int(selection)]["Policy"].items():
        try:
            LOGGER.info(
                f"Pushing cached policy: {policy_name} (role: {role.role_name} account {account_number})"
            )

            put_role_policy(
                RoleName=role.role_name,
                PolicyName=policy_name,
                PolicyDocument=json.dumps(policy, indent=2, sort_keys=True),
                **conn,
            )

        except botocore.exceptions.ClientError as e:
            message = "Unable to push policy {}.  Error: {} (role: {} account {})".format(
                policy_name, e.message, role.role_name, account_number)
            LOGGER.error(message, exc_info=True)
            errors.append(message)

        else:
            # remove the policy name if it's in the list
            try:
                policies_to_remove.remove(policy_name)
            except Exception:  # nosec
                pass

    if policies_to_remove:
        for policy_name in policies_to_remove:
            try:
                LOGGER.info(
                    f"Deleting policy {policy_name} for rollback (role: {role.role_name} account {account_number})"
                )
                delete_role_policy(RoleName=role.role_name,
                                   PolicyName=policy_name,
                                   **conn)

            except botocore.excpetions.ClientError as e:
                message = "Unable to delete policy {}.  Error: {} (role: {} account {})".format(
                    policy_name, e.message, role.role_name, account_number)
                LOGGER.error(message, exc_info=True)
                errors.append(message)

    partial_update_role_data(
        role,
        dynamo_table,
        account_number,
        config,
        conn,
        hooks,
        source="Restore",
        add_no_repo=False,
    )

    if not errors:
        LOGGER.info(
            f"Successfully restored selected version {selection} of role policies (role: {role.role_name} "
            f"account: {account_number}")
    return errors