def test_admin_verified_for_group_member(self): admin_policy = Policy( 'arn:aws:iam::000000000000:group/admins', 'inline_admin', { 'Version': '2012-10-17', 'Statement': [{ 'Effect': 'Allow', 'Action': '*', 'Resource': '*' }] }) admin_group = Group('arn:aws:iam::000000000000:group/admins', [admin_policy]) not_admin_group = Group('arn:aws:iam::000000000000:group/losers', []) new_node_1 = Node('arn:aws:iam::000000000000:user/node_1', 'id1', [], [admin_group], None, None, 1, False, False, None, False, None) new_node_2 = Node('arn:aws:iam::000000000000:user/node_2', 'id2', [], [not_admin_group], None, None, 1, False, False, None, False, None) update_admin_status([new_node_1, new_node_2]) self.assertTrue(new_node_1.is_admin, 'Member of admin group should be marked as an admin') self.assertFalse( new_node_2.is_admin, 'Member of non-admin group should not be marked as an admin')
def build_playground_graph() -> Graph: """Constructs and returns a Graph objects with many nodes, edges, groups, and policies""" common_iam_prefix = 'arn:aws:iam::000000000000:' # policies to use and add admin_policy = Policy('arn:aws:iam::aws:policy/AdministratorAccess', 'AdministratorAccess', _get_admin_policy()) ec2_for_ssm_policy = Policy('arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM', 'AmazonEC2RoleforSSM', _get_ec2_for_ssm_policy()) s3_full_access_policy = Policy('arn:aws:iam::aws:policy/AmazonS3FullAccess', 'AmazonS3FullAccess', _get_s3_full_access_policy()) jump_policy = Policy('arn:aws:iam::000000000000:policy/JumpPolicy', 'JumpPolicy', _get_jump_policy()) policies = [admin_policy, ec2_for_ssm_policy, s3_full_access_policy, jump_policy] # IAM role trust docs to be used ec2_trusted_policy_doc = _make_trust_document({'Service': 'ec2.amazonaws.com'}) root_trusted_policy_doc = _make_trust_document({'AWS': 'arn:aws:iam::000000000000:root'}) alt_root_trusted_policy_doc = _make_trust_document({'AWS': '000000000000'}) other_acct_trusted_policy_doc = _make_trust_document({'AWS': '999999999999'}) # nodes to add nodes = [] # Regular admin user nodes.append(Node(common_iam_prefix + 'user/admin', 'AIDA00000000000000000', [admin_policy], [], None, None, 1, True, True)) # Regular ec2 role nodes.append(Node(common_iam_prefix + 'role/ec2_ssm_role', 'AIDA00000000000000001', [ec2_for_ssm_policy], [], ec2_trusted_policy_doc, common_iam_prefix + 'instance-profile/ec2_ssm_role', 0, False, False)) # ec2 role with admin nodes.append(Node(common_iam_prefix + 'role/ec2_admin_role', 'AIDA00000000000000002', [ec2_for_ssm_policy], [], ec2_trusted_policy_doc, common_iam_prefix + 'instance-profile/ec2_admin_role', 0, False, True)) # assumable role with s3 access nodes.append(Node(common_iam_prefix + 'role/s3_access_role', 'AIDA00000000000000003', [s3_full_access_policy], [], root_trusted_policy_doc, None, 0, False, False)) # second assumable role with s3 access with alternative trust policy nodes.append(Node(common_iam_prefix + 'role/s3_access_role_alt', 'AIDA00000000000000004', [s3_full_access_policy], [], alt_root_trusted_policy_doc, None, 0, False, False)) # externally assumable role with s3 access nodes.append(Node(common_iam_prefix + 'role/external_s3_access_role', 'AIDA00000000000000005', [s3_full_access_policy], [], other_acct_trusted_policy_doc, None, 0, False, False)) # jump user with access to sts:AssumeRole nodes.append(Node(common_iam_prefix + 'user/jumpuser', 'AIDA00000000000000006', [jump_policy], [], None, None, 1, True, False)) # user with S3 access, path in user's ARN nodes.append(Node(common_iam_prefix + 'user/somepath/some_other_jumpuser', 'AIDA00000000000000007', [jump_policy], [], None, None, 1, True, False)) # role with S3 access, path in role's ARN nodes.append(Node(common_iam_prefix + 'role/somepath/somerole', 'AIDA00000000000000008', [s3_full_access_policy], [], alt_root_trusted_policy_doc, None, 0, False, False)) # edges to add edges = obtain_edges(None, checker_map.keys(), nodes, sys.stdout, True) return Graph(nodes, edges, policies, [], _get_default_metadata())
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 build_graph_with_one_admin() -> Graph: """Constructs and returns a Graph object with one node that is an admin""" admin_user_arn = 'arn:aws:iam::000000000000:user/admin' policy = Policy(admin_user_arn, 'InlineAdminPolicy', _get_admin_policy()) node = Node(admin_user_arn, 'AIDA00000000000000000', [policy], [], None, None, 1, True, True, None, False, None) return Graph([node], [], [policy], [], _get_default_metadata())
def _build_user_with_policy(policy_dict, policy_name='single_user_policy', user_name='asdf', number='0') -> Node: """Helper function: builds an IAM User with a given input policy.""" policy = Policy('arn:aws:iam::000000000000:policy/{}'.format(policy_name), policy_name, policy_dict) result = Node('arn:aws:iam::000000000000:user/{}'.format(user_name), 'AIDA0000000000000000{}'.format(number), [policy], [], None, None, 1, True, False, None, False, None) return result
def test_admin_verified_by_inline_policies(self): admin_policy = Policy( 'arn:aws:iam::000000000000:user/user_1', 'inline_admin', { 'Version': '2012-10-17', 'Statement': [{ 'Effect': 'Allow', 'Action': '*', 'Resource': '*' }] }) not_admin_policy = Policy( 'arn:aws:iam::000000000000:user/user_2', 'inline_not_admin', { 'Version': '2012-10-17', 'Statement': [{ 'Effect': 'Allow', 'Action': '*', 'Resource': '*' }, { 'Effect': 'Deny', 'Action': '*', 'Resource': '*' }] }) new_node_1 = Node('arn:aws:iam::000000000000:user/user_1', 'id1', [admin_policy], [], None, None, 1, False, False, None, False, None) new_node_2 = Node('arn:aws:iam::000000000000:user/user_2', 'id2', [not_admin_policy], [], None, None, 1, False, False, None, False, None) update_admin_status([new_node_1, new_node_2]) self.assertTrue(new_node_1.is_admin, 'User with admin policy should be marked as an admin') self.assertFalse( new_node_2.is_admin, 'User with non-admin policy should not be marked as an admin')
def get_edges_interaccount(source_graph: Graph, inter_account_edges: List[Edge], node: Node, ignored_nodes: List[Node]) -> List[Edge]: """Given a Node, the Graph it belongs to, a list of inter-account Edges, and a list of Nodes to skip, this returns any Edges where the Node is the source element as long as the destination element isn't included in the skipped Nodes. If the given node is an admin, those Edge objects get generated and returned. """ result = [] for outbound_edge in node.get_outbound_edges(source_graph): if outbound_edge.destination not in ignored_nodes: result.append(outbound_edge) for inter_account_edge in inter_account_edges: if inter_account_edge.source == node and inter_account_edge.destination not in ignored_nodes: result.append(inter_account_edge) return result
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 get_edges_with_node_source(graph: Graph, node: Node, ignored_nodes: List[Node]) -> List[Edge]: """Returns a list of nodes that are the destination of edges from the given graph where source of the edge is the passed node. """ return [x for x in node.get_outbound_edges(graph) if x.source not in ignored_nodes]
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
def main(): """Body of the script.""" # handle arguments parser = argparse.ArgumentParser() parser.add_argument('--account', default='000000000000', help='The account ID to assign the simulated Graph') file_arg_group = parser.add_mutually_exclusive_group(required=True) file_arg_group.add_argument( '--json', help='The CloudFormation JSON template file to read from') file_arg_group.add_argument( '--yaml', help='The CloudFormation YAML template file to read from') parsed_args = parser.parse_args() # Parse file if parsed_args.json: print('[+] Loading file {}'.format(parsed_args.json)) fd = open(parsed_args.json) data = json.load(fd) else: print('[+] Loading file {}'.format(parsed_args.yaml)) fd = open(parsed_args.yaml) data = yaml.safe_load(fd) fd.close() # Create metadata metadata = { 'account_id': parsed_args.account, 'pmapper_version': principalmapper.__version__ } print('[+] Building a Graph object for an account with ID {}'.format( metadata['account_id'])) if 'Resources' not in data: print('[!] Missing required template element "Resources"') return -1 # Create space to stash all the data we generate groups = [] policies = [] nodes = [] # Handle data from IAM iam_id_counter = 0 template_resources = data['Resources'] # TODO: Handle policies to start # TODO: Handle groups for logical_id, contents in template_resources.items(): # Get data on IAM Users and Roles if contents['Type'] == 'AWS::IAM::User': properties = contents['Properties'] node_path = '/' if 'Path' not in properties else properties['Path'] node_arn = 'arn:aws:iam::{}:user{}'.format( metadata['account_id'], '{}{}'.format(node_path, properties['UserName'])) print('[+] Adding user {}'.format(node_arn)) nodes.append( Node( node_arn, _generate_iam_id('user', iam_id_counter), [], # TODO: add policy handling [], # TODO: add group handling None, None, 0, # TODO: fix access keys stuff False, # TODO: fix password handling False, # TODO: implement admin checks None, # TODO: handle permission boundaries False, # TODO: handle MFA stuff in CF template reading {} # TODO: add tag handling )) iam_id_counter += 1 elif contents['Type'] == 'AWS::IAM::Role': properties = contents['Properties'] # TODO: finish out roles # TODO: update access keys for users # Sort out administrative principals gathering.update_admin_status(nodes) # Create Edges edges = iam_edges.generate_edges_locally( nodes) + sts_edges.generate_edges_locally(nodes) # Create our graph and finish graph = Graph(nodes, edges, policies, groups, metadata) graph_actions.print_graph_data(graph)