Beispiel #1
0
def find_and_mark_inactive(account_number, active_roles):
    """
    Mark roles in the account that aren't currently active inactive. Do this by getting all roles in the account and
    subtracting the active roles, any that are left are inactive and should be marked thusly.

    Args:
        account_number (string)
        active_roles (set): the currently active roles discovered in the most recent scan

    Returns:
        None
    """

    active_roles = set(active_roles)
    known_roles = set(role_ids_for_account(account_number))
    inactive_roles = known_roles - active_roles

    for roleID in inactive_roles:
        role_dict = _get_role_data(roleID, fields=['Active', 'Arn'])
        if role_dict['Active']:
            try:
                DYNAMO_TABLE.update_item(
                    Key={'RoleId': roleID},
                    UpdateExpression="SET Active = :false",
                    ExpressionAttributeValues={":false": False})
            except BotoClientError as e:
                LOGGER.error('Dynamo table error: {}'.format(e))
            else:
                LOGGER.info('Marked role ({}): {} inactive'.format(
                    roleID, role_dict['Arn']))
Beispiel #2
0
def schedule_repo(account_number, dynamo_table, config, hooks):
    """
    Schedule a repo for a given account.  Schedule repo for a time in the future (default 7 days) for any roles in
    the account with repoable permissions.
    """
    scheduled_roles = []

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

    scheduled_time = int(time.time()) + (86400 * config.get('repo_schedule_period_days', 7))
    for role in roles:
        if role.repoable_permissions > 0 and not role.repo_scheduled:
            role.repo_scheduled = scheduled_time
            # freeze the scheduled perms to whatever is repoable right now
            set_role_data(dynamo_table, role.role_id,
                          {'RepoScheduled': scheduled_time, 'ScheduledPerms': role.repoable_services})

            scheduled_roles.append(role)

    LOGGER.info("Scheduled repo for {} days from now for account {} and these roles:\n\t{}".format(
                config.get('repo_schedule_period_days', 7),
                account_number,
                ', '.join([r.role_name for r in scheduled_roles])))

    repokid.hooks.call_hooks(hooks, 'AFTER_SCHEDULE_REPO', {'roles': scheduled_roles})
Beispiel #3
0
def log_deleted_and_repoed_policies(deleted_policy_names, repoed_policies,
                                    role_name, account_number):
    """Logs data on policies that would otherwise be modified or deleted if the commit flag were set.

    Args:
        deleted_policy_names (list<string>)
        repoed_policies (list<dict>)
        role_name (string)
        account_number (string)

    Returns:
        None
    """
    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,
            ))
Beispiel #4
0
def update_role_data(dynamo_table,
                     account_number,
                     role,
                     current_policy,
                     source='Scan',
                     add_no_repo=True):
    """
    Compare the current version of a policy for a role and what has been previously stored in Dynamo.
      - If current and new policy versions are different store the new version in Dynamo. Add any newly added
          permissions to temporary permission blacklist. Purge any old entries from permission blacklist.
      - Refresh the updated time on the role policy
      - If the role is completely new, store the first version in Dynamo
      - Updates the role with full history of policies, including current version

    Args:
        dynamo_table
        account_number
        role (Role): current role being updated
        current_policy (dict): representation of the current policy version
        source: Default 'Scan' but could be Repo, Rollback, etc

    Returns:
        None
    """

    # policy_entry: source, discovered, policy
    stored_role = get_role_data(dynamo_table,
                                role.role_id,
                                fields=['OptOut', 'Policies'])
    if not stored_role:
        role_dict = store_initial_role_data(dynamo_table, role.arn,
                                            role.create_date, role.role_id,
                                            role.role_name, account_number,
                                            current_policy)
        role.set_attributes(role_dict)
        LOGGER.info('Added new role ({}): {}'.format(role.role_id, role.arn))
    else:
        # is the policy list the same as the last we had?
        old_policy = stored_role['Policies'][-1]['Policy']
        if current_policy != old_policy:
            add_new_policy_version(dynamo_table, role, current_policy, source)
            LOGGER.info(
                '{} has different inline policies than last time, adding to role store'
                .format(role.arn))

            newly_added_permissions = find_newly_added_permissions(
                old_policy, current_policy)
        else:
            newly_added_permissions = set()

        if add_no_repo:
            update_no_repo_permissions(dynamo_table, role,
                                       newly_added_permissions)
        update_opt_out(dynamo_table, role)
        set_role_data(dynamo_table, role.role_id,
                      {'Refreshed': datetime.datetime.utcnow().isoformat()})

        role.policies = get_role_data(dynamo_table,
                                      role.role_id,
                                      fields=['Policies'])['Policies']
