def _rollback_role(account_number, role_name, dynamo_table, config, hooks, selection=None, commit=None): """ Display the historical policy versions for a roll as a numbered list. Restore to a specific version if selected. Indicate changes that will be made and then actually make them if commit is selected. Args: account_number (string) role_name (string) selection (int): which policy version in the list to rollback to commit (bool): actually make the change Returns: errors (list): if any """ errors = [] role_id = find_role_in_cache(dynamo_table, account_number, role_name) if not role_id: message = "Could not find role with name {} in account {}".format( role_name, account_number) errors.append(message) LOGGER.warning(message) return errors else: role = Role(get_role_data(dynamo_table, role_id)) # no option selected, display a table of options if not selection: headers = ["Number", "Source", "Discovered", "Permissions", "Services"] rows = [] for index, policies_version in enumerate(role.policies): policy_permissions, _ = roledata._get_permissions_in_policy( policies_version["Policy"]) rows.append([ index, policies_version["Source"], policies_version["Discovered"], len(policy_permissions), roledata._get_services_in_permissions(policy_permissions), ]) print(tabulate(rows, headers=headers)) return conn = config["connection_iam"] conn["account_number"] = account_number current_policies = get_role_inline_policies(role.as_dict(), **conn) if selection: pp = pprint.PrettyPrinter() print("Will restore the following policies:") pp.pprint(role.policies[int(selection)]["Policy"]) print("Current policies:") pp.pprint(current_policies) current_permissions, _ = roledata._get_permissions_in_policy( role.policies[-1]["Policy"]) selected_permissions, _ = roledata._get_permissions_in_policy( role.policies[int(selection)]["Policy"]) restored_permissions = selected_permissions - current_permissions print("\nResore will return these permissions:") print("\n".join([perm for perm in sorted(restored_permissions)])) if not commit: return False # if we're restoring from a version with fewer policies than we have now, we need to remove them to # complete the restore. To do so we'll store all the policy names we currently have and remove them # from the list as we update. Any policy names left need to be manually removed policies_to_remove = current_policies.keys() for policy_name, policy in role.policies[int(selection)]["Policy"].items(): try: LOGGER.info( f"Pushing cached policy: {policy_name} (role: {role.role_name} account {account_number})" ) put_role_policy( RoleName=role.role_name, PolicyName=policy_name, PolicyDocument=json.dumps(policy, indent=2, sort_keys=True), **conn, ) except botocore.exceptions.ClientError as e: message = "Unable to push policy {}. Error: {} (role: {} account {})".format( policy_name, e.message, role.role_name, account_number) LOGGER.error(message, exc_info=True) errors.append(message) else: # remove the policy name if it's in the list try: policies_to_remove.remove(policy_name) except Exception: # nosec pass if policies_to_remove: for policy_name in policies_to_remove: try: LOGGER.info( f"Deleting policy {policy_name} for rollback (role: {role.role_name} account {account_number})" ) delete_role_policy(RoleName=role.role_name, PolicyName=policy_name, **conn) except botocore.excpetions.ClientError as e: message = "Unable to delete policy {}. Error: {} (role: {} account {})".format( policy_name, e.message, role.role_name, account_number) LOGGER.error(message, exc_info=True) errors.append(message) partial_update_role_data( role, dynamo_table, account_number, config, conn, hooks, source="Restore", add_no_repo=False, ) if not errors: LOGGER.info( f"Successfully restored selected version {selection} of role policies (role: {role.role_name} " f"account: {account_number}") return errors
def rollback_role(account_number, role_name, dynamo_table, config, hooks, selection=None, commit=None): """ Display the historical policy versions for a roll as a numbered list. Restore to a specific version if selected. Indicate changes that will be made and then actually make them if commit is selected. Args: account_number (string) role_name (string) selection (int): which policy version in the list to rollback to commit (bool): actually make the change Returns: errors (list): if any """ errors = [] role_id = find_role_in_cache(dynamo_table, account_number, role_name) if not role_id: message = 'Could not find role with name {} in account {}'.format(role_name, account_number) errors.append(message) LOGGER.warning(message) return errors else: role = Role(get_role_data(dynamo_table, role_id)) # no option selected, display a table of options if not selection: headers = ['Number', 'Source', 'Discovered', 'Permissions', 'Services'] rows = [] for index, policies_version in enumerate(role.policies): policy_permissions = roledata._get_permissions_in_policy(policies_version['Policy']) rows.append([index, policies_version['Source'], policies_version['Discovered'], len(policy_permissions), roledata._get_services_in_permissions(policy_permissions)]) print tabulate(rows, headers=headers) return from cloudaux import CloudAux conn = config['connection_iam'] conn['account_number'] = account_number ca = CloudAux(**conn) current_policies = get_role_inline_policies(role.as_dict(), **conn) if selection: pp = pprint.PrettyPrinter() print "Will restore the following policies:" pp.pprint(role.policies[int(selection)]['Policy']) print "Current policies:" pp.pprint(current_policies) current_permissions = roledata._get_permissions_in_policy(role.policies[-1]['Policy']) selected_permissions = roledata._get_permissions_in_policy(role.policies[int(selection)]['Policy']) restored_permissions = selected_permissions - current_permissions print "\nResore will return these permissions:" print '\n'.join([perm for perm in sorted(restored_permissions)]) if not commit: return False # if we're restoring from a version with fewer policies than we have now, we need to remove them to # complete the restore. To do so we'll store all the policy names we currently have and remove them # from the list as we update. Any policy names left need to be manually removed policies_to_remove = current_policies.keys() for policy_name, policy in role.policies[int(selection)]['Policy'].items(): try: LOGGER.info("Pushing cached policy: {} (role: {} account {})".format( policy_name, role.role_name, account_number)) ca.call('iam.client.put_role_policy', RoleName=role.role_name, PolicyName=policy_name, PolicyDocument=json.dumps(policy, indent=2, sort_keys=True)) except botocore.exceptions.ClientError as e: message = "Unable to push policy {}. Error: {} (role: {} account {})".format( policy_name, e.message, role.role_name, account_number) LOGGER.error(message) errors.append(message) else: # remove the policy name if it's in the list try: policies_to_remove.remove(policy_name) except Exception: pass if policies_to_remove: for policy_name in policies_to_remove: try: ca.call('iam.client.delete_role_policy', RoleName=role.role_name, PolicyName=policy_name) except botocore.excpetions.ClientError as e: message = "Unable to delete policy {}. Error: {} (role: {} account {})".format( policy_name, e.message, role.role_name, account_number) LOGGER.error(message) errors.append(message) _update_role_data(role, dynamo_table, account_number, config, conn, hooks, source='Restore', add_no_repo=False) if not errors: LOGGER.info('Successfully restored selected version of role policies (role: {} account: {})'.format( role.role_name, account_number)) return errors
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 _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) )