Beispiel #1
0
def _get_permissions_in_policy(policy_dict, warn_unknown_perms=False):
    """
    Given a set of policies for a role, return a set of all allowed permissions

    Args:
        policy_dict
        warn_unknown_perms

    Returns
        set - all permissions allowed by the policies
    """
    permissions = set()

    for policy_name, policy in policy_dict.items():
        policy = expand_policy(policy=policy, expand_deny=False)
        for statement in policy.get('Statement'):
            if statement['Effect'].lower() == 'allow':
                permissions = permissions.union(
                    get_actions_from_statement(statement))

    weird_permissions = permissions.difference(all_permissions)
    if weird_permissions and warn_unknown_perms:
        LOGGER.warn('Unknown permissions found: {}'.format(weird_permissions))

    return permissions
Beispiel #2
0
def _get_permissions_in_policy(policy_dict, warn_unknown_perms=False):
    """
    Given a set of policies for a role, return a set of all allowed permissions

    Args:
        policy_dict
        warn_unknown_perms

    Returns
        tuple
        set - all permissions allowed by the policies
        set - all permisisons allowed by the policies not marked with STATEMENT_SKIP_SID
    """
    total_permissions = set()
    eligible_permissions = set()

    for policy_name, policy in list(policy_dict.items()):
        policy = expand_policy(policy=policy, expand_deny=False)
        for statement in policy.get("Statement"):
            if statement["Effect"].lower() == "allow":
                total_permissions = total_permissions.union(
                    get_actions_from_statement(statement))
                if not ("Sid" in statement
                        and statement["Sid"].startswith(STATEMENT_SKIP_SID)):
                    # No Sid
                    # Sid exists, but doesn't start with STATEMENT_SKIP_SID
                    eligible_permissions = eligible_permissions.union(
                        get_actions_from_statement(statement))

    weird_permissions = total_permissions.difference(all_permissions)
    if weird_permissions and warn_unknown_perms:
        LOGGER.warn("Unknown permissions found: {}".format(weird_permissions))

    return total_permissions, eligible_permissions
Beispiel #3
0
def _get_role_permissions(role, warn_unknown_perms=False):
    """
    Expand the most recent version of policies from a role to produce a list of all the permissions that are allowed
    (permission is included in one or more statements that is allowed).  To perform expansion the policyuniverse
    library is used. The result is a list of all of the individual permissions that are allowed in any of the
    statements. If our resultant list contains any permissions that aren't listed in the master list of permissions
    we'll raise an exception with the set of unknown permissions found.

    Args:
        role (Role): The role object that we're getting a list of permissions for

    Returns:
        set: A set of permissions that the role has policies that allow
    """
    permissions = set()

    for policy_name, policy in role.policies[-1]['Policy'].items():
        policy = expand_policy(policy=policy, expand_deny=False)
        for statement in policy.get('Statement'):
            if statement['Effect'].lower() == 'allow':
                permissions = permissions.union(
                    get_actions_from_statement(statement))

    weird_permissions = permissions.difference(all_permissions)
    if weird_permissions and warn_unknown_perms:
        LOGGER.warn('Unknown permissions found: {}'.format(weird_permissions))

    return permissions
Beispiel #4
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))
Beispiel #5
0
def _get_potentially_repoable_permissions(role_name, account_number, aa_data,
                                          permissions, no_repo_permissions,
                                          minimum_age):
    ago = datetime.timedelta(minimum_age)
    now = datetime.datetime.now(tzlocal())

    current_time = time.time()
    no_repo_list = [
        perm.lower() for perm in no_repo_permissions
        if no_repo_permissions[perm] > current_time
    ]

    # cast all permissions to lowercase
    permissions = [permission.lower() for permission in permissions]
    potentially_repoable_permissions = {
        permission: RepoablePermissionDecision()
        for permission in permissions if permission not in no_repo_list
    }

    used_services = set()
    for service in aa_data:
        (accessed, valid_authenticated) = _get_epoch_authenticated(
            service["lastAuthenticated"])

        if not accessed:
            continue

        if not valid_authenticated:
            LOGGER.error(
                "Got malformed Access Advisor data for {role_name} in {account_number} for service {service}"
                ": {last_authenticated}".format(
                    role_name=role_name,
                    account_number=account_number,
                    service=service.get("serviceNamespace"),
                    last_authenticated=service["lastAuthenticated"],
                ))
            used_services.add(service["serviceNamespace"])

        accessed = datetime.datetime.fromtimestamp(accessed, tzlocal())
        if accessed > now - ago:
            used_services.add(service["serviceNamespace"])

    for permission_name, permission_decision in list(
            potentially_repoable_permissions.items()):
        if permission_name.split(
                ":")[0] in IAM_ACCESS_ADVISOR_UNSUPPORTED_SERVICES:
            LOGGER.warn("skipping {}".format(permission_name))
            continue

        # we have an unused service but need to make sure it's repoable
        if permission_name.split(":")[0] not in used_services:
            if permission_name in IAM_ACCESS_ADVISOR_UNSUPPORTED_ACTIONS:
                LOGGER.warn("skipping {}".format(permission_name))
                continue

            permission_decision.repoable = True
            permission_decision.decider = "Access Advisor"

    return potentially_repoable_permissions