Beispiel #5
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 #6
0
def repo_stats(output_file, dynamo_table, account_number=None):
    """
    Create a csv file with stats about roles, total permissions, and applicable filters over time

    Args:
        output_file (string): the name of the csv file to write
        account_number (string): if specified only display roles from selected account, otherwise display all

    Returns:
        None
    """
    roleIDs = (role_ids_for_account(dynamo_table, account_number) if account_number else
               role_ids_for_all_accounts(dynamo_table))
    headers = ['RoleId', 'Role Name', 'Account', 'Active', 'Date', 'Source', 'Permissions Count',
               'Repoable Permissions Count', 'Disqualified By']
    rows = []

    for roleID in roleIDs:
        role_data = get_role_data(dynamo_table, roleID, fields=['RoleId', 'RoleName', 'Account', 'Active', 'Stats'])
        for stats_entry in role_data.get('Stats', []):
            rows.append([role_data['RoleId'], role_data['RoleName'], role_data['Account'], role_data['Active'],
                         stats_entry['Date'], stats_entry['Source'], stats_entry['PermissionsCount'],
                         stats_entry.get('RepoablePermissionsCount'), stats_entry.get('DisqualifiedBy', [])])

    try:
        with open(output_file, 'wb') as csvfile:
            csv_writer = csv.writer(csvfile)
            csv_writer.writerow(headers)
            for row in rows:
                csv_writer.writerow(row)
    except IOError as e:
        LOGGER.error('Unable to write file {}: {}'.format(output_file, e))
    else:
        LOGGER.info('Successfully wrote stats to {}'.format(output_file))
Beispiel #7
0
def replace_policies(repoed_policies, role, account_number, conn):
    """Overwrite IAM Role inline policies with those supplied.

    Args:
        repoed_policies (dict)
        role (Role object)
        account_number (string)
        conn (dict)

    Returns:
        error (string) or None
    """
    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),
            )
            return error
