def _get_permissions_in_policy(policy_dict, warn_unknown_perms=False): """ Given a set of policies for a role, return a set of all allowed permissions Args: policy_dict warn_unknown_perms Returns set - all permissions allowed by the policies """ permissions = set() for policy_name, policy in policy_dict.items(): policy = expand_policy(policy=policy, expand_deny=False) for statement in policy.get('Statement'): if statement['Effect'].lower() == 'allow': permissions = permissions.union( get_actions_from_statement(statement)) weird_permissions = permissions.difference(all_permissions) if weird_permissions and warn_unknown_perms: LOGGER.warn('Unknown permissions found: {}'.format(weird_permissions)) return permissions
def _get_permissions_in_policy(policy_dict, warn_unknown_perms=False): """ Given a set of policies for a role, return a set of all allowed permissions Args: policy_dict warn_unknown_perms Returns tuple set - all permissions allowed by the policies set - all permisisons allowed by the policies not marked with STATEMENT_SKIP_SID """ total_permissions = set() eligible_permissions = set() for policy_name, policy in list(policy_dict.items()): policy = expand_policy(policy=policy, expand_deny=False) for statement in policy.get("Statement"): if statement["Effect"].lower() == "allow": total_permissions = total_permissions.union( get_actions_from_statement(statement)) if not ("Sid" in statement and statement["Sid"].startswith(STATEMENT_SKIP_SID)): # No Sid # Sid exists, but doesn't start with STATEMENT_SKIP_SID eligible_permissions = eligible_permissions.union( get_actions_from_statement(statement)) weird_permissions = total_permissions.difference(all_permissions) if weird_permissions and warn_unknown_perms: LOGGER.warn("Unknown permissions found: {}".format(weird_permissions)) return total_permissions, eligible_permissions
def _get_role_permissions(role, warn_unknown_perms=False): """ Expand the most recent version of policies from a role to produce a list of all the permissions that are allowed (permission is included in one or more statements that is allowed). To perform expansion the policyuniverse library is used. The result is a list of all of the individual permissions that are allowed in any of the statements. If our resultant list contains any permissions that aren't listed in the master list of permissions we'll raise an exception with the set of unknown permissions found. Args: role (Role): The role object that we're getting a list of permissions for Returns: set: A set of permissions that the role has policies that allow """ permissions = set() for policy_name, policy in role.policies[-1]['Policy'].items(): policy = expand_policy(policy=policy, expand_deny=False) for statement in policy.get('Statement'): if statement['Effect'].lower() == 'allow': permissions = permissions.union( get_actions_from_statement(statement)) weird_permissions = permissions.difference(all_permissions) if weird_permissions and warn_unknown_perms: LOGGER.warn('Unknown permissions found: {}'.format(weird_permissions)) return permissions
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))
def _get_potentially_repoable_permissions(role_name, account_number, aa_data, permissions, no_repo_permissions, minimum_age): ago = datetime.timedelta(minimum_age) now = datetime.datetime.now(tzlocal()) current_time = time.time() no_repo_list = [ perm.lower() for perm in no_repo_permissions if no_repo_permissions[perm] > current_time ] # cast all permissions to lowercase permissions = [permission.lower() for permission in permissions] potentially_repoable_permissions = { permission: RepoablePermissionDecision() for permission in permissions if permission not in no_repo_list } used_services = set() for service in aa_data: (accessed, valid_authenticated) = _get_epoch_authenticated( service["lastAuthenticated"]) if not accessed: continue if not valid_authenticated: LOGGER.error( "Got malformed Access Advisor data for {role_name} in {account_number} for service {service}" ": {last_authenticated}".format( role_name=role_name, account_number=account_number, service=service.get("serviceNamespace"), last_authenticated=service["lastAuthenticated"], )) used_services.add(service["serviceNamespace"]) accessed = datetime.datetime.fromtimestamp(accessed, tzlocal()) if accessed > now - ago: used_services.add(service["serviceNamespace"]) for permission_name, permission_decision in list( potentially_repoable_permissions.items()): if permission_name.split( ":")[0] in IAM_ACCESS_ADVISOR_UNSUPPORTED_SERVICES: LOGGER.warn("skipping {}".format(permission_name)) continue # we have an unused service but need to make sure it's repoable if permission_name.split(":")[0] not in used_services: if permission_name in IAM_ACCESS_ADVISOR_UNSUPPORTED_ACTIONS: LOGGER.warn("skipping {}".format(permission_name)) continue permission_decision.repoable = True permission_decision.decider = "Access Advisor" return potentially_repoable_permissions
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)))
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)
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))
def _get_repoable_permissions(permissions, aa_data, no_repo_permissions): """ Generate a list of repoable permissions for a role based on the list of all permissions the role's policies currently allow and Access Advisor data for the services included in the role's policies. The first step is to come up with a list of services that were used within the time threshold (the same defined) in the age filter config. Permissions are repoable if they aren't in the used list, aren't in the global list of unsupported services/actions (IAM_ACCESS_ADVISOR_UNSUPPORTED_SERVICES, IAM_ACCESS_ADVISOR_UNSUPPORTED_ACTIONS), and aren't being temporarily ignored because they're on the no_repo_permissions list (newly added). Args: permissions (list): The full list of permissions that the role's permissions allow aa_data (list): A list of Access Advisor data for a role. Each element is a dictionary with a couple required attributes: lastAuthenticated (epoch time in milliseconds when the service was last used and serviceNamespace (the service used) no_repo_permissions (dict): Keys are the name of permissions and values are the time the entry expires Returns: set: Permissions that are 'repoable' (not used within the time threshold) """ ago = datetime.timedelta( CONFIG['filter_config']['AgeFilter']['minimum_age']) now = datetime.datetime.now(tzlocal()) current_time = time.time() no_repo_list = [ perm.lower() for perm in no_repo_permissions if no_repo_permissions[perm] > current_time ] used_services = set() for service in aa_data: accessed = service['lastAuthenticated'] if not accessed: continue accessed = datetime.datetime.fromtimestamp(accessed / 1000, tzlocal()) if accessed > now - ago: used_services.add(service['serviceNamespace']) repoable_permissions = set() for permission in permissions: if permission.split(':')[0] in IAM_ACCESS_ADVISOR_UNSUPPORTED_SERVICES: LOGGER.warn('skipping {}'.format(permission)) continue # we have an unused service but need to make sure it's repoable if permission.split(':')[0] not in used_services: if permission.lower() in IAM_ACCESS_ADVISOR_UNSUPPORTED_ACTIONS: LOGGER.warn('skipping {}'.format(permission)) continue if permission.lower() in no_repo_list: LOGGER.warn( 'skipping {} because it is in the no repo list'.format( permission)) continue repoable_permissions.add(permission.lower()) return repoable_permissions
def _get_repoable_permissions(account_number, role_name, permissions, aa_data, no_repo_permissions, minimum_age, hooks): """ Generate a list of repoable permissions for a role based on the list of all permissions the role's policies currently allow and Access Advisor data for the services included in the role's policies. The first step is to come up with a list of services that were used within the time threshold (the same defined) in the age filter config. Permissions are repoable if they aren't in the used list, aren't in the constant list of unsupported services/actions (IAM_ACCESS_ADVISOR_UNSUPPORTED_SERVICES, IAM_ACCESS_ADVISOR_UNSUPPORTED_ACTIONS), and aren't being temporarily ignored because they're on the no_repo_permissions list (newly added). Args: account_number role_name permissions (list): The full list of permissions that the role's permissions allow aa_data (list): A list of Access Advisor data for a role. Each element is a dictionary with a couple required attributes: lastAuthenticated (epoch time in milliseconds when the service was last used and serviceNamespace (the service used) no_repo_permissions (dict): Keys are the name of permissions and values are the time the entry expires minimum_age: Minimum age of a role (in days) for it to be repoable hooks: Dict containing hook names and functions to run Returns: set: Permissions that are 'repoable' (not used within the time threshold) """ ago = datetime.timedelta(minimum_age) now = datetime.datetime.now(tzlocal()) current_time = time.time() no_repo_list = [ perm.lower() for perm in no_repo_permissions if no_repo_permissions[perm] > current_time ] # cast all permissions to lowercase permissions = [permission.lower() for permission in permissions] potentially_repoable_permissions = { permission: RepoablePermissionDecision() for permission in permissions if permission not in no_repo_list } used_services = set() for service in aa_data: accessed = service['lastAuthenticated'] if not accessed: continue accessed = datetime.datetime.fromtimestamp(accessed / 1000, tzlocal()) if accessed > now - ago: used_services.add(service['serviceNamespace']) for permission_name, permission_decision in potentially_repoable_permissions.items( ): if permission_name.split( ':')[0] in IAM_ACCESS_ADVISOR_UNSUPPORTED_SERVICES: LOGGER.warn('skipping {}'.format(permission_name)) continue # we have an unused service but need to make sure it's repoable if permission_name.split(':')[0] not in used_services: if permission_name in IAM_ACCESS_ADVISOR_UNSUPPORTED_ACTIONS: LOGGER.warn('skipping {}'.format(permission_name)) continue permission_decision.repoable = True permission_decision.decider = 'Access Advisor' hooks_output = repokid.hooks.call_hooks( hooks, 'DURING_REPOABLE_CALCULATION', { 'account_number': account_number, 'role_name': role_name, 'potentially_repoable_permissions': potentially_repoable_permissions, 'minimum_age': minimum_age }) # TODO: make option to show source of repoable? return set([ permission_name for permission_name, permission_value in hooks_output['potentially_repoable_permissions'].items() if permission_value.repoable ])
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
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))
def rollback_role(account_number, role_name, dynamo_table, config, hooks, selection=None, commit=None): """ Display the historical policy versions for a roll as a numbered list. Restore to a specific version if selected. Indicate changes that will be made and then actually make them if commit is selected. Args: account_number (string) role_name (string) selection (int): which policy version in the list to rollback to commit (bool): actually make the change Returns: errors (list): if any """ errors = [] role_id = find_role_in_cache(dynamo_table, account_number, role_name) if not role_id: message = 'Could not find role with name {}'.format(role_name) errors.append(message) LOGGER.warn(message) return errors else: role = Role(get_role_data(dynamo_table, role_id)) # no option selected, display a table of options if not selection: headers = ['Number', 'Source', 'Discovered', 'Permissions', 'Services'] rows = [] for index, policies_version in enumerate(role.policies): policy_permissions = roledata._get_permissions_in_policy( policies_version['Policy']) rows.append([ index, policies_version['Source'], policies_version['Discovered'], len(policy_permissions), roledata._get_services_in_permissions(policy_permissions) ]) print tabulate(rows, headers=headers) return from cloudaux import CloudAux conn = config['connection_iam'] conn['account_number'] = account_number ca = CloudAux(**conn) current_policies = get_role_inline_policies(role.as_dict(), **conn) if selection: pp = pprint.PrettyPrinter() print "Will restore the following policies:" pp.pprint(role.policies[int(selection)]['Policy']) print "Current policies:" pp.pprint(current_policies) current_permissions = roledata._get_permissions_in_policy( role.policies[-1]['Policy']) selected_permissions = roledata._get_permissions_in_policy( role.policies[int(selection)]['Policy']) restored_permissions = selected_permissions - current_permissions print "\nResore will return these permissions:" print '\n'.join([perm for perm in sorted(restored_permissions)]) if not commit: return False # if we're restoring from a version with fewer policies than we have now, we need to remove them to # complete the restore. To do so we'll store all the policy names we currently have and remove them # from the list as we update. Any policy names left need to be manually removed policies_to_remove = current_policies.keys() for policy_name, policy in role.policies[int(selection)]['Policy'].items(): try: LOGGER.info("Pushing cached policy: {}".format(policy_name)) ca.call('iam.client.put_role_policy', RoleName=role.role_name, PolicyName=policy_name, PolicyDocument=json.dumps(policy, indent=2, sort_keys=True)) except botocore.exceptions.ClientError as e: message = "Unable to push policy {}. Error: {}".format( policy_name, e.message) LOGGER.error(message) errors.append(message) else: # remove the policy name if it's in the list try: policies_to_remove.remove(policy_name) except Exception: pass if policies_to_remove: for policy_name in policies_to_remove: try: ca.call('iam.client.delete_role_policy', RoleName=role.role_name, PolicyName=policy_name) except botocore.excpetions.ClientError as e: message = "Unable to delete policy {}. Error: {}".format( policy_name, e.message) LOGGER.error(message) errors.append(message) _update_role_data(role, dynamo_table, account_number, config, conn, hooks, source='Restore', add_no_repo=False) if not errors: LOGGER.info('Successfully restored selected version of role policies') return errors
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) )