def apply(self, input_list: RoleList) -> RoleList: now = datetime.datetime.now() if self.config: days_delta = self.config.get("minimum_age", 90) else: log.info("Minimum age not set in config, using default 90 days") days_delta = 90 ago = datetime.timedelta(days=days_delta) too_young = RoleList([]) for role in input_list: if not role.create_date: log.warning(f"Role {role.role_name} is missing create_date") too_young.append(role) continue # Ensure create_date is an offset-naive datetime create_date = datetime.datetime.fromtimestamp( role.create_date.timestamp()) if create_date > now - ago: log.info( f"Role {role.role_name} created too recently to cleanup. ({create_date})" ) too_young.append(role) return too_young
def apply(self, input_list: RoleList) -> RoleList: lambda_roles: RoleList = RoleList([]) for role in input_list: if "lambda" in str(role.assume_role_policy_document).lower(): lambda_roles.append(role) return lambda_roles
def apply(self, input_list: RoleList) -> RoleList: blocklisted_roles = RoleList([]) for role in input_list: if (role.role_name.lower() in self.blocklisted_role_names or role.arn in self.blocklisted_arns): blocklisted_roles.append(role) return blocklisted_roles
def test_age_too_young_no_tz(mock_role: Role): age_filter = AgeFilter() create_date = datetime.datetime.now() assert not create_date.tzinfo mock_role.create_date = create_date role_list = RoleList([mock_role]) result = age_filter.apply(role_list) assert len(result) == 1
def apply(self, input_list: RoleList) -> RoleList: opt_out_roles: RoleList = RoleList([]) for role in input_list: if role.opt_out and role.opt_out[ "expire"] > self.current_time_epoch: opt_out_roles.append(role) return opt_out_roles
def test_age_with_tz(mock_role: Role): age_filter = AgeFilter() create_date = datetime.datetime.now(tz=tz.tzutc()) - datetime.timedelta( days=100) assert create_date.tzinfo mock_role.create_date = create_date role_list = RoleList([mock_role]) result = age_filter.apply(role_list) assert len(result) == 0
def apply(self, input_list: RoleList) -> RoleList: exclusive_roles = [] for role_glob in self.exclusive_role_globs: exclusive_roles += [ role for role in input_list if fnmatch.fnmatch(role.role_name.lower(), role_glob) ] filtered_roles = list(set(input_list) - set(exclusive_roles)) return RoleList(filtered_roles)
def test_schedule_repo( self, mock_time, mock_get_all_role_ids_for_account, mock_role_list_from_ids, mock_fetch, mock_role_store, mock_call_hooks, ): hooks = RepokidHooks mock_get_all_role_ids_for_account.return_value = [ "AROAABCDEFGHIJKLMNOPA", "AROAABCDEFGHIJKLMNOPB", ] # first role is not repoable, second role is repoable test_roles = [ ROLES_FOR_DISPLAY[0].copy(update=Role( **{ "RoleId": "AROAABCDEFGHIJKLMNOPA" }).dict()), ROLES_FOR_DISPLAY[1].copy(update=Role( **{ "RoleId": "AROAABCDEFGHIJKLMNOPB", "AAData": [{ "foo": "bar" }], "RepoablePermissions": 10, }).dict()), ] mock_role_list_from_ids.return_value = RoleList( [test_roles[0], test_roles[1]]) mock_time.return_value = 1 config = {"repo_schedule_period_days": 1} repokid.commands.schedule._schedule_repo("1234567890", config, hooks) mock_role_store.assert_called() assert mock_call_hooks.mock_calls == [ call( hooks, "AFTER_SCHEDULE_REPO", {"roles": [test_roles[1]]}, ) ]
def _update_role_cache( account_number: str, config: RepokidConfig, hooks: RepokidHooks, ) -> None: """ 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 """ access_advisor_datasource = AccessAdvisorDatasource() access_advisor_datasource.seed(account_number) iam_datasource = IAMDatasource() role_ids = iam_datasource.seed(account_number) # We only iterate over the newly-seeded data (`role_ids`) so we don't duplicate work for runs on multiple accounts roles = RoleList([Role(**iam_datasource[role_id]) for role_id in role_ids]) LOGGER.info("Updating role data for account {}".format(account_number)) for role in tqdm(roles): role.account = account_number role.gather_role_data(hooks, config=config, source="Scan", store=False) LOGGER.info("Finding inactive roles in account {}".format(account_number)) find_and_mark_inactive(account_number, roles) LOGGER.info("Filtering roles") plugins = get_filter_plugins(account_number, config=config) for plugin in plugins.filter_plugins: filtered_list = plugin.apply(roles) class_name = plugin.__class__.__name__ for filtered_role in filtered_list: LOGGER.debug("Role {} filtered by {}".format( filtered_role.role_name, class_name)) # There may be existing duplicate records, so we do a dance here to dedupe them. disqualified_by = set(filtered_role.disqualified_by) disqualified_by.add(class_name) filtered_role.disqualified_by = list(disqualified_by) 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, )) LOGGER.info( "Storing updated role data in account {}".format(account_number)) roles.store()
def test_cancel_scheduled_repo( self, mock_get_all_role_ids_for_account, mock_find_role_in_cache, mock_role_list_from_ids, mock_role_store, mock_role_fetch, ): mock_get_all_role_ids_for_account.return_value = [ "AROAABCDEFGHIJKLMNOPA", "AROAABCDEFGHIJKLMNOPB", ] roles = RoleList( [ Role( **{ "Arn": "arn:aws:iam::123456789012:role/ROLE_A", "RoleId": "AROAABCDEFGHIJKLMNOPA", "Active": True, "RoleName": "ROLE_A", "RepoScheduled": 100, "CreateDate": datetime.datetime.now() - datetime.timedelta(days=100), } ), Role( **{ "Arn": "arn:aws:iam::123456789012:role/ROLE_B", "RoleId": "AROAABCDEFGHIJKLMNOPB", "Active": True, "RoleName": "ROLE_B", "RepoScheduled": 0, "CreateDate": datetime.datetime.now() - datetime.timedelta(days=100), } ), Role( **{ "Arn": "arn:aws:iam::123456789012:role/ROLE_C", "RoleId": "AROAABCDEFGHIJKLMNOPC", "Active": True, "RoleName": "ROLE_C", "RepoScheduled": 5, "CreateDate": datetime.datetime.now() - datetime.timedelta(days=100), } ), ] ) mock_role_list_from_ids.return_value = RoleList([roles[0], roles[2]]) # first check all repokid.commands.schedule._cancel_scheduled_repo("", role_name="", is_all=True) assert mock_role_store.call_count == 2 mock_role_store.reset_mock() # ensure all are cancelled mock_find_role_in_cache.return_value = "AROAABCDEFGHIJKLMNOPA" repokid.commands.schedule._cancel_scheduled_repo( "", role_name="ROLE_A", is_all=False )
def test_repo_all_roles( self, mock_time, mock_repo_role, mock_iam_datasource_seed, mock_aa_datasource, mock_role_list_fetch_all, mock_role_list_from_ids, mock_call_hooks, ): hooks = RepokidHooks() mock_iam_datasource_seed.return_value = [ "AROAABCDEFGHIJKLMNOPA", "AROAABCDEFGHIJKLMNOPB", "AROAABCDEFGHIJKLMNOPC", ] roles = RoleList( [ Role( **{ "Arn": "arn:aws:iam::123456789012:role/ROLE_A", "RoleId": "AROAABCDEFGHIJKLMNOPA", "Active": True, "RoleName": "ROLE_A", "RepoScheduled": 100, "CreateDate": datetime.datetime.now() - datetime.timedelta(days=100), } ), Role( **{ "Arn": "arn:aws:iam::123456789012:role/ROLE_B", "RoleId": "AROAABCDEFGHIJKLMNOPB", "Active": True, "RoleName": "ROLE_B", "RepoScheduled": 0, "CreateDate": datetime.datetime.now() - datetime.timedelta(days=100), } ), Role( **{ "Arn": "arn:aws:iam::123456789012:role/ROLE_C", "RoleId": "AROAABCDEFGHIJKLMNOPC", "Active": True, "RoleName": "ROLE_C", "RepoScheduled": 5, "CreateDate": datetime.datetime.now() - datetime.timedelta(days=100), } ), ] ) # time is past ROLE_C but before ROLE_A mock_time.return_value = 10 mock_role_list_from_ids.return_value = RoleList( [ roles[0], roles[1], roles[2], ] ) mock_repo_role.return_value = None # repo all roles in the account, should call repo with all roles repokid.commands.repo._repo_all_roles("", {}, hooks, scheduled=False) # repo only scheduled, should only call repo role with role C repokid.commands.repo._repo_all_roles("", {}, hooks, scheduled=True) assert mock_repo_role.mock_calls == [ call(hooks, commit=False, scheduled=False), call(hooks, commit=False, scheduled=False), call(hooks, commit=False, scheduled=False), call(hooks, commit=False, scheduled=True), ] assert mock_call_hooks.mock_calls == [ call( hooks, "BEFORE_REPO_ROLES", {"account_number": "", "roles": roles}, ), call( hooks, "AFTER_REPO_ROLES", {"account_number": "", "roles": roles, "errors": []}, ), call( hooks, "BEFORE_REPO_ROLES", {"account_number": "", "roles": RoleList([roles[2]])}, ), call( hooks, "AFTER_REPO_ROLES", { "account_number": "", "roles": RoleList([roles[2]]), "errors": [], }, ), ]
def test_repokid_display_roles( self, mock_get_all_role_ids_for_account, mock_role_list_from_ids, mock_tabview ): console_logger = logging.StreamHandler() console_logger.setLevel(logging.WARNING) repokid.cli.repokid_cli.logger = logging.getLogger("test") repokid.cli.repokid_cli.logger.addHandler(console_logger) mock_get_all_role_ids_for_account.return_value = [ "AROAABCDEFGHIJKLMNOPA", "AROAABCDEFGHIJKLMNOPB", "AROAABCDEFGHIJKLMNOPC", "AROAABCDEFGHIJKLMNOPD", ] test_roles = [] for x, role in enumerate(ROLES_FOR_DISPLAY): test_roles.append( role.copy(update=Role(**ROLES[x]).dict(exclude_unset=True)) ) # loop over all roles twice (one for each call below) mock_role_list_from_ids.return_value = RoleList( [ test_roles[0], test_roles[1], test_roles[2], test_roles[3], ] ) repokid.commands.role._display_roles("123456789012", inactive=True) repokid.commands.role._display_roles("123456789012", inactive=False) # first call has inactive role, second doesn't because it's filtered assert mock_tabview.mock_calls == [ call( [ [ "Name", "Refreshed", "Disqualified By", "Can be repoed", "Permissions", "Repoable", "Repoed", "Services", ], ["all_services_used", "Someday", [], True, 4, 0, "Never", []], ["inactive_role", "Someday", [], True, 4, 0, "Never", []], ["young_role", "Someday", [], True, 4, 0, "Never", []], ["unused_ec2", "Someday", [], True, 4, 2, "Never", ["ec2"]], ] ), call( [ [ "Name", "Refreshed", "Disqualified By", "Can be repoed", "Permissions", "Repoable", "Repoed", "Services", ], ["all_services_used", "Someday", [], True, 4, 0, "Never", []], ["young_role", "Someday", [], True, 4, 0, "Never", []], ["unused_ec2", "Someday", [], True, 4, 2, "Never", ["ec2"]], ] ), ]
def test_repokid_update_role_cache( self, mock_iam_datasource_fetch, mock_access_advisor_datasource, mock_gather_role_data, mock_role_list_store, mock_find_and_mark_inactive, ): hooks = {} role_data = ROLES[:3] role_data[0]["RolePolicyList"] = [ { "PolicyName": "all_services_used", "PolicyDocument": ROLE_POLICIES["all_services_used"], } ] role_data[1]["RolePolicyList"] = [ {"PolicyName": "unused_ec2", "PolicyDocument": ROLE_POLICIES["unused_ec2"]} ] role_data[2]["RolePolicyList"] = [ { "PolicyName": "all_services_used", "PolicyDocument": ROLE_POLICIES["all_services_used"], } ] role_data = {item["RoleId"]: item for item in role_data} mock_iam_datasource_fetch.return_value = role_data config = { "aardvark_api_location": "", "connection_iam": {}, "active_filters": ["repokid.filters.age:AgeFilter"], "filter_config": {"AgeFilter": {"minimum_age": 90}, "BlocklistFilter": {}}, } console_logger = logging.StreamHandler() console_logger.setLevel(logging.WARNING) repokid.cli.repokid_cli.logger = logging.getLogger("test") repokid.cli.repokid_cli.logger.addHandler(console_logger) account_number = "123456789012" repokid.commands.role_cache._update_role_cache(account_number, config, hooks) assert mock_gather_role_data.call_count == 3 # all roles active assert mock_find_and_mark_inactive.mock_calls[-1] == call( account_number, RoleList( [ Role(**ROLES[0]), Role(**ROLES[1]), Role(**ROLES[2]), ] ), )
def _repo_all_roles( account_number: str, config: RepokidConfig, hooks: RepokidHooks, commit: bool = False, scheduled: bool = True, limit: int = -1, ) -> None: """ Repo all scheduled or eligible roles in an account. Collect any errors and display them at the end. Args: account_number (string) dynamo_table config commit (bool): actually make the changes scheduled (bool): if True only repo the scheduled roles, if False repo all the (eligible) roles limit (int): limit number of roles to be repoed per run (< 0 is unlimited) Returns: None """ access_advisor_datasource = AccessAdvisorDatasource() access_advisor_datasource.seed(account_number) iam_datasource = IAMDatasource() role_arns = iam_datasource.seed(account_number) errors = [] roles = RoleList.from_arns(role_arns, config=config) roles = roles.get_active() if scheduled: roles = roles.get_scheduled() if not roles: LOGGER.info(f"No roles to repo in account {account_number}") return LOGGER.info( "Repoing these {}roles from account {}:\n\t{}".format( "scheduled " if scheduled else "", account_number, ", ".join([role.role_name for role in roles]), ) ) repokid.hooks.call_hooks( hooks, "BEFORE_REPO_ROLES", {"account_number": account_number, "roles": roles} ) count = 0 repoed = RoleList([]) for role in roles: if limit >= 0 and count == limit: break role_errors = role.repo(hooks, commit=commit, scheduled=scheduled) if role_errors: errors.extend(role_errors) repoed.append(role) count += 1 if errors: LOGGER.error(f"Error(s) during repo in account: {account_number}: {errors}") LOGGER.info(f"Successfully repoed {count} roles in account {account_number}") repokid.hooks.call_hooks( hooks, "AFTER_REPO_ROLES", {"account_number": account_number, "roles": repoed, "errors": errors}, )