Beispiel #8
0
def _calculate_repo_scores(roles, minimum_age, hooks, batch=False, batch_size=100):
    """
    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
        minimum_age
        hooks

    Returns:
        None
    """
    repo_able_roles = []
    eligible_permissions_dict = {}
    for role in roles:
        total_permissions, eligible_permissions = _get_role_permissions(role)
        role.total_permissions = len(total_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:
            repo_able_roles.append(role)
            eligible_permissions_dict[role.arn] = eligible_permissions
        else:
            role.repoable_permissions = 0
            role.repoable_services = []

    repoable_permissions_dict = {}
    if batch:
        repoable_permissions_dict = _get_repoable_permissions_batch(
            repo_able_roles, eligible_permissions_dict, minimum_age, hooks, batch_size
        )
    else:
        for role in repo_able_roles:
            repoable_permissions_dict[role.arn] = _get_repoable_permissions(
                role.account,
                role.role_name,
                eligible_permissions_dict[role.arn],
                role.aa_data,
                role.no_repo_permissions,
                minimum_age,
                hooks,
            )

    for role in repo_able_roles:
        eligible_permissions = eligible_permissions_dict[role.arn]
        repoable_permissions = repoable_permissions_dict[role.arn]
        _update_repoable_services(role, repoable_permissions, eligible_permissions)
Beispiel #9
0
def repo_all_roles(account_number,
                   dynamo_table,
                   config,
                   hooks,
                   commit=False,
                   scheduled=True):
    """
    Repo all scheduled or eligible roles in an account.  Collect any errors and display them at the end.

    Args:
        account_number (string)
        dynamo_table
        config
        commit (bool): actually make the changes
        scheduled (bool): if True only repo the scheduled roles, if False repo all the (eligible) roles

    Returns:
        None
    """
    errors = []

    role_ids_in_account = role_ids_for_account(dynamo_table, account_number)
    roles = Roles([])
    for role_id in role_ids_in_account:
        roles.append(
            Role(
                get_role_data(dynamo_table,
                              role_id,
                              fields=['Active', 'RoleName', 'RepoScheduled'])))

    roles = roles.filter(active=True)

    cur_time = int(time.time())
    if scheduled:
        roles = [
            role for role in roles
            if (role.repo_scheduled and cur_time > role.repo_scheduled)
        ]

    LOGGER.info('Repoing these {}roles from account {}:\n\t{}'.format(
        'scheduled ' if scheduled else '', account_number,
        ', '.join([role.role_name for role in roles])))

    for role in roles:
        error = repo_role(account_number,
                          role.role_name,
                          dynamo_table,
                          config,
                          hooks,
                          commit=commit)
        if error:
            errors.append(error)

    if errors:
        LOGGER.error('Error(s) during repo: \n{}'.format(errors))
    else:
        LOGGER.info('Everything successful!')
Beispiel #10
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 #11
0
def find_roles_with_permission(permission, dynamo_table):
    """
    Search roles in all accounts for a policy with a given permission, log the ARN of each role with this permission

    Args:
        permission (string): The name of the permission to find

    Returns:
        None
    """
    for roleID in role_ids_for_all_accounts(dynamo_table):
        role = Role(get_role_data(dynamo_table, roleID, fields=['Policies', 'RoleName', 'Arn', 'Active']))
        permissions = roledata._get_role_permissions(role)
        if permission.lower() in permissions and role.active:
            LOGGER.info('ARN {arn} has {permission}'.format(arn=role.arn, permission=permission))
Beispiel #12
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 #13
0
def _calculate_repo_scores(roles, minimum_age, hooks):
    """
    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
        minimum_age
        hooks

    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(
                role.account, role.role_name, permissions, role.aa_data,
                role.no_repo_permissions, minimum_age, hooks)
            (repoable_permissions_set, repoable_services_set
             ) = _convert_repoable_perms_to_perms_and_services(
                 permissions, repoable_permissions)

            role.repoable_permissions = len(repoable_permissions)

            # we're going to store both repoable permissions and repoable services in the field "RepoableServices"
            role.repoable_services = repoable_services_set + repoable_permissions_set
        else:
            role.repoable_permissions = 0
            role.repoable_services = []
Beispiel #14
0
    def apply(self, input_list):
        now = datetime.datetime.now(tzlocal())
        try:
            days_delta = self.config['minimum_age']
        except KeyError:
            LOGGER.info('Minimum age not set in config, using default 90 days')
            days_delta = 90

        ago = datetime.timedelta(days=days_delta)

        too_young = []
        for role in input_list:
            if role.create_date > now - ago:
                LOGGER.info('Role {name} created too recently to cleanup. ({date})'.format(
                            name=role.role_name, date=role.create_date))
                too_young.append(role)
        return too_young
Beispiel #15
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 #16
0
def update_role_data(role, current_policy):
    """
    Compare the current version of a policy for a role and what has been previously stored in Dynamo.
      - If current and new policy versions are different store the new version in Dynamo. Add any newly added
          permissions to temporary permission blacklist. Purge any old entries from permission blacklist.
      - Refresh the updated time on the role policy
      - If the role is completely new, store the first version in Dynamo
      - Updates the role with full history of policies, including current version

    Args:
        role (Role): current role being updated
        current_policy (dict): representation of the current policy version

    Returns:
        None
    """

    # policy_entry: source, discovered, policy
    stored_role = _get_role_data(role.role_id, fields=['OptOut', 'Policies'])

    if stored_role:
        # is the policy list the same as the last we had?
        old_policy = _empty_string_from_dynamo_replace(
            stored_role['Policies'][-1]['Policy'])
        if current_policy != old_policy:
            add_new_policy_version(role, current_policy, 'Scan')
            LOGGER.info(
                '{} has different inline policies than last time, adding to role store'
                .format(role.arn))

            newly_added_permissions = find_newly_added_permissions(
                old_policy, current_policy)
        else:
            newly_added_permissions = set()

        update_no_repo_permissions(role, newly_added_permissions)
        update_opt_out(role)
        _refresh_updated_time(role.role_id)
    else:
        _store_item(role, current_policy)
        LOGGER.info('Added new role ({}): {}'.format(role.role_id, role.arn))

    role.policies = get_role_data(role.role_id,
                                  fields=['Policies']).get('Policies', [])
Beispiel #17
0
def delete_policy(name, role, account_number, conn):
    """Deletes the specified IAM Role inline policy.

    Args:
        name (string)
        role (Role object)
        account_number (string)
        conn (dict)

    Returns:
        error (string) or None
    """
    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:
        return "Error deleting policy: {} from role: {} in account {}.  Exception: {}".format(
            name, role.role_name, account_number, e)
Beispiel #18
0
def find_roles_with_permissions(permissions, dynamo_table, output_file):
    """
    Search roles in all accounts for a policy with any of the provided permissions, log the ARN of each role.

    Args:
        permissions (list[string]): The name of the permissions to find
        output_file (string): filename to write the output

    Returns:
        None
    """
    arns = list()
    for roleID in role_ids_for_all_accounts(dynamo_table):
        role = Role(
            get_role_data(
                dynamo_table, roleID, fields=["Policies", "RoleName", "Arn", "Active"]
            )
        )
        role_permissions, _ = roledata._get_role_permissions(role)

        permissions = set([p.lower() for p in permissions])
        found_permissions = permissions.intersection(role_permissions)

        if found_permissions and role.active:
            arns.append(role.arn)
            LOGGER.info(
                "ARN {arn} has {permissions}".format(
                    arn=role.arn, permissions=list(found_permissions)
                )
            )

    if not output_file:
        return

    with open(output_file, "w") as fd:
        json.dump(arns, fd)

    LOGGER.info('Ouput written to file "{output_file}"'.format(output_file=output_file))
Beispiel #19
0
def schedule_repo(account_number, dynamo_table, config):
    """
    Schedule a repo for a given account.  Schedule repo for a time in the future (default 7 days) for any roles in
    the account with repoable permissions.
    """
    scheduled_roles = []

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

    scheduled_time = int(
        time.time()) + (86400 * config.get('repo_schedule_period_days', 7))
    for role in roles:
        if role.repoable_permissions > 0:
            set_role_data(dynamo_table, role.role_id,
                          {'RepoScheduled': scheduled_time})
            scheduled_roles.append(role.role_name)

    LOGGER.info(
        "Scheduled repo for {} days from now for these roles:\n\t{}".format(
            config.get('repo_schedule_period_days', 7),
            ', '.join([r for r in scheduled_roles])))
Beispiel #20
0
def _get_repoable_permissions(role_name, permissions, aa_data,
                              no_repo_permissions, minimum_age):
    """
    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:
        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

    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
    ]

    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.info(
                    'skipping {} for role {} because it is in the no repo list'
                    .format(permission, role_name))
                continue

            repoable_permissions.add(permission.lower())

    return repoable_permissions
