def set_sqs_policy(self, event, policy): conn_details = self.conn_details_from_event(event) ca = CloudAux(**conn_details) if policy: policy = json.dumps(policy) ca.call('sqs.client.set_queue_attributes', QueueUrl=self.queue_url_from_event(event), Attributes=dict(Policy=policy))
def _list_org_roots(ca: CloudAux, **kwargs) -> List[Dict[str, Any]]: """Wrapper for organizations:ListRoots Args: ca: CloudAux instance """ return ca.call("organizations.client.list_roots", **kwargs)
def _list_service_control_policies(ca: CloudAux, **kwargs) -> List[Dict]: """Return a complete list of service control policy metadata dicts from the paginated ListPolicies API call Args: ca: CloudAux instance """ return ca.call("organizations.client.list_policies", Filter="SERVICE_CONTROL_POLICY", MaxResults=20, **kwargs)
def _describe_ou(ca: CloudAux, ou_id: str, **kwargs) -> Dict[str, str]: """Wrapper for organizations:DescribeOrganizationalUnit Args: ca: CloudAux instance ou_id: organizational unit ID """ result = ca.call("organizations.client.describe_organizational_unit", OrganizationalUnitId=ou_id, **kwargs) return result.get("OrganizationalUnit")
def _describe_account(ca: CloudAux, account_id: str, **kwargs) -> Dict[str, str]: """Wrapper for organizations:DescribeAccount Args: ca: CloudAux instance account_id: AWS account ID """ result = ca.call( "organizations.client.describe_account", AccountId=account_id, **kwargs ) return result.get("Account")
def _list_targets_for_policy(ca: CloudAux, scp_id: str, **kwargs) -> List[Dict[str, str]]: """Return a complete list of target metadata dicts from the paginated ListTargetsForPolicy API call Args: ca: CloudAux instance scp_id: service control policy ID """ return ca.call("organizations.client.list_targets_for_policy", PolicyId=scp_id, MaxResults=20, **kwargs)
def _list_children_for_ou(ca: CloudAux, parent_id: str, child_type: Literal["ACCOUNT", "ORGANIZATIONAL_UNIT"], **kwargs) -> List[Dict[str, Any]]: """Wrapper for organizations:ListChildren Args: ca: CloudAux instance parent_id: ID of organization root or organizational unit child_type: ACCOUNT or ORGANIZATIONAL_UNIT """ return ca.call("organizations.client.list_children", ChildType=child_type, ParentId=parent_id, **kwargs)
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 repo_role(account_number, role_name, dynamo_table, config, hooks, commit=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) 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 ca = CloudAux(**conn) for name in deleted_policy_names: LOGGER.info('Deleting policy with name {} from {} in account {}'.format(name, role.role_name, account_number)) try: ca.call('iam.client.delete_role_policy', RoleName=role.role_name, PolicyName=name) 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: 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: 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}) 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 rollback_role(account_number, role_name, selection=None, commit=None): role_data = _find_role_in_cache(role_name) if not role_data: LOGGER.error("Couldn't find role ({}) in cache".format(role_name)) return # no option selected, display a table of options if not selection: headers = [ 'Number', 'Source', 'Discovered', 'Policy Length', 'Policy Contents' ] rows = [] for index, policies_version in enumerate(role_data['Policies']): rows.append([ index, policies_version['Source'], policies_version['Discovered'], len(str(policies_version['Policy'])), str(policies_version['Policy'])[:50] ]) 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_data, **conn) if selection and not commit: pp = pprint.PrettyPrinter() print "Will restore the following policies:" pp.pprint(role_data['Policies'][int(selection)]['Policy']) print "Current policies:" pp.pprint(current_policies) return # 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_data['Policies'][int( selection)]['Policy'].items(): try: LOGGER.info("Pushing cached policy: {}".format(policy_name)) ca.call('iam.client.put_role_policy', RoleName=role_data['RoleName'], PolicyName=policy_name, PolicyDocument=json.dumps(policy, indent=2, sort_keys=True)) except Exception as e: LOGGER.error("Unable to push policy {}. Error: {}".format( policy_name, e.message)) else: # remove the policy name if it's in the list try: policies_to_remove.remove(policy_name) except: pass if policies_to_remove: for policy_name in policies_to_remove: try: ca.call('iam.client.delete_role_policy', RoleName=role_data['RoleName'], PolicyName=policy_name) except Exception as e: LOGGER.error("Unable to delete policy {}. Error: {}".format( policy_name, e.message)) role_data['policies'] = get_role_inline_policies(role_data, **conn) or {} roledata.add_new_policy_version(role_data, 'Restore') LOGGER.info('Successfully restored selected version of role policies')
def repo_role(account_number, role_name, commit=False): role_data = _find_role_in_cache(role_name) if not role_data: LOGGER.error('Could not find role with name {}'.format(role_name)) return disqualified_by = role_data.get('DisqualifiedBy', []) if len(disqualified_by) > 0: LOGGER.info( 'Cannot repo role {} because it is being disqualified by: {}'. format(role_name, disqualified_by)) return if 'AAData' not in role_data: LOGGER.warn('ARN not found in Access Advisor: {}'.format( role_data['Arn'])) return old_aa_data_services = [] for aa_service in role_data['AAData']: 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: {}'.format( old_aa_data_services)) return permissions = _get_role_permissions(role_data) repoable_permissions = _get_repoable_permissions(permissions, role_data) repoed_policies, deleted_policy_names = _get_repoed_policy( role_data, repoable_permissions) policies_length = len(json.dumps(repoed_policies)) if not commit: for name in deleted_policy_names: LOGGER.info('Would delete policy from {} with name {}'.format( role_name, name)) if repoed_policies: LOGGER.info('Would replace policies for role {} with: \n{}'.format( role_name, json.dumps(repoed_policies, indent=2, sort_keys=True))) if policies_length > MAX_AWS_POLICY_SIZE: LOGGER.error( "Policies would exceed the AWS size limit after repo for role: {}. " "Please manually minify.".format(role_name)) return from cloudaux import CloudAux conn = CONFIG['connection_iam'] conn['account_number'] = account_number ca = CloudAux(**conn) if policies_length > MAX_AWS_POLICY_SIZE: LOGGER.error( "Policies would exceed the AWS size limit after repo for role: {}. " "Please manually minify.".format(role_name)) return for name in deleted_policy_names: LOGGER.info('Deleting policy with name {} from {}'.format( name, role_data['RoleName'])) ca.call('iam.client.delete_role_policy', RoleName=role_data['RoleName'], PolicyName=name) if repoed_policies: LOGGER.info('Replacing Policies With: \n{}'.format( json.dumps(repoed_policies, indent=2, sort_keys=True))) for policy_name, policy in repoed_policies.items(): try: ca.call('iam.client.put_role_policy', RoleName=role_data['RoleName'], PolicyName=policy_name, PolicyDocument=json.dumps(policy, indent=2, sort_keys=True)) except Exception as e: LOGGER.error( 'Exception calling PutRolePolicy on {role}/{policy}\n{e}\n' .format(role=role_data['RoleName'], policy=policy_name, e=str(e))) return role_data['policies'] = get_role_inline_policies(role_data, **conn) or {} roledata.add_new_policy_version(role_data, 'Repo') roledata.set_repoed(role_data['RoleId']) # update total permissions count for stats permissions_count = len(_get_role_permissions(role_data)) roledata.update_total_permissions(role_data['RoleId'], permissions_count) roledata.update_stats(source='Repo', roleID=role_data['RoleId']) LOGGER.info('Successfully repoed role: {}'.format(role_data['RoleName']))
def rollback_role(account_number, role_name, 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: None """ role_data = _find_role_in_cache(account_number, role_name) if not role_data: LOGGER.warn('Could not find role with name {}'.format(role_name)) return else: role = Role(role_data) # no option selected, display a table of options if not selection: headers = [ 'Number', 'Source', 'Discovered', 'Policy Length', 'Policy Contents' ] rows = [] for index, policies_version in enumerate(role.policies): rows.append([ index, policies_version['Source'], policies_version['Discovered'], len(str(policies_version['Policy'])), str(policies_version['Policy'])[:50] ]) 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 and not commit: pp = pprint.PrettyPrinter() print "Will restore the following policies:" pp.pprint(role.policies[int(selection)]['Policy']) print "Current policies:" pp.pprint(current_policies) return # 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.excpetion.ClientError as e: LOGGER.error("Unable to push policy {}. Error: {}".format( policy_name, e.message)) else: # remove the policy name if it's in the list try: policies_to_remove.remove(policy_name) except: 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.excpetion.ClientError as e: LOGGER.error("Unable to delete policy {}. Error: {}".format( policy_name, e.message)) # TODO: possibly update the total and repoable permissions here, we'd have to get Aardvark and all that current_policies = get_role_inline_policies(role.as_dict(), **conn) or {} roledata.add_new_policy_version(role, current_policies, 'Restore') role.total_permissions = len(roledata._get_role_permissions(role)) # update stats roledata.update_stats([role], source='Restore') LOGGER.info('Successfully restored selected version of role policies')
def repo_role(account_number, role_name, commit=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_data = _find_role_in_cache(account_number, role_name) 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 {} because it is being disqualified by: {}'. format(role_name, role.disqualified_by)) return if not role.aa_data: LOGGER.warn('ARN not found in Access Advisor: {}'.format(role.arn)) return if not role.repoable_permissions: LOGGER.info('No permissions to repo for role {}'.format(role_name)) return 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: {}'.format( old_aa_data_services)) return permissions = roledata._get_role_permissions(role) repoable_permissions = roledata._get_repoable_permissions( permissions, role.aa_data, role.no_repo_permissions) repoed_policies, deleted_policy_names = roledata._get_repoed_policy( role.policies[-1]['Policy'], repoable_permissions) policies_length = len(json.dumps(repoed_policies)) if not commit: for name in deleted_policy_names: LOGGER.info('Would delete policy from {} with name {}'.format( role_name, name)) if repoed_policies: LOGGER.info('Would replace policies for role {} with: \n{}'.format( role_name, json.dumps(repoed_policies, indent=2, sort_keys=True))) if policies_length > MAX_AWS_POLICY_SIZE: LOGGER.error( "Policies would exceed the AWS size limit after repo for role: {}. " "Please manually minify.".format(role_name)) return from cloudaux import CloudAux conn = CONFIG['connection_iam'] conn['account_number'] = account_number ca = CloudAux(**conn) if policies_length > MAX_AWS_POLICY_SIZE: LOGGER.error( "Policies would exceed the AWS size limit after repo for role: {}. " "Please manually minify.".format(role_name)) return for name in deleted_policy_names: LOGGER.info('Deleting policy with name {} from {}'.format( name, role.role_name)) try: ca.call('iam.client.delete_role_policy', RoleName=role.role_name, PolicyName=name) except botocore.exceptions.ClientError as e: error = 'Error deleting policy: {} from role: {}. Exception: {}'.format( name, role.role_name, e) LOGGER.error(error) errors.append(error) if repoed_policies: LOGGER.info('Replacing Policies With: \n{}'.format( json.dumps(repoed_policies, indent=2, sort_keys=True))) for policy_name, policy in repoed_policies.items(): try: 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: error = 'Exception calling PutRolePolicy on {role}/{policy}\n{e}\n'.format( role=role.role_name, policy=policy_name, 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(role, current_policies, 'Repo') if not errors: roledata.set_repoed(role.role_id) # update total and repoable permissions and services role.total_permissions = len(roledata._get_role_permissions(role)) role.repoable_permissions = 0 role.repoable_services = [] roledata.update_repoable_data([role]) # update stats roledata.update_stats([role], source='Repo') LOGGER.info('Successfully repoed role: {}'.format(role.role_name)) return errors