def get_unfilled_groups(iamclient, nodes: List[Node]) -> List[Group]: """Using an IAM.Client object, returns a list of Group objects. Adds to each passed Node's group_memberships property. Does not set Policy objects. Those have to be filled in later. Writes high-level progress information to parameter output """ result = [] # paginate through groups and build result logger.info("Obtaining IAM groups in the account.") group_paginator = iamclient.get_paginator('list_groups') for page in group_paginator.paginate(PaginationConfig={'PageSize': 25}): logger.debug('list_groups page: {}'.format(page)) for group in page['Groups']: result.append(Group(arn=group['Arn'], attached_policies=[])) # loop through group memberships logger.info("Connecting IAM users to their groups.") for node in nodes: if not arns.get_resource(node.arn).startswith('user/'): continue # skip when not an IAM user logger.debug('finding groups for user {}'.format(node.arn)) user_name = arns.get_resource(node.arn)[5:] if '/' in user_name: user_name = user_name.split('/')[-1] logger.debug('removed path from username {}'.format(user_name)) group_list = iamclient.list_groups_for_user(UserName=user_name) for group in group_list['Groups']: for group_obj in result: if group['Arn'] == group_obj.arn: node.group_memberships.append(group_obj) return result
def pull_resource_policy_by_arn(session: botocore.session.Session, arn: Optional[str], query: str = None) -> dict: """helper function for pulling the resource policy for a resource at the denoted ARN. raises ValueError if it cannot be retrieved, or a botocore ClientError if another issue arises """ if query is not None: if arn is not None: raise ValueError('Must specify either arn or query, not both.') pattern = re.compile(r'.*(arn:[^:]*:[^:]*:[^:]*:[^:]*:\S+).*') matches = pattern.match(query) if matches is None: raise ValueError('Resource policy retrieval error: could not extract resource ARN from query') arn = matches.group(1) if '?' in arn or '*' in arn: raise ValueError('Resource component from query must not have wildcard (? or *) when evaluating ' 'resource policies.') service = arns.get_service(arn) if service == 'iam': # arn:aws:iam::<account_id>:role/<role_name> client = session.create_client('iam') role_name = arns.get_resource(arn).split('/')[-1] logger.debug('Calling IAM API to retrieve AssumeRolePolicyDocument of {}'.format(role_name)) trust_doc = client.get_role(RoleName=role_name)['Role']['AssumeRolePolicyDocument'] return trust_doc elif service == 's3': # arn:aws:s3:::<bucket>/<path_to_object_with_potential_colons> client = session.create_client('s3') bucket_name = arns.get_resource(arn).split('arn:aws:s3:::')[-1].split('/')[0] logger.debug('Calling S3 API to retrieve bucket policy of {}'.format(bucket_name)) bucket_policy = json.loads(client.get_bucket_policy(Bucket=bucket_name)['Policy']) return bucket_policy elif service == 'sns': region = arns.get_region(arn) client = session.create_client('sns', region_name=region) logger.debug('Calling SNS API to retrieve topic policy of {}'.format(arn)) policy_str = client.get_topic_attributes(TopicArn=arn)['Attributes']['Policy'] return json.loads(policy_str) elif service == 'sqs': region = arns.get_region(arn) client = session.create_client('sqs', region_name=region) logger.debug('Calling SQS API to retrieve queue policy of {}'.format(arn)) queue_url = 'https://sqs.{}.amazonaws.com/{}/{}'.format( arns.get_region(arn), arns.get_account_id(arn), arns.get_resource(arn) ) # TODO: future proof queue URL creation? this still work with FIFO queues? policy_str = client.get_queue_attributes(QueueUrl=queue_url, AttributeNames=['Policy'])['Policy'] return json.loads(policy_str) elif service == 'kms': region = arns.get_region(arn) client = session.create_client('kms', region_name=region) logger.debug('Calling KMS API to retrieve key policy of {}'.format(arn)) key_policy = json.loads(client.get_key_policy(KeyId=arn, PolicyName='default')['Policy']) return key_policy
def pull_cached_resource_policy_by_arn(graph: Graph, arn: Optional[str], query: str = None) -> Union[Policy, dict]: """Function that pulls a resource policy that's cached on-disk from the given Graph object. Returns either a Policy object or a dictionary representing the resource policy. Caller is responsible for checking before sending it along to other components. Raises ValueError if it is not able to be retrieved. """ if query is not None: if arn is not None: raise ValueError('Must specify either arn or query, not both.') pattern = re.compile(r'.*(arn:[^:]*:[^:]*:[^:]*:[^:]*:\S+).*') matches = pattern.match(query) if matches is None: raise ValueError('Resource policy retrieval error: could not extract resource ARN from query') arn = matches.group(1) if '?' in arn or '*' in arn: raise ValueError('Resource component from query must not have wildcard (? or *) when evaluating ' 'resource policies.') logger.debug('Looking for cached policy for {}'.format(arn)) # manipulate the ARN as needed service = arns.get_service(arn) if service == 's3': # we only need the ARN of the bucket search_arn = 'arn:{}:s3:::{}'.format(arns.get_partition(arn), arns.get_resource(arn).split('/')[0]) elif service == 'iam': # special case: trust policies role_name = arns.get_resource(arn).split('/')[-1] # get the last part of :role/path/to/role_name role_node = graph.get_node_by_searchable_name('role/{}'.format(role_name)) return role_node.trust_policy elif service == 'sns': search_arn = arn elif service == 'sqs': search_arn = arn elif service == 'kms': search_arn = arn elif service == 'secretsmanager': search_arn = arn else: raise NotImplementedError('Service policies for {} are not (currently) cached.'.format(service)) for policy in graph.policies: if search_arn == policy.arn: return policy raise ValueError('Unable to locate a cached policy for resource {}'.format(arn))
def _get_admin_reason(node: Node) -> List[str]: """Return a list of reasons why this given node is an admin.""" result = [] logger.debug("Checking if {} is an admin".format(node.searchable_name())) node_type = arns.get_resource(node.arn).split('/')[0] # check if node can modify its own inline policies if node_type == 'user': action = 'iam:PutUserPolicy' else: # node_type == 'role' action = 'iam:PutRolePolicy' if query_interface.local_check_authorization_handling_mfa(node, action, node.arn, {})[0]: result.append('Can call {} to add/update their own inline policies'.format(action)) # check if node can attach the AdministratorAccess policy to itself if node_type == 'user': action = 'iam:AttachUserPolicy' else: action = 'iam:AttachRolePolicy' condition_keys = {'iam:PolicyARN': 'arn:aws:iam::aws:policy/AdministratorAccess'} if query_interface.local_check_authorization_handling_mfa(node, action, node.arn, condition_keys)[0]: result.append('Can call {} to attach the AdministratorAccess policy to itself'.format(action)) # check if node can create a role and attach the AdministratorAccess policy or an inline policy if query_interface.local_check_authorization_handling_mfa(node, 'iam:CreateRole', '*', {})[0]: if query_interface.local_check_authorization_handling_mfa(node, 'iam:AttachRolePolicy', '*', condition_keys)[0]: result.append('Can create an IAM Role (iam:CreateRole) and attach the AdministratorAccess policy to it (iam:AttachRolePolicy)'.format(action)) if query_interface.local_check_authorization_handling_mfa(node, 'iam:PutRolePolicy', '*', condition_keys)[0]: result.append('Can create an IAM Role (iam:CreateRole) and create an inline policy for it (iam:PutRolePolicy)'.format(action)) # check if node can update an attached customer-managed policy (assumes SetAsDefault is set to True) for attached_policy in node.attached_policies: if attached_policy.arn != node.arn and ':aws:policy/' not in attached_policy.arn: if query_interface.local_check_authorization_handling_mfa(node, 'iam:CreatePolicyVersion', attached_policy.arn, {})[0]: result.append('Can modify the attached managed policy {} (iam:CreatePolicyVersion)'.format(attached_policy.arn)) break # reduce output # check if node is a user, and if it can attach or modify any of its groups's policies if node_type == 'user': for group in node.group_memberships: group_name = group.arn.split('/')[-1] if query_interface.local_check_authorization_handling_mfa(node, 'iam:PutGroupPolicy', group.arn, {})[0]: result.append('Can add/update an inline policy for the group {} (iam:PutGroupPolicy)'.format(group_name)) if query_interface.local_check_authorization_handling_mfa(node, 'iam:AttachGroupPolicy', group.arn, condition_keys)[0]: result.append('Can attach the AdministratorAccess policy to the group {} (iam:AttachGroupPolicy)'.format(group_name)) for attached_policy in group.attached_policies: if attached_policy.arn != group.arn and ':aws:policy/' not in attached_policy.arn: if query_interface.local_check_authorization_handling_mfa(node, 'iam:CreatePolicyVersion', attached_policy.arn, {})[0]: result.append('Can update the managed policy {} that is attached to the group {} (iam:CreatePolicyVersion)'.format(attached_policy.arn, group_name)) break # reduce output return result
def searchable_name(self): """Creates and caches the searchable name of this node. First it splits the user/.../name into its parts divided by slashes, then returns the first and last element. The last element is supposed to be unique within users and roles (RoleName/--role-name or UserName/--user-name parameter when using the API/CLI). """ if 'searchable_name' not in self.cache: components = arns.get_resource(self.arn).split('/') self.cache['searchable_name'] = "{}/{}".format(components[0], components[-1]) return self.cache['searchable_name']
def __init__(self, arn: str, attached_policies: Optional[List[Policy]]): """Constructor""" if arn is None or not arns.get_resource(arn).startswith('group/'): raise ValueError('Group objects must be constructed with a valid ARN for a group') self.arn = arn if attached_policies is None: self.attached_policies = [] else: self.attached_policies = attached_policies
def __init__(self, arn: str, id_value: str, attached_policies: Optional[List[Policy]], group_memberships: Optional[List[Group]], trust_policy: Optional[dict], instance_profile: Optional[str], num_access_keys: int, active_password: bool, is_admin: bool): """Constructor. Expects an ARN and ID value. Validates parameters based on the type of Node (User/Role), and rejects contradictory arguments like an IAM User with a trust policy. """ resource_value = arns.get_resource(arn) if arn is None or not (resource_value.startswith('user/') or resource_value.startswith('role/')): raise ValueError('The parameter arn must be a valid ARN for an IAM user or role.') self.arn = arn if id_value is None or len(id_value) == 0: raise ValueError('The parameter id_value must be a non-empty string.') self.id_value = id_value if attached_policies is None: self.attached_policies = [] else: self.attached_policies = attached_policies if group_memberships is None: self.group_memberships = [] else: self.group_memberships = group_memberships if resource_value.startswith('user/') and trust_policy is not None: raise ValueError('IAM users do not have trust policies, pass None for the parameter trust_policy.') if resource_value.startswith('role/') and (trust_policy is None or not isinstance(trust_policy, dict)): raise ValueError('IAM roles have trust policies, which must be passed as a dictionary in trust_policy') self.trust_policy = trust_policy # None denotes no trust policy (not a role), {} denotes empty trust policy if resource_value.startswith('user/') and instance_profile is not None: raise ValueError('IAM users do not have instance profiles. Pass None for the parameter instance_profile.') self.instance_profile = instance_profile self.active_password = active_password if num_access_keys is None: self.access_keys = [] else: self.access_keys = num_access_keys self.is_admin = is_admin self.cache = {}
def pull_cached_resource_policy_by_arn(policies: List[Policy], arn: Optional[str], query: str = None) -> Policy: """Function that pulls a resource policy that's cached on-disk. Raises ValueError if it is not able to be retrieved. Returns the dict, not the Policy object. """ if query is not None: if arn is not None: raise ValueError('Must specify either arn or query, not both.') pattern = re.compile(r'.*(arn:[^:]*:[^:]*:[^:]*:[^:]*:\S+).*') matches = pattern.match(query) if matches is None: raise ValueError('Resource policy retrieval error: could not extract resource ARN from query') arn = matches.group(1) if '?' in arn or '*' in arn: raise ValueError('Resource component from query must not have wildcard (? or *) when evaluating ' 'resource policies.') # manipulate the ARN as needed service = arns.get_service(arn) if service == 's3': # we only need the ARN of the bucket search_arn = 'arn:{}:s3:::{}'.format(arns.get_partition(arn), arns.get_resource(arn).split('/')[0]) elif service == 'iam': search_arn = arn elif service == 'sns': search_arn = arn elif service == 'sqs': search_arn = arn elif service == 'kms': search_arn = arn elif service == 'secretsmanager': search_arn = arn else: raise NotImplementedError('Service policies for {} are not (currently) cached.'.format(service)) for policy in policies: if search_arn == policy.arn: return policy raise ValueError('Unable to locate a cached policy for resource {}'.format(arn))
def describe_edge(self) -> str: """Returns a human-readable string explaining the edge""" return "{} {} {}".format(arns.get_resource(self.source.arn), self.reason, arns.get_resource(self.destination.arn))
def get_unfilled_nodes(iamclient, output: io.StringIO = os.devnull, debug=False) -> List[Node]: """Using an IAM.Client object, return a list of Node object for each IAM user and role in an account. Does not set Group or Policy objects. Those have to be filled in later. Writes high-level information on progress to the output file """ result = [] # Get users, paginating results, still need to handle policies + group memberships + is_admin output.write("Obtaining IAM users in account\n") user_paginator = iamclient.get_paginator('list_users') for page in user_paginator.paginate(PaginationConfig={'PageSize': 25}): dprint(debug, 'list_users page: {}'.format(page)) for user in page['Users']: result.append( Node(arn=user['Arn'], id_value=user['UserId'], attached_policies=[], group_memberships=[], trust_policy=None, instance_profile=None, num_access_keys=0, active_password='******' in user, is_admin=False)) dprint(debug, 'Adding Node for user ' + user['Arn']) # Get roles, paginating results, still need to handle policies + is_admin output.write("Obtaining IAM roles in account\n") role_paginator = iamclient.get_paginator('list_roles') for page in role_paginator.paginate(PaginationConfig={'PageSize': 25}): dprint(debug, 'list_roles page: {}'.format(page)) for role in page['Roles']: result.append( Node(arn=role['Arn'], id_value=role['RoleId'], attached_policies=[], group_memberships=[], trust_policy=role['AssumeRolePolicyDocument'], instance_profile=None, num_access_keys=0, active_password=False, is_admin=False)) # Get instance profiles, paginating results, and attach to roles as appropriate output.write("Obtaining EC2 instance profiles in account\n") ip_paginator = iamclient.get_paginator('list_instance_profiles') for page in ip_paginator.paginate(PaginationConfig={'PageSize': 25}): dprint(debug, 'list_instance_profiles page: {}'.format(page)) for iprofile in page['InstanceProfiles']: iprofile_arn = iprofile['Arn'] role_arns = [] for role in iprofile['Roles']: role_arns.append(role['Arn']) for node in result: if ':role/' in node.arn and node.arn in role_arns: node.instance_profile = iprofile_arn # Handle access keys output.write("Obtaining Access Keys data for IAM users\n") for node in result: if arns.get_resource(node.arn).startswith('user/'): # Grab access-key count and update node user_name = arns.get_resource(node.arn)[5:] if '/' in user_name: user_name = user_name.split('/')[-1] dprint(debug, 'removed path from username {}'.format(user_name)) access_keys_data = iamclient.list_access_keys(UserName=user_name) num_access_keys = 0 for access_key in access_keys_data['AccessKeyMetadata']: if access_key['Status'] == 'Active': num_access_keys += 1 node.access_keys = num_access_keys dprint( debug, 'Access Key Count for {}: {} {}'.format( user_name, len(access_keys_data['AccessKeyMetadata']), num_access_keys)) return result
def update_admin_status(nodes: List[Node], output: io.StringIO = os.devnull, debug: bool = False) -> None: """Given a list of nodes, goes through and updates each node's is_admin data.""" for node in nodes: output.write("checking if {} is an admin\n".format( node.searchable_name())) node_type = arns.get_resource(node.arn).split('/')[0] # check if node can modify its own inline policies if node_type == 'user': action = 'iam:PutUserPolicy' else: # node_type == 'role' action = 'iam:PutRolePolicy' if query_interface.local_check_authorization_handling_mfa( node, action, node.arn, {}, debug)[0]: node.is_admin = True continue # check if node can attach the AdministratorAccess policy to itself if node_type == 'user': action = 'iam:AttachUserPolicy' else: action = 'iam:AttachRolePolicy' condition_keys = { 'iam:PolicyARN': 'arn:aws:iam::aws:policy/AdministratorAccess' } if query_interface.local_check_authorization_handling_mfa( node, action, node.arn, condition_keys, debug)[0]: node.is_admin = True continue # check if node can create a role and attach the AdministratorAccess policy or an inline policy if query_interface.local_check_authorization_handling_mfa( node, 'iam:CreateRole', '*', {}, debug)[0]: if query_interface.local_check_authorization_handling_mfa( node, 'iam:AttachRolePolicy', '*', condition_keys, debug)[0]: node.is_admin = True continue if query_interface.local_check_authorization_handling_mfa( node, 'iam:PutRolePolicy', '*', condition_keys, debug)[0]: node.is_admin = True continue # check if node can update an attached customer-managed policy (assumes SetAsDefault is set to True) for attached_policy in node.attached_policies: if attached_policy.arn != node.arn: if query_interface.local_check_authorization_handling_mfa( node, 'iam:CreatePolicyVersion', attached_policy.arn, {}, debug)[0]: node.is_admin = True continue # check if node is a user, and if it can attach or modify any of its groups's policies if node_type == 'user': for group in node.group_memberships: if query_interface.local_check_authorization_handling_mfa( node, 'iam:PutGroupPolicy', group.arn, {}, debug)[0]: node.is_admin = True break # break the loop through groups if query_interface.local_check_authorization_handling_mfa( node, 'iam:AttachGroupPolicy', group.arn, condition_keys, debug)[0]: node.is_admin = True break # as above for attached_policy in group.attached_policies: if attached_policy.arn != group.arn: if query_interface.local_check_authorization_handling_mfa( node, 'iam:CreatePolicyVersion', attached_policy.arn, {}, debug)[0]: node.is_admin = True break # break the loop through policies if node.is_admin: break # break the loop through groups if node.is_admin: continue # if we add more checks later, this optimizes them out when appropriate
def get_policies_and_fill_out(iamclient, nodes: List[Node], groups: List[Group], output: io.StringIO = os.devnull, debug=False) -> List[Policy]: """Using an IAM.Client object, return a list of Policy objects. Adds references to each passed Node and Group object where applicable. Writes high-level progress information to parameter output """ result = [] # navigate through nodes and add policy objects if they do not already exist in result output.write("Obtaining policies used by all IAM users and roles\n") for node in nodes: node_name_components = arns.get_resource(node.arn).split('/') node_type, node_name = node_name_components[0], node_name_components[ -1] dprint(debug, 'Grabbing inline policies for {}'.format(node.arn)) # get inline policies if node_type == 'user': inline_policy_arns = iamclient.list_user_policies( UserName=node_name) # get each inline policy, append it to node's policies and result list for policy_name in inline_policy_arns['PolicyNames']: dprint(debug, ' Grabbing inline policy: {}'.format(policy_name)) inline_policy = iamclient.get_user_policy( UserName=node_name, PolicyName=policy_name) policy_object = Policy( arn=node.arn, name=policy_name, policy_doc=inline_policy['PolicyDocument']) node.attached_policies.append(policy_object) result.append(policy_object) elif node_type == 'role': inline_policy_arns = iamclient.list_role_policies( RoleName=node_name) # get each inline policy, append it to the node's policies and result list # in hindsight, it's possible this could be folded with the above code, assuming the API doesn't change for policy_name in inline_policy_arns['PolicyNames']: dprint(debug, ' Grabbing inline policy: {}'.format(policy_name)) inline_policy = iamclient.get_role_policy( RoleName=node_name, PolicyName=policy_name) policy_object = Policy( arn=node.arn, name=policy_name, policy_doc=inline_policy['PolicyDocument']) node.attached_policies.append(policy_object) result.append(policy_object) # get attached policies for users and roles if node_type == 'user': attached_policies = iamclient.list_attached_user_policies( UserName=node_name) else: # node_type == 'role': attached_policies = iamclient.list_attached_role_policies( RoleName=node_name) for attached_policy in attached_policies['AttachedPolicies']: policy_arn = attached_policy['PolicyArn'] dprint(debug, ' Grabbing managed policy: {}'.format(policy_arn)) # reduce API calls, search existing policies for matching arns policy_object = _get_policy_by_arn(policy_arn, result) if policy_object is None: # Gotta retrieve the policy's current default version dprint(debug, ' Policy cache miss, calling API') policy_response = iamclient.get_policy(PolicyArn=policy_arn) dprint( debug, ' Policy version: {}'.format( policy_response['Policy']['DefaultVersionId'])) policy_version_response = iamclient.get_policy_version( PolicyArn=policy_arn, VersionId=policy_response['Policy']['DefaultVersionId']) policy_object = Policy( arn=policy_arn, name=policy_response['Policy']['PolicyName'], policy_doc=policy_version_response['PolicyVersion'] ['Document']) result.append(policy_object) node.attached_policies.append(policy_object) output.write("Obtaining policies used by IAM groups\n") for group in groups: group_name = arns.get_resource(group.arn).split( '/', 1)[-1] # split by slashes and take the final item dprint(debug, 'Getting policies for: {}'.format(group.arn)) # get inline policies inline_policies = iamclient.list_group_policies(GroupName=group_name) for policy_name in inline_policies['PolicyNames']: dprint(debug, ' Grabbing inline policy: {}'.format(policy_name)) inline_policy = iamclient.get_group_policy(GroupName=group_name, PolicyName=policy_name) policy_object = Policy(arn=group.arn, name=policy_name, policy_doc=inline_policy['PolicyDocument']) group.attached_policies.append(policy_object) result.append(policy_object) # get attached policies attached_policies = iamclient.list_attached_group_policies( GroupName=group_name) for attached_policy in attached_policies['AttachedPolicies']: policy_arn = attached_policy['PolicyArn'] dprint(debug, ' Grabbing managed policy: {}'.format(policy_arn)) # check cached policies first policy_object = _get_policy_by_arn(policy_arn, result) if policy_object is None: dprint(debug, ' Policy cache miss, calling API') policy_response = iamclient.get_policy(PolicyArn=policy_arn) dprint( debug, ' Policy version: {}'.format( policy_response['Policy']['DefaultVersionId'])) policy_version_response = iamclient.get_policy_version( PolicyArn=policy_arn, VersionId=policy_response['Policy']['DefaultVersionId']) policy_object = Policy( arn=policy_arn, name=policy_response['Policy']['PolicyName'], policy_doc=policy_version_response['PolicyVersion'] ['Document']) result.append(policy_object) group.attached_policies.append(policy_object) return result
def get_nodes_groups_and_policies(iamclient) -> dict: """Using an IAM.Client object, return a dictionary containing nodes, groups, and policies to be added to a Graph object. Admin status for the nodes are not updated. Writes high-level information on progress to the output stream. """ logger.info('Obtaining IAM Users/Roles/Groups/Policies in the account.') result_paginator = iamclient.get_paginator( 'get_account_authorization_details') user_results = [] group_results = [] role_results = [] policy_results = [] for page in result_paginator.paginate(): user_results += page['UserDetailList'] group_results += page['GroupDetailList'] role_results += page['RoleDetailList'] policy_results += page['Policies'] logger.info( 'Sorting users, roles, groups, policies, and their relationships.') # this is the result we return: dictionary with nodes/groups/users all filled out result = {'nodes': [], 'groups': [], 'policies': []} for p in policy_results: # go through each policy and update policy_results doc = [ x['Document'] for x in p['PolicyVersionList'] if x['IsDefaultVersion'] ][0] result['policies'].append(Policy(p['Arn'], p['PolicyName'], doc)) for g in group_results: # go through all inline policies and update policy_results group_policies = [] if 'GroupPolicyList' in g: # have to key-check these for p in g['GroupPolicyList']: group_policies.append( Policy( g['Arn'], # inline policies get the same Arn as their principal p['PolicyName'], p['PolicyDocument'])) result[ 'policies'] += group_policies # this is just adding the inline policies for the group for p in g['AttachedManagedPolicies']: group_policies.append( _get_policy_by_arn_or_raise(p['PolicyArn'], result['policies'])) result['groups'].append(Group(g['Arn'], group_policies)) for u in user_results: # go through all inline policies and update policy_results user_policies = [] if 'UserPolicyList' in u: # have to key-check these for p in u['UserPolicyList']: user_policies.append( Policy( u['Arn'], # inline policies inherit the Arn of their principal for the purposes of tracking p['PolicyName'], p['PolicyDocument'])) result['policies'] += user_policies for p in u['AttachedManagedPolicies']: user_policies.append( _get_policy_by_arn_or_raise(p['PolicyArn'], result['policies'])) if 'PermissionsBoundary' in u: boundary_policy = _get_policy_by_arn_or_raise( u['PermissionsBoundary']['PermissionsBoundaryArn'], result['policies']) else: boundary_policy = None group_list = [] for group_name in u['GroupList']: for group in result['groups']: if arns.get_resource(group.arn).split('/')[-1] == group_name: group_list.append(group) break _tags = {} if 'Tags' in u: for tag in u['Tags']: _tags[tag['Key']] = tag['Value'] # still need to figure out access keys result['nodes'].append( Node(u['Arn'], u['UserId'], user_policies, group_list, None, None, 0, 'PasswordLastUsed' in u, False, boundary_policy, False, _tags)) for r in role_results: # go through all inline policies and update policy_results role_policies = [] for p in r['RolePolicyList']: role_policies.append( Policy( r['Arn'], # inline policies inherit the Arn of their principal for the purposes of tracking p['PolicyName'], p['PolicyDocument'])) result['policies'] += role_policies for p in r['AttachedManagedPolicies']: role_policies.append( _get_policy_by_arn_or_raise(p['PolicyArn'], result['policies'])) _tags = {} if 'Tags' in r: for tag in r['Tags']: _tags[tag['Key']] = tag['Value'] result['nodes'].append( Node(r['Arn'], r['RoleId'], role_policies, None, r['AssumeRolePolicyDocument'], [x['Arn'] for x in r['InstanceProfileList']], 0, False, False, None, False, _tags)) logger.info("Obtaining Access Keys data for IAM users") for node in result['nodes']: if arns.get_resource(node.arn).startswith('user/'): # Grab access-key count and update node user_name = arns.get_resource(node.arn)[5:] if '/' in user_name: user_name = user_name.split('/')[-1] access_keys_data = iamclient.list_access_keys(UserName=user_name) node.access_keys = len(access_keys_data['AccessKeyMetadata']) # logger.debug('Access Key Count for {}: {}'.format(user_name, len(access_keys_data['AccessKeyMetadata']))) logger.info('Gathering MFA virtual device information') mfa_paginator = iamclient.get_paginator('list_virtual_mfa_devices') for page in mfa_paginator.paginate(AssignmentStatus='Assigned'): for device in page['VirtualMFADevices']: user_arn = device['User']['Arn'] logger.debug('Found virtual MFA device for {}'.format(user_arn)) for node in result['nodes']: if node.arn == user_arn: node.has_mfa = True break logger.info('Gathering MFA physical device information') for node in result['nodes']: node_resource_name = arns.get_resource(node.arn) if node_resource_name.startswith('user/'): user_name = node_resource_name.split('/')[-1] mfa_devices_response = iamclient.list_mfa_devices( UserName=user_name) if len(mfa_devices_response['MFADevices']) > 0: node.has_mfa = True return result
def get_unfilled_nodes(iamclient) -> List[Node]: """Using an IAM.Client object, return a list of Node object for each IAM user and role in an account. Does not set Group or Policy objects, does not set permissions boundary attr. Those have to be filled in later. Writes high-level information on progress to the output file """ result = [] # Get users, paginating results, still need to handle policies + group memberships + is_admin logger.info("Obtaining IAM users in account") user_paginator = iamclient.get_paginator('list_users') for page in user_paginator.paginate(PaginationConfig={'PageSize': 25}): logger.debug('list_users page: {}'.format(page)) for user in page['Users']: # grab permission boundary ARN if applicable # TODO: iam:ListUsers does not return boundary information. may need to wait for a fix. if 'PermissionsBoundary' in user: _pb = user['PermissionsBoundary']['PermissionsBoundaryArn'] else: _pb = None result.append( Node( arn=user['Arn'], id_value=user['UserId'], attached_policies=[], group_memberships=[], trust_policy=None, instance_profile=None, num_access_keys=0, active_password='******' in user, is_admin=False, permissions_boundary=_pb, has_mfa=False, tags=None # TODO: fix tags for old user-gathering method )) logger.debug('Adding Node for user ' + user['Arn']) # Get roles, paginating results, still need to handle policies + is_admin logger.info("Obtaining IAM roles in account") role_paginator = iamclient.get_paginator('list_roles') for page in role_paginator.paginate(PaginationConfig={'PageSize': 25}): logger.debug('list_roles page: {}'.format(page)) for role in page['Roles']: # grab permission boundary ARN if applicable if 'PermissionsBoundary' in role: _pb = role['PermissionsBoundary']['PermissionsBoundaryArn'] else: _pb = None result.append( Node( arn=role['Arn'], id_value=role['RoleId'], attached_policies=[], group_memberships=[], trust_policy=role['AssumeRolePolicyDocument'], instance_profile=None, num_access_keys=0, active_password=False, is_admin=False, permissions_boundary=_pb, has_mfa=False, tags=None # TODO: fix tags for old role-gathering method )) # Get instance profiles, paginating results, and attach to roles as appropriate logger.info("Obtaining EC2 instance profiles in account") ip_paginator = iamclient.get_paginator('list_instance_profiles') for page in ip_paginator.paginate(PaginationConfig={'PageSize': 25}): logger.debug('list_instance_profiles page: {}'.format(page)) for iprofile in page['InstanceProfiles']: iprofile_arn = iprofile['Arn'] role_arns = [] for role in iprofile['Roles']: role_arns.append(role['Arn']) for node in result: if ':role/' in node.arn and node.arn in role_arns: node.instance_profile = iprofile_arn # Handle access keys logger.info("Obtaining Access Keys data for IAM users") for node in result: if arns.get_resource(node.arn).startswith('user/'): # Grab access-key count and update node user_name = arns.get_resource(node.arn)[5:] if '/' in user_name: user_name = user_name.split('/')[-1] logger.debug('removed path from username {}'.format(user_name)) access_keys_data = iamclient.list_access_keys(UserName=user_name) node.access_keys = len(access_keys_data['AccessKeyMetadata']) logger.debug('Access Key Count for {}: {}'.format( user_name, len(access_keys_data['AccessKeyMetadata']))) return result