Beispiel #21
0
def update_role_cache(account_number, dynamo_table, config, hooks):
    """
    Update data about all roles in a given account:
      1) list all the roles and initiate a role object with basic data including name and roleID
      2) get inline policies for each of the roles
      3) build a list of active roles - we'll want to keep data about roles that may have been deleted in case we
         need to restore them, so if we used to have a role and now we don't see it we'll mark it inactive
      4) update data about the roles in Dynamo
      5) mark inactive roles in Dynamo
      6) load and instantiate filter plugins
      7) for each filter determine the list of roles that it filters
      8) update data in Dynamo about filters
      9) get Aardvark data for each role
      10) update Dynamo with Aardvark data
      11) calculate repoable permissions/policies for all the roles
      12) update Dynamo with information about how many total and repoable permissions and which services are repoable
      13) update stats in Dynamo with basic information like total permissions and which filters are applicable

    Args:
        account_number (string): The current account number Repokid is being run against

    Returns:
        None
    """
    conn = config["connection_iam"]
    conn["account_number"] = account_number

    LOGGER.info(
        "Getting current role data for account {} (this may take a while for large accounts)"
        .format(account_number))

    role_data = get_account_authorization_details(filter="Role", **conn)
    role_data_by_id = {item["RoleId"]: item for item in role_data}

    # convert policies list to dictionary to maintain consistency with old call which returned a dict
    for _, data in role_data_by_id.items():
        data["RolePolicyList"] = {
            item["PolicyName"]: item["PolicyDocument"]
            for item in data["RolePolicyList"]
        }

    roles = Roles([Role(rd) for rd in role_data])

    active_roles = []
    LOGGER.info("Updating role data for account {}".format(account_number))
    for role in tqdm(roles):
        role.account = account_number
        current_policies = role_data_by_id[role.role_id]["RolePolicyList"]
        active_roles.append(role.role_id)
        roledata.update_role_data(dynamo_table, account_number, role,
                                  current_policies)

    LOGGER.info("Finding inactive roles in account {}".format(account_number))
    roledata.find_and_mark_inactive(dynamo_table, account_number, active_roles)

    LOGGER.info("Filtering roles")
    plugins = FilterPlugins()

    # Blocklist needs to know the current account
    filter_config = config["filter_config"]
    blocklist_filter_config = filter_config.get(
        "BlocklistFilter", filter_config.get("BlacklistFilter"))
    blocklist_filter_config["current_account"] = account_number

    for plugin_path in config.get("active_filters"):
        plugin_name = plugin_path.split(":")[1]
        if plugin_name == "ExclusiveFilter":
            # ExclusiveFilter plugin active; try loading its config. Also, it requires the current account, so add it.
            exclusive_filter_config = filter_config.get("ExclusiveFilter", {})
            exclusive_filter_config["current_account"] = account_number
        plugins.load_plugin(plugin_path,
                            config=config["filter_config"].get(
                                plugin_name, None))

    for plugin in plugins.filter_plugins:
        filtered_list = plugin.apply(roles)
        class_name = plugin.__class__.__name__
        for filtered_role in filtered_list:
            LOGGER.info("Role {} filtered by {}".format(
                filtered_role.role_name, class_name))
            filtered_role.disqualified_by.append(class_name)

    for role in roles:
        set_role_data(dynamo_table, role.role_id,
                      {"DisqualifiedBy": role.disqualified_by})

    LOGGER.info(
        "Getting data from Aardvark for account {}".format(account_number))
    aardvark_data = get_aardvark_data(config["aardvark_api_location"],
                                      account_number=account_number)

    LOGGER.info("Updating roles with Aardvark data in account {}".format(
        account_number))
    for role in roles:
        try:
            role.aa_data = aardvark_data[role.arn]
        except KeyError:
            LOGGER.warning("Aardvark data not found for role: {} ({})".format(
                role.role_id, role.role_name))
        else:
            set_role_data(dynamo_table, role.role_id, {"AAData": role.aa_data})

    LOGGER.info(
        "Calculating repoable permissions and services for account {}".format(
            account_number))

    batch_processing = config.get("query_role_data_in_batch", False)
    batch_size = config.get("batch_processing_size", 100)
    roledata._calculate_repo_scores(
        roles,
        config["filter_config"]["AgeFilter"]["minimum_age"],
        hooks,
        batch_processing,
        batch_size,
    )
    for role in roles:
        LOGGER.debug(
            "Role {} in account {} has\nrepoable permissions: {}\nrepoable services: {}"
            .format(
                role.role_name,
                account_number,
                role.repoable_permissions,
                role.repoable_services,
            ))
        set_role_data(
            dynamo_table,
            role.role_id,
            {
                "TotalPermissions": role.total_permissions,
                "RepoablePermissions": role.repoable_permissions,
                "RepoableServices": role.repoable_services,
            },
        )

    LOGGER.info("Updating stats in account {}".format(account_number))
    roledata.update_stats(dynamo_table, roles, source="Scan")
