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) continuing = True 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)) continuing = False if not role.aa_data: LOGGER.warning("ARN not found in Access Advisor: {}".format(role.arn)) continuing = False if not role.repoable_permissions: LOGGER.info("No permissions to repo for role {} in account {}".format( role_name, account_number)) continuing = False # 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), exc_info=True, ) continuing = False total_permissions, eligible_permissions = roledata._get_role_permissions( role) 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, ) # 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) if inline_policies_size_exceeds_maximum(repoed_policies): 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) continuing = False # if we aren't repoing for some reason, unschedule the role if not continuing: set_role_data(dynamo_table, role.role_id, { "RepoScheduled": 0, "ScheduledPerms": [] }) return if not commit: log_deleted_and_repoed_policies(deleted_policy_names, repoed_policies, role_name, account_number) return conn = config["connection_iam"] conn["account_number"] = account_number for name in deleted_policy_names: error = delete_policy(name, role, account_number, conn) if error: LOGGER.error(error) errors.append(error) if repoed_policies: error = replace_policies(repoed_policies, role, account_number, conn) if error: 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, "errors": errors }) 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) partial_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, 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) )
def repo(self, hooks: RepokidHooks, commit: bool = False, scheduled: bool = False) -> List[str]: errors: List[str] = [] eligible, reason = self.is_eligible_for_repo() if not eligible: errors.append( f"Role {self.role_name} not eligible for repo: {reason}") return errors self.calculate_repo_scores( self.config["filter_config"]["AgeFilter"]["minimum_age"], hooks # type: ignore ) try: repoed_policies, deleted_policy_names = self.get_repoed_policy( scheduled=scheduled) except MissingRepoableServices as e: errors.append(f"Role {self.role_name} cannot be repoed: {e}") return errors if inline_policies_size_exceeds_maximum(repoed_policies): error = ( "Policies would exceed the AWS size limit after repo for role: {} in account {}. " "Please manually minify.".format(self.role_name, self.account)) logger.error(error) errors.append(error) self.repo_scheduled = 0 self.scheduled_perms = [] self.store(["repo_scheduled", "scheduled_perms"]) return errors if not commit: log_deleted_and_repoed_policies(deleted_policy_names, repoed_policies, self.role_name, self.account) return errors conn = self.config["connection_iam"] # type: ignore conn["account_number"] = self.account for name in deleted_policy_names: try: delete_policy(name, self.role_name, self.account, conn) except IAMError as e: logger.error(e) errors.append(str(e)) if repoed_policies: try: replace_policies(repoed_policies, self.role_name, self.account, conn) except IAMError as e: logger.error(e) errors.append(str(e)) current_policies = (get_role_inline_policies(self.dict(by_alias=True), **conn) or {}) self.add_policy_version(current_policies, source="Repo") # regardless of whether we're successful we want to unschedule the repo self.repo_scheduled = 0 self.scheduled_perms = [] call_hooks(hooks, "AFTER_REPO", {"role": self, "errors": errors}) if not errors: # repos will stay scheduled until they are successful self.repoed = datetime.datetime.now( tz=datetime.timezone.utc).isoformat() update_repoed_description(self.role_name, conn) logger.info("Successfully repoed role: {} in account {}".format( self.role_name, self.account)) try: self.store() except RoleStoreError: logger.exception("failed to store role after repo", exc_info=True) return errors
def remove_permissions(self, permissions: List[str], hooks: RepokidHooks, commit: bool = False) -> None: """Remove the list of permissions from the provided role. Args: account_number (string) permissions (list<string>) role (Role object) role_id (string) commit (bool) Returns: None """ ( repoed_policies, deleted_policy_names, ) = get_repoed_policy(self.policies[-1]["Policy"], set(permissions)) if inline_policies_size_exceeds_maximum(repoed_policies): logger.error( "Policies would exceed the AWS size limit after repo for role: {} in account {}. " "Please manually minify.".format(self.role_name, self.account)) return if not commit: log_deleted_and_repoed_policies(deleted_policy_names, repoed_policies, self.role_name, self.account) return conn = self.config["connection_iam"] # type: ignore conn["account_number"] = self.account for name in deleted_policy_names: try: delete_policy(name, self.role_name, self.account, conn) except IAMError as e: logger.error(e) if repoed_policies: try: replace_policies(repoed_policies, self.role_name, self.account, conn) except IAMError as e: logger.error(e) current_policies = get_role_inline_policies(self.dict(), **conn) or {} self.add_policy_version(current_policies, "Repo") self.repoed = datetime.datetime.now( tz=datetime.timezone.utc).isoformat() update_repoed_description(self.role_name, conn) self.gather_role_data( hooks, current_policies=current_policies, source="ManualPermissionRepo", add_no_repo=False, ) logger.info( "Successfully removed {permissions} from role: {role} in account {account_number}" .format( permissions=permissions, role=self.role_name, account_number=self.account, ))
def _deal_with_policies(role, account_number, config, hooks, scheduled, role_name, dynamo_table, commit, continuing): errors = [] total_permissions, eligible_permissions = roledata._get_role_permissions( role) 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, ) # 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) if inline_policies_size_exceeds_maximum(repoed_policies): 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) continuing = False # if we aren't repoing for some reason, unschedule the role if not continuing: set_role_data(dynamo_table, role.role_id, { "RepoScheduled": 0, "ScheduledPerms": [] }) return if not commit: log_deleted_and_repoed_policies(deleted_policy_names, repoed_policies, role_name, account_number) return conn = config["connection_iam"] conn["account_number"] = account_number for name in deleted_policy_names: error = delete_policy(name, role, account_number, conn) if error: LOGGER.error(error) errors.append(error) if repoed_policies: error = replace_policies(repoed_policies, role, account_number, conn) if error: 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, "errors": errors }) 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) partial_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 _repo_role( account_number: str, role_name: str, config: RepokidConfig, hooks: RepokidHooks, commit: bool = False, scheduled: bool = False, ) -> List[str]: """ 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: List[str] = [] role_id = find_role_in_cache(role_name, account_number) # only load partial data that we need to determine if we should keep going role = Role(role_id=role_id) role.fetch() continuing = True eligible, reason = role.is_eligible_for_repo() if not eligible: errors.append(f"Role {role_name} not eligible for repo: {reason}") return errors role.calculate_repo_scores( config["filter_config"]["AgeFilter"]["minimum_age"], hooks) repoed_policies, deleted_policy_names = role.get_repoed_policy( scheduled=scheduled) if inline_policies_size_exceeds_maximum(repoed_policies): 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) continuing = False # if we aren't repoing for some reason, unschedule the role if not continuing: role.repo_scheduled = 0 role.scheduled_perms = [] role.store(["repo_scheduled", "scheduled_perms"]) return errors if not commit: log_deleted_and_repoed_policies(deleted_policy_names, repoed_policies, role_name, account_number) return errors conn = config["connection_iam"] conn["account_number"] = account_number for name in deleted_policy_names: try: delete_policy(name, role, account_number, conn) except IAMError as e: LOGGER.error(e) errors.append(str(e)) if repoed_policies: try: replace_policies(repoed_policies, role, account_number, conn) except IAMError as e: LOGGER.error(e) errors.append(str(e)) current_policies = get_role_inline_policies(role.dict(by_alias=True), ** conn) or {} role.add_policy_version(current_policies, source="Repo") # regardless of whether we're successful we want to unschedule the repo role.repo_scheduled = 0 role.scheduled_perms = [] repokid.hooks.call_hooks(hooks, "AFTER_REPO", { "role": role, "errors": errors }) if not errors: # repos will stay scheduled until they are successful role.repoed = datetime.datetime.now( tz=datetime.timezone.utc).isoformat() update_repoed_description(role.role_name, conn) role.gather_role_data(current_policies, hooks, source="Repo", add_no_repo=False) LOGGER.info("Successfully repoed role: {} in account {}".format( role.role_name, account_number)) role.store() return errors