Beispiel #6
0
def _calculate_repo_scores(roles):
    """
    Get the total and repoable permissions count and set of repoable services for every role in the account.
    For each role:
      1) call _get_role_permissions
      2) call _get_repoable_permissions (count), repoable_permissions (count), and repoable_services (list) for role

    Each time we got the role permissions we built a list of any permissions that the role's policies granted access
    to but weren't in our master list of permissions AWS has.  At the end of this run we'll warn about any of these.

    Args:
        roles (Roles): The set of all roles we're analyzing

    Returns:
        None
    """
    for role in roles:
        permissions = _get_role_permissions(role)
        role.total_permissions = len(permissions)

        # if we don't have any access advisor data for a service than nothing is repoable
        if not role.aa_data:
            LOGGER.info('No data found in access advisor for {}'.format(
                role.role_id))
            role.repoable_permissions = 0
            role.repoable_services = []
            continue

        # permissions are only repoable if the role isn't being disqualified by filter(s)
        if len(role.disqualified_by) == 0:
            repoable_permissions = _get_repoable_permissions(
                permissions, role.aa_data, role.no_repo_permissions)
            repoable_services = set([
                permission.split(':')[0] for permission in repoable_permissions
            ])
            repoable_services = sorted(repoable_services)
            role.repoable_permissions = len(repoable_permissions)
            role.repoable_services = repoable_services
        else:
            role.repoable_permissions = 0
            role.repoable_services = []

    if WEIRD:
        all_services = set(
            [permission.split(':')[0] for permission in all_permissions])
        # print('Not sure about these permissions:\n{}'.format(json.dumps(list(WEIRD), indent=2, sort_keys=True)))
        weird_services = set(
            [permission.split(':')[0] for permission in WEIRD])
        weird_services = weird_services.difference(all_services)
        LOGGER.warn('Not sure about these services:\n{}'.format(
            json.dumps(list(weird_services), indent=2, sort_keys=True)))
Beispiel #7
0
 def load_plugin(self, module, config=None):
     """Import a module by path, instantiate it with plugin specific config and add to the list of active plugins"""
     cls = None
     try:
         cls = import_string(module)
     except ImportError as e:
         LOGGER.warn("Unable to find plugin {}, exception: {}".format(module, e))
     else:
         plugin = None
         try:
             plugin = cls(config=config)
         except KeyError:
             plugin = cls()
         LOGGER.info('Loaded plugin {}'.format(module))
         self.filter_plugins.append(plugin)
Beispiel #8
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))
Beispiel #9
0
def _get_repoable_permissions(permissions, aa_data, no_repo_permissions):
    """
    Generate a list of repoable permissions for a role based on the list of all permissions the role's policies
    currently allow and Access Advisor data for the services included in the role's policies.

    The first step is to come up with a list of services that were used within the time threshold (the same defined)
    in the age filter config. Permissions are repoable if they aren't in the used list, aren't in the global list
    of unsupported services/actions (IAM_ACCESS_ADVISOR_UNSUPPORTED_SERVICES, IAM_ACCESS_ADVISOR_UNSUPPORTED_ACTIONS),
    and aren't being temporarily ignored because they're on the no_repo_permissions list (newly added).

    Args:
        permissions (list): The full list of permissions that the role's permissions allow
        aa_data (list): A list of Access Advisor data for a role. Each element is a dictionary with a couple required
                        attributes: lastAuthenticated (epoch time in milliseconds when the service was last used and
                        serviceNamespace (the service used)
        no_repo_permissions (dict): Keys are the name of permissions and values are the time the entry expires

    Returns:
        set: Permissions that are 'repoable' (not used within the time threshold)
    """
    ago = datetime.timedelta(
        CONFIG['filter_config']['AgeFilter']['minimum_age'])
    now = datetime.datetime.now(tzlocal())

    current_time = time.time()
    no_repo_list = [
        perm.lower() for perm in no_repo_permissions
        if no_repo_permissions[perm] > current_time
    ]

    used_services = set()
    for service in aa_data:
        accessed = service['lastAuthenticated']
        if not accessed:
            continue
        accessed = datetime.datetime.fromtimestamp(accessed / 1000, tzlocal())
        if accessed > now - ago:
            used_services.add(service['serviceNamespace'])

    repoable_permissions = set()
    for permission in permissions:
        if permission.split(':')[0] in IAM_ACCESS_ADVISOR_UNSUPPORTED_SERVICES:
            LOGGER.warn('skipping {}'.format(permission))
            continue

        # we have an unused service but need to make sure it's repoable
        if permission.split(':')[0] not in used_services:
            if permission.lower() in IAM_ACCESS_ADVISOR_UNSUPPORTED_ACTIONS:
                LOGGER.warn('skipping {}'.format(permission))
                continue

            if permission.lower() in no_repo_list:
                LOGGER.warn(
                    'skipping {} because it is in the no repo list'.format(
                        permission))
                continue

            repoable_permissions.add(permission.lower())

    return repoable_permissions