Beispiel #22
0
def remove_permissions_from_role(
    account_number,
    permissions,
    role,
    role_id,
    dynamo_table,
    config,
    hooks,
    commit=False,
):
    """Remove the list of permissions from the provided role.

    Args:
        account_number (string)
        permissions (list<string>)
        role (Role object)
        role_id (string)
        commit (bool)

    Returns:
        None
    """
    repoed_policies, deleted_policy_names = roledata._get_repoed_policy(
        role.policies[-1]["Policy"], permissions)

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

    if not commit:
        log_deleted_and_repoed_policies(deleted_policy_names, repoed_policies,
                                        role.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)

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

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

    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="ManualPermissionRepo",
        add_no_repo=False,
    )
    LOGGER.info(
        "Successfully removed {permissions} from role: {role} in account {account_number}"
        .format(permissions=permissions,
                role=role.role_name,
                account_number=account_number))
Beispiel #23
0
def update_role_data(dynamo_table,
                     account_number,
                     role,
                     current_policy,
                     source="Scan",
                     add_no_repo=True):
    """
    Compare the current version of a policy for a role and what has been previously stored in Dynamo.
      - If current and new policy versions are different store the new version in Dynamo. Add any newly added
          permissions to temporary permission blocklist. Purge any old entries from permission blocklist.
      - Refresh the updated time on the role policy
      - If the role is completely new, store the first version in Dynamo
      - Updates the role with full history of policies, including current version

    Args:
        dynamo_table
        account_number
        role (Role): current role being updated
        current_policy (dict): representation of the current policy version
        source: Default 'Scan' but could be Repo, Rollback, etc

    Returns:
        None
    """

    # policy_entry: source, discovered, policy
    stored_role = get_role_data(dynamo_table,
                                role.role_id,
                                fields=["OptOut", "Policies", "Tags"])
    if not stored_role:
        role_dict = store_initial_role_data(
            dynamo_table,
            role.arn,
            role.create_date,
            role.role_id,
            role.role_name,
            account_number,
            current_policy,
            role.tags,
        )
        role.set_attributes(role_dict)
        LOGGER.info("Added new role ({}): {}".format(role.role_id, role.arn))
    else:
        # is the policy list the same as the last we had?
        old_policy = stored_role["Policies"][-1]["Policy"]
        if current_policy != old_policy:
            add_new_policy_version(dynamo_table, role, current_policy, source)
            LOGGER.info(
                "{} has different inline policies than last time, adding to role store"
                .format(role.arn))

            newly_added_permissions = find_newly_added_permissions(
                old_policy, current_policy)
        else:
            newly_added_permissions = set()

        # update tags if needed
        if role.tags != stored_role.get("Tags", []):
            set_role_data(dynamo_table, role.role_id, {"Tags": role.tags})

        if add_no_repo:
            update_no_repo_permissions(dynamo_table, role,
                                       newly_added_permissions)
        update_opt_out(dynamo_table, role)
        set_role_data(
            dynamo_table,
            role.role_id,
            {"Refreshed": datetime.datetime.utcnow().isoformat()},
        )

        # Update all data from Dynamo except CreateDate (it's in the wrong format) and DQ_by (we're going to recalc)
        current_role_data = get_role_data(dynamo_table, role.role_id)
        current_role_data.pop("CreateDate", None)
        current_role_data.pop("DisqualifiedBy", None)
        role.set_attributes(current_role_data)
