Beispiel #1
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.warning("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.warning("skipping {}".format(permission_name))
                continue

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

    return potentially_repoable_permissions
Beispiel #2
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 #3
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 #4
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 #5
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 #6
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')
def repo_cloud_trail(input_dict):
    # input_dict: account_number, role_name, potentially_repoable_permissions, minimum_age
    if input_dict['minimum_age'] > len(INDEXES):
        LOGGER.warning(
            "Cloudtrail doesn't have {} days worth of data, skipping")
        return input_dict

    try:
        ct_used_actions = ct_utils.actor_usage(
            input_dict['role_name'], input_dict['account_number'], 'iamrole',
            INDEXES[:input_dict['minimum_age']], 'anything_but_denied')
    except Exception as e:
        LOGGER.warning(
            "Unable to retrieve Cloudtrail data for role {}: {}".format(
                input_dict['role_name'], e))
        return input_dict

    ct_used_actions = [action.lower() for action in ct_used_actions]

    # we don't want to repo any permissions that have been observed inside our minimum age window because
    # the action could have been used, but cloudtrail support didn't exist
    observed_cutoff_time = int(time.time() -
                               (input_dict['minimum_age'] * 86400))
    # valid actions for repo are those that have been observed before our cutoff time, chop off the date version if
    # present
    repoable_actions = set([
        action.split('20')[0]
        for action, observed in CT_OBSERVED_ACTIONS.items()
        if observed < observed_cutoff_time
    ])

    # filter out S3 actions that aren't in the whitelist
    repoable_actions = set([
        action for action in repoable_actions
        if (not action.startswith('s3:') or action in S3_REPO_WHITE_LIST)
    ])

    # actions are repoable if we have seen them before but don't see them for this role for time period and
    # they aren't in NO_REPO
    ct_says_removable = [
        action for action in repoable_actions
        if action not in ct_used_actions and action not in CT_NO_REPO
    ]

    # attempt to get CT -> action mapping, fall back on action, if mapping doesn't exist fall back to the action name
    repokid_ct_says_removable = [
        CT_TO_POLICY_MAP.get(ct_action, ct_action).lower()
        for ct_action in ct_says_removable
    ]

    for permission_name, permission_decision in input_dict[
            'potentially_repoable_permissions'].items():
        if permission_name in ct_used_actions and permission_decision.repoable:
            LOGGER.warning(
                'Cloudtrail plugin disagrees with {} about permission: {} for role {}!  '
                'CT says used, {} says not'.format(
                    permission_decision.decider, permission_name,
                    input_dict['role_name'], permission_decision.decider))

        elif permission_name in repokid_ct_says_removable:
            permission_decision.repoable = True
            permission_decision.decider = ('Cloudtrail'
                                           if not permission_decision.decider
                                           else permission_decision.decider +
                                           ", Cloudtrail")

    # TODO: there is a contingency to be aware of - if Access Advisor says something in a service is used, cloudtrail
    # might have missed the call.  So if we're removing all of a service that access advisor says keep we might want
    # to check

    return input_dict
Beispiel #8
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)
        )