def __init__(self, config=None): blocklist_json = None bucket_config = config.get( "blocklist_bucket", config.get("blacklist_bucket", None) ) if bucket_config: blocklist_json = get_blocklist_from_bucket(bucket_config) current_account = config.get("current_account") or None if not current_account: LOGGER.error("Unable to get current account for Blocklist Filter") blocklisted_role_names = set() blocklisted_role_names.update( [rolename.lower() for rolename in config.get(current_account, [])] ) blocklisted_role_names.update( [rolename.lower() for rolename in config.get("all", [])] ) if blocklist_json: blocklisted_role_names.update( [ name.lower() for name, accounts in blocklist_json["names"].items() if ("all" in accounts or config.get("current_account") in accounts) ] ) self.blocklisted_arns = ( set() if not blocklist_json else blocklist_json.get("arns", []) ) self.blocklisted_role_names = blocklisted_role_names
def update_repoed_description(role_name, client=None): description = None try: description = client.get_role(RoleName=role_name)["Role"].get( "Description", "") except KeyError: return date_string = datetime.datetime.utcnow().strftime("%m/%d/%y") if "; Repokid repoed" in description: new_description = re.sub( r"; Repokid repoed [0-9]{2}\/[0-9]{2}\/[0-9]{2}", "; Repokid repoed {}".format(date_string), description, ) else: new_description = description + " ; Repokid repoed {}".format( date_string) # IAM role descriptions have a max length of 1000, if our new length would be longer, skip this if len(new_description) < 1000: client.update_role_description(RoleName=role_name, Description=new_description) else: LOGGER.error( "Unable to set repo description ({}) for role {}, length would be too long" .format(new_description, role_name))
def __init__(self, config=None): blocklist_json = None bucket_config = config.get('blocklist_bucket', config.get('blacklist_bucket', None)) if bucket_config: blocklist_json = get_blocklist_from_bucket(bucket_config) current_account = config.get('current_account') or None if not current_account: LOGGER.error('Unable to get current account for Blocklist Filter') blocklisted_role_names = set() blocklisted_role_names.update( [rolename.lower() for rolename in config.get(current_account, [])]) blocklisted_role_names.update( [rolename.lower() for rolename in config.get('all', [])]) if blocklist_json: blocklisted_role_names.update([ name.lower() for name, accounts in blocklist_json['names'].items() if ('all' in accounts or config.get('current_account') in accounts) ]) self.blocklisted_arns = set( ) if not blocklist_json else blocklist_json.get('arns', []) self.blocklisted_role_names = blocklisted_role_names
def update_aardvark_data(aardvark_data, roles): """ Update Aardvark data for a given set of roles by looking for the ARN in the aardvark data dict. If the ARN is in Aardvark data update the role's aa_data attribute and Dynamo. Args: aardvark_data (dict): A dict of Aardvark data from an account roles (Roles): a list of all the role objects to update data for Returns: None """ for role in roles: if role.arn in aardvark_data: role.aa_data = aardvark_data[role.arn] try: DYNAMO_TABLE.update_item(Key={'RoleId': role.role_id}, UpdateExpression="SET AAData=:aa_data", ExpressionAttributeValues={ ":aa_data": _empty_string_to_dynamo_replace( role.aa_data) }) except BotoClientError as e: LOGGER.error('Dynamo table error: {}'.format(e))
def update_stats(roles, source='Scan'): """ Create a new stats entry for each role in a set of roles and add it to Dynamo Args: roles (Roles): a list of all the role objects to update data for source (string): the source of the new stats data (repo, scan, etc) Returns: None """ for role in roles: cur_stats = { 'Date': datetime.datetime.utcnow().isoformat(), 'DisqualifiedBy': role.disqualified_by, 'PermissionsCount': role.total_permissions, 'Source': source } try: DYNAMO_TABLE.update_item( Key={'RoleId': role.role_id}, UpdateExpression=("SET #statsarray = list_append(if_not_exists" "(#statsarray, :empty_list), :stats)"), ExpressionAttributeNames={"#statsarray": "Stats"}, ExpressionAttributeValues={ ":empty_list": [], ":stats": [cur_stats] }) except BotoClientError as e: LOGGER.error('Dynamo table error: {}'.format(e))
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 repo_stats(output_file, dynamo_table, account_number=None): """ Create a csv file with stats about roles, total permissions, and applicable filters over time Args: output_file (string): the name of the csv file to write account_number (string): if specified only display roles from selected account, otherwise display all Returns: None """ roleIDs = (role_ids_for_account(dynamo_table, account_number) if account_number else role_ids_for_all_accounts(dynamo_table)) headers = ['RoleId', 'Role Name', 'Account', 'Active', 'Date', 'Source', 'Permissions Count', 'Repoable Permissions Count', 'Disqualified By'] rows = [] for roleID in roleIDs: role_data = get_role_data(dynamo_table, roleID, fields=['RoleId', 'RoleName', 'Account', 'Active', 'Stats']) for stats_entry in role_data.get('Stats', []): rows.append([role_data['RoleId'], role_data['RoleName'], role_data['Account'], role_data['Active'], stats_entry['Date'], stats_entry['Source'], stats_entry['PermissionsCount'], stats_entry.get('RepoablePermissionsCount'), stats_entry.get('DisqualifiedBy', [])]) try: with open(output_file, 'wb') as csvfile: csv_writer = csv.writer(csvfile) csv_writer.writerow(headers) for row in rows: csv_writer.writerow(row) except IOError as e: LOGGER.error('Unable to write file {}: {}'.format(output_file, e)) else: LOGGER.info('Successfully wrote stats to {}'.format(output_file))
def _get_role_data(roleID, fields=None): """ Get raw role data as a dictionary for a given role by ID Do not use for data presented to the user because this data still has dynamo empty string placeholders, use get_role_data() instead Args: roleID (string) Returns: dict: data for the role if it exists, else None """ try: if fields: response = DYNAMO_TABLE.get_item(Key={'RoleId': roleID}, AttributesToGet=fields) else: response = DYNAMO_TABLE.get_item(Key={'RoleId': roleID}) except BotoClientError as e: LOGGER.error('Dynamo table error: {}'.format(e)) else: if 'Item' in response: return response['Item'] else: return None
def role_ids_for_all_accounts(): """ Get a list of all role IDs for all accounts by scanning the Dynamo table Args: None Returns: list: role ids in all accounts """ role_ids = [] try: response = DYNAMO_TABLE.scan(ProjectionExpression='RoleId') role_ids.extend( [role_dict['RoleId'] for role_dict in response['Items']]) while 'LastEvaluatedKey' in response: response = DYNAMO_TABLE.scan( ProjectionExpression='RoleId', ExclusiveStartKey=response['LastEvaluatedKey']) role_ids.extend( [role_dict['RoleId'] for role_dict in response['Items']]) except BotoClientError as e: LOGGER.error('Dynamo table error: {}'.format(e)) return role_ids
def role_ids_for_account(account_number): """ Get a list of all role IDs in a given account by querying the Dynamo secondary index 'account' Args: account_number (string) Returns: list: role ids in given account """ role_ids = set() try: results = DYNAMO_TABLE.query( IndexName='Account', ProjectionExpression='RoleId', KeyConditionExpression='Account = :act', ExpressionAttributeValues={':act': account_number}) role_ids.update( [return_dict['RoleId'] for return_dict in results.get('Items')]) while 'LastEvaluatedKey' in results: results = DYNAMO_TABLE.query( IndexName='Account', ProjectionExpression='RoleId', KeyConditionExpression='Account = :act', ExpressionAttributeValues={':act': account_number}, ExclusiveStartKey=results.get('LastEvaluatedKey')) role_ids.update([ return_dict['RoleId'] for return_dict in results.get('Items') ]) except BotoClientError as e: LOGGER.error('Dynamo table error: {}'.format(e)) return role_ids
def find_and_mark_inactive(account_number, active_roles): """ Mark roles in the account that aren't currently active inactive. Do this by getting all roles in the account and subtracting the active roles, any that are left are inactive and should be marked thusly. Args: account_number (string) active_roles (set): the currently active roles discovered in the most recent scan Returns: None """ active_roles = set(active_roles) known_roles = set(role_ids_for_account(account_number)) inactive_roles = known_roles - active_roles for roleID in inactive_roles: role_dict = _get_role_data(roleID, fields=['Active', 'Arn']) if role_dict['Active']: try: DYNAMO_TABLE.update_item( Key={'RoleId': roleID}, UpdateExpression="SET Active = :false", ExpressionAttributeValues={":false": False}) except BotoClientError as e: LOGGER.error('Dynamo table error: {}'.format(e)) else: LOGGER.info('Marked role ({}): {} inactive'.format( roleID, role_dict['Arn']))
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 repo_all_roles(account_number, dynamo_table, config, hooks, commit=False, scheduled=True): """ 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 Returns: None """ errors = [] role_ids_in_account = role_ids_for_account(dynamo_table, account_number) roles = Roles([]) for role_id in role_ids_in_account: roles.append( Role( get_role_data(dynamo_table, role_id, fields=['Active', 'RoleName', 'RepoScheduled']))) roles = roles.filter(active=True) cur_time = int(time.time()) if scheduled: roles = [ role for role in roles if (role.repo_scheduled and cur_time > role.repo_scheduled) ] LOGGER.info('Repoing these {}roles from account {}:\n\t{}'.format( 'scheduled ' if scheduled else '', account_number, ', '.join([role.role_name for role in roles]))) for role in roles: error = repo_role(account_number, role.role_name, dynamo_table, config, hooks, commit=commit) if error: errors.append(error) if errors: LOGGER.error('Error(s) during repo: \n{}'.format(errors)) else: LOGGER.info('Everything successful!')
def __init__(self, config=None): current_account = config.get("current_account") or None if not current_account: LOGGER.error("Unable to get current account for Exclusive Filter") exclusive_role_globs = set() exclusive_role_globs.update([ role_glob.lower() for role_glob in config.get(current_account, []) ]) exclusive_role_globs.update( [role_glob.lower() for role_glob in config.get("all", [])]) self.exclusive_role_globs = exclusive_role_globs
def get_aardvark_data(aardvark_api_location, account_number=None, arn=None): """ Make a request to the Aardvark server to get all data about a given account or ARN. We'll request in groups of PAGE_SIZE and check the current count to see if we're done. Keep requesting as long as the total count (reported by the API) is greater than the number of pages we've received times the page size. As we go, keeping building the dict and return it when done. Args: aardvark_api_location account_number (string): Used to form the phrase query for Aardvark so we only get data for the account we want arn (string) Returns: dict: Aardvark data is a dict with the role ARN as the key and a list of services as value """ response_data = {} PAGE_SIZE = 1000 page_num = 1 if account_number: payload = {"phrase": "{}".format(account_number)} elif arn: payload = {"arn": [arn]} else: return while True: params = {"count": PAGE_SIZE, "page": page_num} try: r_aardvark = requests.post( aardvark_api_location, params=params, json=payload ) except requests.exceptions.RequestException as e: LOGGER.error("Unable to get Aardvark data: {}".format(e)) sys.exit(1) else: if r_aardvark.status_code != 200: LOGGER.error("Unable to get Aardvark data") sys.exit(1) response_data.update(r_aardvark.json()) # don't want these in our Aardvark data response_data.pop("count") response_data.pop("page") response_data.pop("total") if PAGE_SIZE * page_num < r_aardvark.json().get("total"): page_num += 1 else: break return response_data
def _update_repoed_description(role_name, client=None): description = None try: description = client.get_role(RoleName=role_name)['Role'].get('Description', '') except KeyError: return date_string = datetime.datetime.utcnow().strftime('%m/%d/%y') if '; Repokid repoed' in description: new_description = re.sub(r'; Repokid repoed [0-9]{2}\/[0-9]{2}\/[0-9]{2}', '; Repokid repoed {}'.format( date_string), description) else: new_description = description + ' ; Repokid repoed {}'.format(date_string) # IAM role descriptions have a max length of 1000, if our new length would be longer, skip this if len(new_description) < 1000: client.update_role_description(RoleName=role_name, Description=new_description) else: LOGGER.error('Unable to set repo description ({}) for role {}, length would be too long'.format( new_description, role_name))
def update_filtered_roles(roles): """ Update the disqualified by (applicable filters) in Dynamo for each role in a list of roles Args: roles (Roles) Returns: None """ for role in roles: try: DYNAMO_TABLE.update_item( Key={'RoleId': role.role_id}, UpdateExpression="SET DisqualifiedBy = :dqby", ExpressionAttributeValues={":dqby": role.disqualified_by}) except BotoClientError as e: LOGGER.error('Dynamo table error: {}'.format(e))
def update_opt_out(role): """ Update opt-out object for a role - remove (set to empty dict) any entries that have expired Opt-out objects should have the form {'expire': xxx, 'owner': xxx, 'reason': xxx} Args: role Returns: None """ if role.opt_out and int(role.opt_out['expire']) < int(time.time()): try: DYNAMO_TABLE.update_item(Key={'RoleId': role.role_id}, UpdateExpression="SET OptOut=:oo", ExpressionAttributeValues={":oo": {}}) except BotoClientError as e: LOGGER.error('Dynamo table error: {}'.format(e))
def _refresh_updated_time(roleID): """ Update refreshed time for given role ID to utcnow Args: rolesID (string): the role ID of the role to update Returns: None """ try: DYNAMO_TABLE.update_item(Key={'RoleId': roleID}, UpdateExpression="SET Refreshed = :cur_time", ExpressionAttributeValues={ ":cur_time": datetime.datetime.utcnow().isoformat() }) except BotoClientError as e: LOGGER.error('Dynamo table error: {}'.format(e))
def set_repoed(role_id): """ Marks a role (by ID) as having been repoed now (utcnow) as string in Dynamo Args: role_id (string) Returns: None """ try: DYNAMO_TABLE.update_item( Key={'RoleId': role_id}, UpdateExpression="SET Repoed = :now, RepoableServices = :el", ExpressionAttributeValues={ ":now": datetime.datetime.utcnow().isoformat(), ":el": [] }) except BotoClientError as e: LOGGER.error('Dynamo table error: {}'.format(e))
def update_no_repo_permissions(role, newly_added_permissions): """ Update Dyanmo entry for newly added permissions. Any that were newly detected get added with an expiration date of now plus the config setting for 'repo_requirements': 'exclude_new_permissions_for_days'. Expired entries get deleted. Also update the role object with the new no-repo-permissions. Args: role newly_added_permissions (set) Returns: None """ current_ignored_permissions = _get_role_data( role.role_id, fields=['NoRepoPermissions']).get('NoRepoPermissions', {}) new_ignored_permissions = {} current_time = int(time.time()) new_perms_expire_time = current_time + ( 24 * 60 * 60 * CONFIG['repo_requirements'].get( 'exclude_new_permissions_for_days', 14)) # only copy non-expired items to the new dictionary for permission, expire_time in current_ignored_permissions.items(): if expire_time > current_time: new_ignored_permissions[permission] = current_ignored_permissions[ permission] for permission in newly_added_permissions: new_ignored_permissions[permission] = new_perms_expire_time role.no_repo_permissions = new_ignored_permissions try: DYNAMO_TABLE.update_item( Key={'RoleId': role.role_id}, UpdateExpression="SET NoRepoPermissions=:nrp", ExpressionAttributeValues={":nrp": new_ignored_permissions}) except BotoClientError as e: LOGGER.error('Dynamo table error: {}'.format(e))
def remove_permissions_from_roles( permissions, role_filename, dynamo_table, config, hooks, commit=False ): """Loads roles specified in file and calls _remove_permissions_from_role() for each one. Args: permissions (list<string>) role_filename (string) commit (bool) Returns: None """ roles = list() with open(role_filename, "r") as fd: roles = json.load(fd) for role_arn in tqdm(roles): arn = ARN(role_arn) if arn.error: LOGGER.error("INVALID ARN: {arn}".format(arn=role_arn)) return account_number = arn.account_number role_name = arn.name.split("/")[-1] role_id = find_role_in_cache(dynamo_table, account_number, role_name) role = Role(get_role_data(dynamo_table, role_id)) remove_permissions_from_role( account_number, permissions, role, role_id, dynamo_table, config, hooks, commit=commit, ) repokid.hooks.call_hooks(hooks, "AFTER_REPO", {"role": role})
def get_blocklist_from_bucket(bucket_config): try: s3_resource = boto3_cached_conn( "s3", service_type="resource", account_number=bucket_config.get("account_number"), assume_role=bucket_config.get("assume_role", None), session_name="repokid", region=bucket_config.get("region", "us-west-2"), ) s3_obj = s3_resource.Object( bucket_name=bucket_config["bucket_name"], key=bucket_config["key"] ) blocklist = s3_obj.get()["Body"].read().decode("utf-8") blocklist_json = json.loads(blocklist) # Blocklist problems are really bad and we should quit rather than silently continue except (botocore.exceptions.ClientError, AttributeError): LOGGER.error( "S3 blocklist config was set but unable to connect retrieve object, quitting" ) sys.exit(1) except ValueError: LOGGER.error( "S3 blocklist config was set but the returned file is bad, quitting" ) sys.exit(1) if set(blocklist_json.keys()) != set(["arns", "names"]): LOGGER.error("S3 blocklist file is malformed, quitting") sys.exit(1) return blocklist_json
def get_blocklist_from_bucket(bucket_config): try: s3_resource = boto3_cached_conn( 's3', service_type='resource', account_number=bucket_config.get('account_number'), assume_role=bucket_config.get('assume_role', None), session_name='repokid', region=bucket_config.get('region', 'us-west-2')) s3_obj = s3_resource.Object(bucket_name=bucket_config['bucket_name'], key=bucket_config['key']) blocklist = s3_obj.get()['Body'].read().decode("utf-8") blocklist_json = json.loads(blocklist) # Blocklist problems are really bad and we should quit rather than silently continue except (botocore.exceptions.ClientError, AttributeError): LOGGER.error( "S3 blocklist config was set but unable to connect retrieve object, quitting" ) sys.exit(1) except ValueError: LOGGER.error( "S3 blocklist config was set but the returned file is bad, quitting" ) sys.exit(1) if set(blocklist_json.keys()) != set(['arns', 'names']): LOGGER.error("S3 blocklist file is malformed, quitting") sys.exit(1) return blocklist_json
def add_new_policy_version(role, current_policy, update_source): """ Create a new entry in the history of policy versions in Dynamo. The entry contains the source of the new policy: (scan, repo, or restore) the current time, and the current policy contents. Updates the role's policies with the full policies including the latest. Args: role (Role) current_policy (dict) update_source (string): ['Repo', 'Scan', 'Restore'] Returns: None """ cur_role_data = _get_role_data(role.role_id, fields=['Policies']) new_item_index = len(cur_role_data.get('Policies', [])) try: policy_entry = { 'Source': update_source, 'Discovered': datetime.datetime.utcnow().isoformat(), 'Policy': current_policy } DYNAMO_TABLE.update_item( Key={'RoleId': role.role_id}, UpdateExpression="SET #polarray[{}] = :pol".format(new_item_index), ExpressionAttributeNames={"#polarray": "Policies"}, ExpressionAttributeValues={ ":pol": _empty_string_to_dynamo_replace(policy_entry) }) except BotoClientError as e: LOGGER.error('Dynamo table error: {}'.format(e)) role.policies = get_role_data(role.role_id, fields=['Policies'])['Policies']
def _store_item(role, current_policy): """ Store the initial version of a role in Dynamo Args: role (Role) current_policy (dict) Returns: None """ policy_entry = { 'Source': 'Scan', 'Discovered': datetime.datetime.utcnow().isoformat(), 'Policy': current_policy } role.policies = [policy_entry] role.refreshed = datetime.datetime.utcnow().isoformat() role.active = True role.repoed = 'Never' try: DYNAMO_TABLE.put_item( Item={ 'Arn': role.arn, 'CreateDate': role.create_date.isoformat(), 'RoleId': role.role_id, 'RoleName': role.role_name, 'Account': role.account, 'Policies': [_empty_string_to_dynamo_replace(policy_entry)], 'Refreshed': role.refreshed, 'Active': role.active, 'Repoed': role.repoed }) except BotoClientError as e: LOGGER.error('Dynamo table error: {}'.format(e))
def update_repoable_data(roles): """ Update total permissions and repoable permissions count and a list of repoable services in Dynamo for each role Args: roles (Roles): a list of all the role objects to update data for Returns: None """ for role in roles: try: DYNAMO_TABLE.update_item( Key={'RoleId': role.role_id}, UpdateExpression=( "SET TotalPermissions=:tp, RepoablePermissions=:rp, " "RepoableServices=:rs"), ExpressionAttributeValues={ ":tp": role.total_permissions, ":rp": role.repoable_permissions, ":rs": role.repoable_services }) except BotoClientError as e: LOGGER.error('Dynamo table error: {}'.format(e))
def dynamo_get_or_create_table(**dynamo_config): """ Create a new table or get a reference to an existing Dynamo table named 'repokid_roles' that will store data all data for Repokid. Return a table with a reference to the dynamo resource Args: dynamo_config (kwargs): account_number (string) assume_role (string) optional session_name (string) region (string) endpoint (string) Returns: dynamo_table object """ if 'localhost' in dynamo_config['endpoint']: resource = boto3.resource('dynamodb', region_name='us-east-1', endpoint_url=dynamo_config['endpoint']) else: resource = boto3_cached_conn( 'dynamodb', service_type='resource', account_number=dynamo_config['account_number'], assume_role=dynamo_config.get('assume_role', None), session_name=dynamo_config['session_name'], region=dynamo_config['region']) for table in resource.tables.all(): if table.name == 'repokid_roles': return table table = None try: table = resource.create_table( TableName='repokid_roles', KeySchema=[ { 'AttributeName': 'RoleId', 'KeyType': 'HASH' # Partition key } ], AttributeDefinitions=[ { 'AttributeName': 'RoleId', 'AttributeType': 'S' }, { 'AttributeName': 'RoleName', 'AttributeType': 'S' }, { 'AttributeName': 'Account', 'AttributeType': 'S' } ], ProvisionedThroughput={ 'ReadCapacityUnits': 50, 'WriteCapacityUnits': 50 }, GlobalSecondaryIndexes=[ { 'IndexName': 'Account', 'KeySchema': [ { 'AttributeName': 'Account', 'KeyType': 'HASH' } ], 'Projection': { 'ProjectionType': 'KEYS_ONLY', }, 'ProvisionedThroughput': { 'ReadCapacityUnits': 10, 'WriteCapacityUnits': 10 } }, { 'IndexName': 'RoleName', 'KeySchema': [ { 'AttributeName': 'RoleName', 'KeyType': 'HASH' } ], 'Projection': { 'ProjectionType': 'KEYS_ONLY', }, 'ProvisionedThroughput': { 'ReadCapacityUnits': 10, 'WriteCapacityUnits': 10 } }]) except BotoClientError as e: LOGGER.error(e) return table
def decorated_func(*args, **kwargs): try: return func(*args, **kwargs) except BotoClientError as e: LOGGER.error('Dynamo table error: {}'.format(e)) sys.exit(1)
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, 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 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 }) LOGGER.debug( 'Repoable permissions for role {role_name} in {account_number}:\n{repoable}' .format(role_name=role_name, account_number=account_number, repoable=''.join( '{}: {}\n'.format(perm, decision.decider) for perm, decision in hooks_output['potentially_repoable_permissions'].items()))) return set([ permission_name for permission_name, permission_value in hooks_output['potentially_repoable_permissions'].items() if permission_value.repoable ])