Beispiel #24
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("Pushing cached policy: {} (role: {} account {})".format(
                policy_name,
                role.role_name,
                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)
            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:
                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)
                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
Beispiel #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)

    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 #26
0
def main():
    args = docopt(__doc__,
                  version="Repokid {version}".format(version=__version__))

    if args.get("config"):
        config_filename = args.get("<config_filename>")
        _generate_default_config(filename=config_filename)
        sys.exit(0)

    account_number = args.get("<account_number>")

    if not CONFIG:
        config = _generate_default_config()
    else:
        config = CONFIG

    LOGGER.debug("Repokid cli called with args {}".format(args))

    hooks = get_hooks(config.get("hooks", ["repokid.hooks.loggers"]))
    dynamo_table = dynamo_get_or_create_table(**config["dynamo_db"])

    if args.get("update_role_cache"):
        return update_role_cache(account_number, dynamo_table, config, hooks)

    if args.get("display_role_cache"):
        inactive = args.get("--inactive")
        return display_roles(account_number, dynamo_table, inactive=inactive)

    if args.get("find_roles_with_permissions"):
        permissions = args.get("<permission>")
        output_file = args.get("--output")
        return find_roles_with_permissions(permissions, dynamo_table,
                                           output_file)

    if args.get("remove_permissions_from_roles"):
        permissions = args.get("<permission>")
        role_filename = args.get("--role-file")
        commit = args.get("--commit")
        return remove_permissions_from_roles(permissions,
                                             role_filename,
                                             dynamo_table,
                                             config,
                                             hooks,
                                             commit=commit)

    if args.get("display_role"):
        role_name = args.get("<role_name>")
        return display_role(account_number, role_name, dynamo_table, config,
                            hooks)

    if args.get("repo_role"):
        role_name = args.get("<role_name>")
        commit = args.get("--commit")
        return repo_role(account_number,
                         role_name,
                         dynamo_table,
                         config,
                         hooks,
                         commit=commit)

    if args.get("rollback_role"):
        role_name = args.get("<role_name>")
        commit = args.get("--commit")
        selection = args.get("--selection")
        return rollback_role(
            account_number,
            role_name,
            dynamo_table,
            config,
            hooks,
            selection=selection,
            commit=commit,
        )

    if args.get("repo_all_roles"):
        LOGGER.info("Updating role data")
        update_role_cache(account_number, dynamo_table, config, hooks)
        LOGGER.info("Repoing all roles")
        commit = args.get("--commit")
        return repo_all_roles(account_number,
                              dynamo_table,
                              config,
                              hooks,
                              commit=commit,
                              scheduled=False)

    if args.get("schedule_repo"):
        LOGGER.info("Updating role data")
        update_role_cache(account_number, dynamo_table, config, hooks)
        return schedule_repo(account_number, dynamo_table, config, hooks)

    if args.get("show_scheduled_roles"):
        LOGGER.info("Showing scheduled roles")
        return show_scheduled_roles(account_number, dynamo_table)

    if args.get("cancel_scheduled_repo"):
        role_name = args.get("--role")
        is_all = args.get("--all")
        if not is_all:
            LOGGER.info(
                "Cancelling scheduled repo for role: {} in account {}".format(
                    role_name, account_number))
        else:
            LOGGER.info(
                "Cancelling scheduled repo for all roles in account {}".format(
                    account_number))
        return cancel_scheduled_repo(account_number,
                                     dynamo_table,
                                     role_name=role_name,
                                     is_all=is_all)

    if args.get("repo_scheduled_roles"):
        update_role_cache(account_number, dynamo_table, config, hooks)
        LOGGER.info("Repoing scheduled roles")
        commit = args.get("--commit")
        return repo_all_roles(account_number,
                              dynamo_table,
                              config,
                              hooks,
                              commit=commit,
                              scheduled=True)

    if args.get("repo_stats"):
        output_file = args.get("<output_filename>")
        account_number = args.get("--account")
        return repo_stats(output_file,
                          dynamo_table,
                          account_number=account_number)