Beispiel #10
0
def _get_repoable_permissions(account_number, role_name, permissions, aa_data,
                              no_repo_permissions, minimum_age, hooks):
    """
    Generate a list of repoable permissions for a role based on the list of all permissions the role's policies
    currently allow and Access Advisor data for the services included in the role's policies.

    The first step is to come up with a list of services that were used within the time threshold (the same defined)
    in the age filter config. Permissions are repoable if they aren't in the used list, aren't in the constant list
    of unsupported services/actions (IAM_ACCESS_ADVISOR_UNSUPPORTED_SERVICES, IAM_ACCESS_ADVISOR_UNSUPPORTED_ACTIONS),
    and aren't being temporarily ignored because they're on the no_repo_permissions list (newly added).

    Args:
        account_number
        role_name
        permissions (list): The full list of permissions that the role's permissions allow
        aa_data (list): A list of Access Advisor data for a role. Each element is a dictionary with a couple required
                        attributes: lastAuthenticated (epoch time in milliseconds when the service was last used and
                        serviceNamespace (the service used)
        no_repo_permissions (dict): Keys are the name of permissions and values are the time the entry expires
        minimum_age: Minimum age of a role (in days) for it to be repoable
        hooks: Dict containing hook names and functions to run

    Returns:
        set: Permissions that are 'repoable' (not used within the time threshold)
    """
    ago = datetime.timedelta(minimum_age)
    now = datetime.datetime.now(tzlocal())

    current_time = time.time()
    no_repo_list = [
        perm.lower() for perm in no_repo_permissions
        if no_repo_permissions[perm] > current_time
    ]

    # cast all permissions to lowercase
    permissions = [permission.lower() for permission in permissions]
    potentially_repoable_permissions = {
        permission: RepoablePermissionDecision()
        for permission in permissions if permission not in no_repo_list
    }

    used_services = set()
    for service in aa_data:
        accessed = service['lastAuthenticated']
        if not accessed:
            continue
        accessed = datetime.datetime.fromtimestamp(accessed / 1000, tzlocal())
        if accessed > now - ago:
            used_services.add(service['serviceNamespace'])

    for permission_name, permission_decision in potentially_repoable_permissions.items(
    ):
        if permission_name.split(
                ':')[0] in IAM_ACCESS_ADVISOR_UNSUPPORTED_SERVICES:
            LOGGER.warn('skipping {}'.format(permission_name))
            continue

        # we have an unused service but need to make sure it's repoable
        if permission_name.split(':')[0] not in used_services:
            if permission_name in IAM_ACCESS_ADVISOR_UNSUPPORTED_ACTIONS:
                LOGGER.warn('skipping {}'.format(permission_name))
                continue

            permission_decision.repoable = True
            permission_decision.decider = 'Access Advisor'

    hooks_output = repokid.hooks.call_hooks(
        hooks, 'DURING_REPOABLE_CALCULATION', {
            'account_number': account_number,
            'role_name': role_name,
            'potentially_repoable_permissions':
            potentially_repoable_permissions,
            'minimum_age': minimum_age
        })

    # TODO: make option to show source of repoable?

    return set([
        permission_name for permission_name, permission_value in
        hooks_output['potentially_repoable_permissions'].items()
        if permission_value.repoable
    ])
Beispiel #11
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
Beispiel #12
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))
Beispiel #13
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 {}'.format(role_name)
        errors.append(message)
        LOGGER.warn(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: {}".format(policy_name))
            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: {}".format(
                policy_name, e.message)
            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: {}".format(
                    policy_name, e.message)
                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')
    return errors
Beispiel #14
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)
        )