Beispiel #27
0
def main():
    args = docopt(__doc__, version='Repokid {version}'.format(version=__version__))

    if args.get('config'):
        config_filename = args.get('<config_filename>')
        _generate_default_config(filename=config_filename)
        sys.exit(0)

    account_number = args.get('<account_number>')

    if not CONFIG:
        config = _generate_default_config()
    else:
        config = CONFIG

    LOGGER.debug('Repokid cli called with args {}'.format(args))

    hooks = _get_hooks(config.get('hooks', ['repokid.hooks.loggers']))
    dynamo_table = dynamo_get_or_create_table(**config['dynamo_db'])

    if args.get('update_role_cache'):
        return update_role_cache(account_number, dynamo_table, config, hooks)

    if args.get('display_role_cache'):
        inactive = args.get('--inactive')
        return display_roles(account_number, dynamo_table, inactive=inactive)

    if args.get('find_roles_with_permission'):
        return find_roles_with_permission(args.get('<permission>'), dynamo_table)

    if args.get('display_role'):
        role_name = args.get('<role_name>')
        return display_role(account_number, role_name, dynamo_table, config, hooks)

    if args.get('repo_role'):
        role_name = args.get('<role_name>')
        commit = args.get('--commit')
        return repo_role(account_number, role_name, dynamo_table, config, hooks, commit=commit)

    if args.get('rollback_role'):
        role_name = args.get('<role_name>')
        commit = args.get('--commit')
        selection = args.get('--selection')
        return rollback_role(account_number, role_name, dynamo_table, config, hooks, selection=selection, commit=commit)

    if args.get('repo_all_roles'):
        LOGGER.info('Updating role data')
        update_role_cache(account_number, dynamo_table, config, hooks)
        LOGGER.info('Repoing all roles')
        commit = args.get('--commit')
        return repo_all_roles(account_number, dynamo_table, config, hooks, commit=commit, scheduled=False)

    if args.get('schedule_repo'):
        LOGGER.info('Updating role data')
        update_role_cache(account_number, dynamo_table, config, hooks)
        return schedule_repo(account_number, dynamo_table, config, hooks)

    if args.get('show_scheduled_roles'):
        LOGGER.info('Showing scheduled roles')
        return show_scheduled_roles(account_number, dynamo_table)

    if args.get('cancel_scheduled_repo'):
        role_name = args.get('--role')
        is_all = args.get('--all')
        if not is_all:
            LOGGER.info('Cancelling scheduled repo for role: {} in account {}'.format(role_name, account_number))
        else:
            LOGGER.info('Cancelling scheduled repo for all roles in account {}'.format(account_number))
        return cancel_scheduled_repo(account_number, dynamo_table, role_name=role_name, is_all=is_all)

    if args.get('repo_scheduled_roles'):
        update_role_cache(account_number, dynamo_table, config, hooks)
        LOGGER.info('Repoing scheduled roles')
        commit = args.get('--commit')
        return repo_all_roles(account_number, dynamo_table, config, hooks, commit=commit, scheduled=True)

    if args.get('repo_stats'):
        output_file = args.get('<output_filename>')
        account_number = args.get('--account')
        return repo_stats(output_file, dynamo_table, account_number=account_number)
Beispiel #28
0
def update_role_cache(account_number, dynamo_table, config, hooks):
    """
    Update data about all roles in a given account:
      1) list all the roles and initiate a role object with basic data including name and roleID
      2) get inline policies for each of the roles
      3) build a list of active roles - we'll want to keep data about roles that may have been deleted in case we
         need to restore them, so if we used to have a role and now we don't see it we'll mark it inactive
      4) update data about the roles in Dynamo
      5) mark inactive roles in Dynamo
      6) load and instantiate filter plugins
      7) for each filter determine the list of roles that it filters
      8) update data in Dynamo about filters
      9) get Aardvark data for each role
      10) update Dynamo with Aardvark data
      11) calculate repoable permissions/policies for all the roles
      12) update Dynamo with information about how many total and repoable permissions and which services are repoable
      13) update stats in Dynamo with basic information like total permissions and which filters are applicable

    Args:
        account_number (string): The current account number Repokid is being run against

    Returns:
        None
    """
    conn = config['connection_iam']
    conn['account_number'] = account_number

    LOGGER.info('Getting current role data for account {} (this may take a while for large accounts)'.format(
        account_number))
    role_data = get_account_authorization_details(filter='Role', **conn)
    role_data_by_id = {item['RoleId']: item for item in role_data}

    # convert policies list to dictionary to maintain consistency with old call which returned a dict
    for _, data in role_data_by_id.items():
        data['RolePolicyList'] = {item['PolicyName']: item['PolicyDocument'] for item in data['RolePolicyList']}

    roles = Roles([Role(rd) for rd in role_data])

    active_roles = []
    LOGGER.info('Updating role data for account {}'.format(account_number))
    for role in tqdm(roles):
        role.account = account_number
        current_policies = role_data_by_id[role.role_id]['RolePolicyList']
        active_roles.append(role.role_id)
        roledata.update_role_data(dynamo_table, account_number, role, current_policies)

    LOGGER.info('Finding inactive roles in account {}'.format(account_number))
    roledata.find_and_mark_inactive(dynamo_table, account_number, active_roles)

    LOGGER.info('Filtering roles')
    plugins = FilterPlugins()

    # Blacklist needs to know the current account
    config['filter_config']['BlacklistFilter']['current_account'] = account_number

    for plugin_path in config.get('active_filters'):
        plugin_name = plugin_path.split(':')[1]
        plugins.load_plugin(plugin_path, config=config['filter_config'].get(plugin_name, None))

    for plugin in plugins.filter_plugins:
        filtered_list = plugin.apply(roles)
        class_name = plugin.__class__.__name__
        for filtered_role in filtered_list:
            LOGGER.info('Role {} filtered by {}'.format(filtered_role.role_name, class_name))
            filtered_role.disqualified_by.append(class_name)

    for role in roles:
        set_role_data(dynamo_table, role.role_id, {'DisqualifiedBy': role.disqualified_by})

    LOGGER.info('Getting data from Aardvark for account {}'.format(account_number))
    aardvark_data = _get_aardvark_data(config['aardvark_api_location'], account_number=account_number)

    LOGGER.info('Updating roles with Aardvark data in account {}'.format(account_number))
    for role in roles:
        try:
            role.aa_data = aardvark_data[role.arn]
        except KeyError:
            LOGGER.warning('Aardvark data not found for role: {} ({})'.format(role.role_id, role.role_name))
        else:
            set_role_data(dynamo_table, role.role_id, {'AAData': role.aa_data})

    LOGGER.info('Calculating repoable permissions and services for account {}'.format(account_number))
    roledata._calculate_repo_scores(roles, config['filter_config']['AgeFilter']['minimum_age'], hooks)
    for role in roles:
        LOGGER.debug('Role {} in account {} has\nrepoable permissions: {}\nrepoable services:'.format(
            role.role_name, account_number, role.repoable_permissions, role.repoable_services
        ))
        set_role_data(dynamo_table, role.role_id, {'TotalPermissions': role.total_permissions,
                                                   'RepoablePermissions': role.repoable_permissions,
                                                   'RepoableServices': role.repoable_services})

    LOGGER.info('Updating stats in account {}'.format(account_number))
    roledata.update_stats(dynamo_table, roles, source='Scan')