def _get_arn_match(block: str, policy_key: str, policy_value: Union[str, List[str]], context: dict, debug: bool = False) -> bool: """Helper method for dealing with Arn* conditions: ArnEquals, ArnLike, ArnNotEquals, ArnNotLike""" dprint(debug, 'Checking {} for value {} with context {}, condition element {}'.format( policy_key, policy_value, context, block )) if_exists_op = 'IfExists' in block for value in _listify_string(policy_value): if 'Not' in block: if policy_key not in context: return True # policy simulator behavior: returns Allowed when context is null for given key in ArnNot* for context_value in _listify_string(context[policy_key]): if not arns.validate_arn(context_value): return False # policy simulator behavior: reject if provided value isn't a legit ARN if _matches_after_expansion(context_value, value, debug=debug): return False else: if policy_key not in context: return if_exists_op for context_value in _listify_string(context[policy_key]): if not arns.validate_arn(context_value): continue # skip invalid arns if _matches_after_expansion(context_value, value, debug=debug): return True # Made it through the loops without an answer, give default response if 'Not' in block: return True else: return False
def has_matching_statement(principal: Node, effect_value: str, action_to_check: str, resource_to_check: str, condition_keys_to_check: dict, debug: bool = False) -> bool: """Locally determine if a node's attached policies (and group's policies if applicable) has at least one matching statement with the given effect. This is the meat of the local policy evaluation. """ dprint( debug, ' Looking for statement match - effect: {}, action: {}, resource: {}, conditions: {}'.format( effect_value, action_to_check, resource_to_check, condition_keys_to_check ) ) for policy in principal.attached_policies: if policy_has_matching_statement(policy, effect_value, action_to_check, resource_to_check, condition_keys_to_check, debug): return True for group in principal.group_memberships: for policy in group.attached_policies: if policy_has_matching_statement(policy, effect_value, action_to_check, resource_to_check, condition_keys_to_check, debug): return True return False
def get_unfilled_groups(iamclient, nodes: List[Node], output: io.StringIO = os.devnull, debug=False) -> 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 output.write("Obtaining IAM groups in the account.\n") group_paginator = iamclient.get_paginator('list_groups') for page in group_paginator.paginate(PaginationConfig={'PageSize': 25}): dprint(debug, 'list_groups page: {}'.format(page)) for group in page['Groups']: result.append(Group( arn=group['Arn'], attached_policies=[] )) # loop through group memberships output.write("Connecting IAM users to their groups.\n") for node in nodes: if not arns.get_resource(node.arn).startswith('user/'): continue # skip when not an IAM user dprint(debug, 'finding groups for user {}'.format(node.arn)) user_name = arns.get_resource(node.arn)[5:] 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 _get_ipaddress_match(block: str, policy_key: str, policy_value: Union[str, List[str]], context: dict, debug: bool = False) -> bool: """Helper method for dealing with *IpAddress conditions: IpAddress, NotIpAddress Parses the policy value as an IPvXNetwork, then the context value as an IPvXAddress, then uses the `in` operator to determine a match. """ dprint(debug, 'Checking {} for value {} with context {}, condition element {}'.format( policy_key, policy_value, context, block )) if_exists_op = 'IfExists' in block for value in _listify_string(policy_value): value_net = ipaddress.ip_network(value) if block == 'IpAddress': if policy_key not in context: return if_exists_op for context_value in _listify_string(context[policy_key]): context_value_addr = ipaddress.ip_address(context_value) if context_value_addr in value_net: return True else: if policy_key not in context: return True # simulator behavior: treat absence as approval for context_value in _listify_string(context[policy_key]): context_value_addr = ipaddress.ip_address(context_value) if context_value_addr in value_net: return False # Finished loops without an answer, give defaults if block == 'IpAddress': return False else: return True
def create_graph(session: botocore.session.Session, service_list: list, output: io.StringIO = os.devnull, debug=False) -> Graph: """Constructs a Graph object. Information about the graph as it's built will be written to the IO parameter `output`. """ stsclient = session.create_client('sts') caller_identity = stsclient.get_caller_identity() dprint(debug, "Caller Identity: {}".format(caller_identity['Arn'])) metadata = { 'account_id': caller_identity['Account'], 'pmapper_version': principalmapper.__version__ } iamclient = session.create_client('iam') # Gather users and roles, generating a Node per user and per role nodes_result = get_unfilled_nodes(iamclient, output, debug) # Gather groups from current list of nodes (users), generate Group objects, attach to nodes in-flight groups_result = get_unfilled_groups(iamclient, nodes_result, output, debug) # Resolve all policies, generate Policy objects, attach to all groups and nodes policies_result = get_policies_and_fill_out(iamclient, nodes_result, groups_result, output, debug) # Determine which nodes are admins and update node objects update_admin_status(nodes_result, output, debug) # Generate edges, generate Edge objects edges_result = edge_identification.obtain_edges(session, service_list, nodes_result, output, debug) return Graph(nodes_result, edges_result, policies_result, groups_result, metadata)
def _matches_after_expansion(string_to_check: str, string_to_check_against: str, condition_keys: Optional[dict] = None, debug: bool = False) -> bool: """Helper function that checks the string_to_check against string_to_check_against. Handles matching with respect to wildcards, variables. """ dprint(debug, 'Checking for post-expansion match.\n string to check: {}\n '.format(string_to_check) + 'string to check against: {}\n'.format(string_to_check_against) + ' condition_keys: {}'.format(condition_keys)) # regexify string_to_check_against # handles use of ${} var substitution, wildcards (*), and periods (.) copy_string = string_to_check_against if condition_keys is not None: for k, v in condition_keys.items(): if isinstance(v, list): v = str(v) # TODO: how would a multi-valued context value be handled in resource fields? full_key = '${' + k + '}' copy_string = copy_string.replace(full_key, v) pattern_string = copy_string \ .replace(".", "\\.") \ .replace("*", ".*") \ .replace("?", ".") \ .replace("$", "\\$") \ .replace("^", "\\^") pattern_string = "^{}$".format(pattern_string) dprint(debug, ' post-processed pattern_string: {}'.format(pattern_string)) # return result of match return re.match(pattern_string, string_to_check, flags=re.IGNORECASE) is not None
def policy_has_matching_statement(policy: Policy, effect_value: str, action_to_check: str, resource_to_check: str, condition_keys_to_check: dict, debug: bool = False) -> bool: """Searches a specific Policy object""" dprint(debug, 'looking at policy named: {}\n'.format(policy.name)) # go through each policy_doc for statement in _listify_dictionary(policy.policy_doc['Statement']): if statement['Effect'] != effect_value: continue # skip if effect doesn't match dprint(debug, 'Checking statement: {}\n'.format(str(statement))) matches_action, matches_resource, matches_condition = False, False, False # start by checking the action if 'Action' in statement: for action in _listify_string(statement['Action']): if _matches_after_expansion(action_to_check, action, debug=debug): matches_action = True break else: # 'NotAction' in statement matches_action = True for notaction in _listify_string(statement['NotAction']): if _matches_after_expansion(action_to_check, notaction, debug=debug): matches_action = False break # finish looping if not matches_action: continue # cut early # if action is good, check resource if 'Resource' in statement: for resource in _listify_string(statement['Resource']): if _matches_after_expansion(resource_to_check, resource, condition_keys_to_check, debug=debug): matches_resource = True break elif 'NotResource' in statement: # 'NotResource' in statement matches_resource = True for notresource in _listify_string(statement['NotResource']): if _matches_after_expansion(resource_to_check, notresource, condition_keys_to_check, debug=debug): matches_resource = False break else: matches_resource = True # TODO: examine validity of not using a Resource/NotResource field (trust docs) if not matches_resource: continue # cut early # if resource is good, check condition if 'Condition' in statement: matches_condition = _get_condition_match(statement['Condition'], condition_keys_to_check, debug) else: matches_condition = True if matches_action and matches_resource and matches_condition: return True return False
def _get_date_match(block: str, policy_key: str, policy_value: Union[str, List[str]], context: dict, debug: bool = False) -> bool: """Helper method for dealing with Date* conditions: DateEquals, DateNotEquals, DateGreaterThan, DateGreaterThanEquals, DateLessThan, DateLessThanEquals. Parses values by distinguishing between epoch values and ISO 8601/RFC 3339 datetimestamps. Assumes the timezone is UTC when not specified. """ dprint(debug, 'Checking {} for value {} with context {}, condition element {}'.format( policy_key, policy_value, context, block )) if_exists_op = 'IfExists' in block for value in _listify_string(policy_value): value_dt = _convert_timestamp_to_datetime_obj(value) if block == 'DateEquals': if policy_key not in context: return if_exists_op for context_value in _listify_string(context[policy_key]): context_value_dt = _convert_timestamp_to_datetime_obj(context_value) if value_dt == context_value_dt: return True elif block == 'DateNotEquals': if policy_key not in context: return True for context_value in _listify_string(context[policy_key]): context_value_dt = _convert_timestamp_to_datetime_obj(context_value) if value_dt == context_value_dt: return False else: # block == 'DateGreaterThan' or 'DateGreaterThanEquals' or 'DateLessThan' or 'DateLessThanEquals' if policy_key not in context: return if_exists_op for context_value in _listify_string(context[policy_key]): context_value_dt = _convert_timestamp_to_datetime_obj(context_value) if block == 'DateGreaterThan': if context_value_dt > value_dt: return True elif block == 'DateGreaterThanEquals': if context_value_dt >= value_dt: return True elif block == 'DateLessThan': if context_value_dt < value_dt: return True elif block == 'DateLessThanEquals': if context_value_dt <= value_dt: return True return False # Finished loops, give default answers if block == 'DateEquals': return False elif block == 'DateNotEquals': return True else: # DateGreaterThan, DateGreaterThanEquals, DateLessThan, DateLessThanEquals return False
def _get_num_match(block: str, policy_key: str, policy_value: Union[str, List[str]], context: dict, debug: bool = False) -> bool: """Helper method for dealing with Numeric* conditions, including: NumericEquals, NumericNotEquals, NumericLessThan, NumericLessThanEquals, NumericGreaterThan, NumericGreaterThanEquals Parses the string inputs into numbers before doing comparisons. """ dprint(debug, 'Checking {} for value {} with context {}, condition element {}'.format( policy_key, policy_value, context, block )) if_exists_op = 'IfExists' in block if block == 'NumericEquals': if policy_key not in context: return if_exists_op for value in _listify_string(policy_value): value_num = ast.literal_eval(value) for context_value in _listify_string(context[policy_key]): context_value_num = ast.literal_eval(context_value) if value_num == context_value_num: return True return False elif block == 'NumericNotEquals': if policy_key not in context: return True for value in _listify_string(policy_value): value_num = ast.literal_eval(value) for context_value in _listify_string(context[policy_key]): context_value_num = ast.literal_eval(context_value) if value_num == context_value_num: return False return True else: if policy_key not in context: return if_exists_op for value in _listify_string(policy_value): value_num = ast.literal_eval(value) for context_value in _listify_string(context[policy_key]): context_value_num = ast.literal_eval(context_value) if block == 'NumericLessThan': if context_value_num < value_num: return True elif block == 'NumericLessThanEquals': if context_value_num <= value_num: return True elif block == 'NumericGreaterThan': if context_value_num > value_num: return True elif block == 'NumericGreaterThanEquals': if context_value_num >= value_num: return True return False
def _get_null_match(policy_key: str, policy_value: Union[str, List[str]], context: Dict, debug: bool = False) -> bool: """Helper method for dealing with Null conditions""" dprint(debug, 'Checking {} for value {} with context {}'.format(policy_key, policy_value, context)) for value in _listify_string(policy_value): if value == 'true': # key is expected not to be in context, or empty if policy_key not in context or context[policy_key] == '': return True else: # key is expected to be in the context with a non-empty value if policy_key in context and context[policy_key] != '': return True return False
def obtain_edges(session: Optional[botocore.session.Session], checker_list: List[str], nodes: List[Node], output: io.StringIO = os.devnull, debug: bool = False) -> List[Edge]: """Given a list of nodes and a botocore Session, return a list of edges between those nodes. Only checks against services passed in the checker_list param. """ result = [] output.write('Initiating edge checks.\n') dprint(debug, 'Checker map: {}'.format(checker_map)) dprint(debug, 'Checker list: {}'.format(checker_list)) for check in checker_list: if check in checker_map: output.write('running edge check for service: {}\n'.format(check)) checker_obj = checker_map[check](session) result.extend(checker_obj.return_edges(nodes, output, debug)) return result
def get_existing_graph(session: Optional[botocore.session.Session], account: Optional[str], debug=False) -> Graph: """Returns a Graph object stored on-disk in a standard location (per-OS, using the get_storage_root utility function in principalmapper.util.storage). Uses the session/account parameter to choose the directory from under the standard location. """ if account is not None: dprint(debug, 'Loading account data based on parameter --account') graph = get_graph_from_disk(os.path.join(get_storage_root(), account)) elif session is not None: dprint(debug, 'Loading account data using a botocore session object') stsclient = session.create_client('sts') response = stsclient.get_caller_identity() graph = get_graph_from_disk(os.path.join(get_storage_root(), response['Account'])) else: raise ValueError('One of the parameters `account` or `session` must not be None') return graph
def resource_policy_authorization(node_or_service: Union[Node, str], resource_owner: str, resource_policy: dict, action_to_check: str, resource_to_check: str, condition_keys_to_check: dict, debug: bool) -> ResourcePolicyEvalResult: """Returns a ResourcePolicyEvalResult for a given request, based on the resource policy.""" dprint(debug, "Local resource policy authorization check: Principal {}, Action {}, Resource {}, Condition Keys {}, " "Resource Owner {}".format(node_or_service, action_to_check, resource_to_check, condition_keys_to_check, resource_owner)) matching_statements = resource_policy_matching_statements(node_or_service, resource_policy, action_to_check, resource_to_check, condition_keys_to_check, debug) if len(matching_statements) == 0: return ResourcePolicyEvalResult.NO_MATCH # handle nodes (IAM Users or Roles) if isinstance(node_or_service, Node): # if in a different account, check for denies and wrap it up if arns.get_account_id(node_or_service.arn) != resource_owner: for statement in matching_statements: if statement['Effect'] == 'Deny': return ResourcePolicyEvalResult.DENY_MATCH return ResourcePolicyEvalResult.DIFF_ACCOUNT_MATCH else: # messy part: find denies, then determine if we send back ROOT or NODE match for statement in matching_statements: if statement['Effect'] == 'Deny': return ResourcePolicyEvalResult.DENY_MATCH node_match = False for statement in matching_statements: if 'NotPrincipal' in statement: # NotPrincipal means a node match (tested with S3) node_match = True else: for principal in _listify_string(statement['Principal']): if node_or_service.arn == principal: node_match = True if node_or_service.id_value == principal: node_match = True # 'AIDA.*' and co. can match here if node_match: return ResourcePolicyEvalResult.NODE_MATCH else: return ResourcePolicyEvalResult.ROOT_MATCH else: return ResourcePolicyEvalResult.SERVICE_MATCH
def policies_include_matching_allow_action(principal: Node, action_to_check: str, debug: bool = False) -> bool: """Helper function for online-testing. Does a 'light' scan of a principal's policies to determine if any of their statements have an Allow statement with a matching action. Helps reduce unecessary API calls to iam:SimulatePrincipalPolicy. """ dprint(debug, 'optimization check, determine if {} could even possibly call {}'.format( principal.arn, action_to_check )) for policy in principal.attached_policies: for statement in _listify_dictionary(policy.policy_doc['Statement']): if statement['Effect'] != 'Allow': continue if 'Action' in statement: for action in _listify_string(statement['Action']): if _matches_after_expansion(action_to_check, action, debug=debug): return True else: # 'NotAction' in statement return True # so broad that we'd need to simulate to make sure return False
def _get_straight_str_match(block: str, policy_key: str, policy_value: Union[str, List[str]], context: dict, debug: bool = False) -> bool: """Helper method for dealing with BinaryEquals Does a straight string comparison to search for a match. """ dprint(debug, 'Checking {} for value {} with context {}, condition element {}'.format( policy_key, policy_value, context, block )) # can knock this out up here if policy_key not in context: return False for value in _listify_string(policy_value): for context_value in _listify_string(context[policy_key]): if value == context_value: return True return False
def write_privesc_results(graph: Graph, nodes: List[Node], skip_admins: bool = False, output: io.StringIO = os.devnull, debug: bool = False) -> None: """Handles a privesc query and writes the result to output.""" for node in nodes: dprint(debug, 'Looking at principal {}'.format(node.searchable_name())) if skip_admins and node.is_admin: continue # skip admins if node.is_admin: output.write('{} is an administrative principal\n'.format(node.searchable_name())) continue privesc, edge_list = can_privesc(graph, node, debug) if privesc: end_of_list = edge_list[-1].destination # the node can access this admin node through the current edge list, print this info out output.write('{} can escalate privileges by accessing the administrative principal {}:\n'.format( node.searchable_name(), end_of_list.searchable_name())) for edge in edge_list: output.write(' {}\n'.format(edge.describe_edge()))
def _get_bool_match(block: str, policy_key: str, policy_value: Union[str, List[str]], context: dict, debug: bool = False) -> bool: """Helper method for dealing with Bool. For 'true' policy values, returns True if context has 'true' as a value. For 'false' policy values, returns True if context has value that's not 'true'. Returns False if no context value. """ dprint(debug, 'Checking {} for value {} with context {}, condition element {}'.format( policy_key, policy_value, context, block )) if_exists_op = 'IfExists' in block if policy_key not in context: return if_exists_op for value in _listify_string(policy_value): for context_value in _listify_string(context[policy_key]): if value == 'true' and context_value.lower() == 'true': return True if value == 'false' and context_value.lower() != 'true': return True return False
def main() -> int: """Point of entry for command-line""" argument_parser = argparse.ArgumentParser(prog='pmapper') argument_parser.add_argument( '--profile', help='AWS CLI (botocore) profile to use to call the AWS API' ) # Note: do NOT set the default, we want to know if the profile arg was specified or not argument_parser.add_argument('--debug', action='store_true', help='Produces debug-level output') argument_parser.add_argument( '--account', help= 'When running offline operations, this parameter determines which account to act against.' ) # Create subparser for various subcommands subparser = argument_parser.add_subparsers( title='subcommand', description='The subcommand to use among this suite of tools', dest='picked_cmd', help='Select a subcommand to execute') # Graph subcommand graphparser = subparser.add_parser( 'graph', description= 'Obtains information about a specific AWS account\'s use of IAM for analysis.', help='Pulls information for an AWS account\'s use of IAM.') command_group = graphparser.add_mutually_exclusive_group(required=True) command_group.add_argument( '--create', action='store_true', help= 'Creates a completely new graph for an AWS account, wiping away any old data.' ) command_group.add_argument( '--display', action='store_true', help= 'Displays information about a currently-stored graph based on the AWS credentials used.' ) command_group.add_argument( '--list', action='store_true', help='List the Account IDs of graphs stored on this computer.') command_group.add_argument( '--update-edges', action='store_true', help= 'Updates the edges of an AWS account. Does not gather information about IAM users or roles.' ) # Query subcommand queryparser = subparser.add_parser( 'query', description= 'Displays information corresponding to a roughly human-readable query.', help='Displays information corresponding to a query') queryparser.add_argument( '-s', '--skip-admin', action='store_true', help= 'Ignores "admin" level principals when querying about multiple principals in an account' ) queryparser.add_argument('query', help='The query to execute.') # New Query subcommand argqueryparser = subparser.add_parser( 'argquery', description= 'Displays information corresponding to a arg-specified query.', help='Displays information corresponding to a query') argqueryparser.add_argument( '-s', '--skip-admin', action='store_true', help= 'Ignores administrative principals when querying about multiple principals in an account' ) argqueryparser.add_argument( '--principal', default='*', help= 'A string matching one or more IAM users or roles in the account, or use * (the default) to include all' ) argqueryparser.add_argument( '--action', help='An AWS action to test for, allows * wildcards') argqueryparser.add_argument( '--resource', default='*', help='An AWS resource (denoted by ARN) to test for') argqueryparser.add_argument( '--condition', action='append', help='A set of key-value pairs to test specific conditions') argqueryparser.add_argument('--preset', help='A preset query to run') # REPL subcommand replparser = subparser.add_parser( 'repl', description= 'Runs a read-evaluate-print-loop of queries, avoiding the need to read from disk for each query', help='Runs a REPL for querying') # Visualization subcommand visualizationparser = subparser.add_parser( 'visualize', description= 'Generates an image file to display information about an AWS account', help='Generates an image representing the AWS account') visualizationparser.add_argument( '--filetype', default='svg', choices=['svg', 'png', 'dot'], help='The (lowercase) filetype to output the image as.') # Analysis subcommand analysisparser = subparser.add_parser( 'analysis', description='Analyzes and reports identified issues', help='Analyzes and reports identified issues') analysisparser.add_argument( '--output-type', default='text', choices=['text', 'json'], help='The type of output for identified issues.') # TODO: Cross-Account subcommand(s) parsed_args = argument_parser.parse_args() dprint(parsed_args.debug, 'Debugging mode enabled.') dprint(parsed_args.debug, 'Parsed Args: ' + str(parsed_args)) if parsed_args.picked_cmd == 'graph': return handle_graph(parsed_args) elif parsed_args.picked_cmd == 'query': return handle_query(parsed_args) elif parsed_args.picked_cmd == 'argquery': return handle_argquery(parsed_args) elif parsed_args.picked_cmd == 'repl': return handle_repl(parsed_args) elif parsed_args.picked_cmd == 'visualize': return handle_visualization(parsed_args) elif parsed_args.picked_cmd == 'analysis': return handle_analysis(parsed_args) return 64 # /usr/include/sysexits.h
def _get_str_match(block: str, policy_key: str, policy_value: Union[str, List[str]], context: dict, debug: bool = False): """Helper method for dealing with String* conditions, including: StringEquals, StringNotEquals, StringEqualsIgnoreCase, StringNotEqualsIgnoreCase, StringLike, StringNotLike Observed policy simulator behavior for *IgnoreCase: if I compare the following, it returns denied: * ê <- 'LATIN SMALL LETTER E WITH CIRCUMFLEX' * ê <- 'LATIN SMALL LETTER E' + 'COMBINING CIRCUMFLEX ACCENT' So even though they're the "same" they end up not matching. Just using casefold() on the strings is enough to match the policy simulator behavior without having to dip into the insanity of unicode. Many thanks to https://stackoverflow.com/a/29247821 for helping this code on this journey. """ dprint(debug, 'Checking {} for value {} with context {}, condition element {}'.format( policy_key, policy_value, context, block )) if_exists_op = 'IfExists' in block if 'StringEquals' in block: if policy_key not in context: return if_exists_op for value in _listify_string(policy_value): for context_value in _listify_string(context[policy_key]): if 'IgnoreCase' in block: if value.casefold() == context_value.casefold(): return True else: if value == context_value: return True return False elif 'StringLike' in block: if policy_key not in context: return if_exists_op for value in _listify_string(policy_value): for context_value in _listify_string(context[policy_key]): if _expand_str_and_compare(value, context_value): return True return False elif 'StringNotEquals' in block: if policy_key not in context: return True for value in _listify_string(policy_value): for context_value in _listify_string(context[policy_key]): if 'IgnoreCase' in block: if value.casefold() == context_value.casefold(): return False else: if value == context_value: return False return True elif 'StringNotLike' in block: if policy_key not in context: return if_exists_op for value in _listify_string(policy_value): for context_value in _listify_string(context[policy_key]): if _expand_str_and_compare(value, context_value): return False return True
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 resource_policy_has_matching_statement_for_principal(principal: Node, resource_policy: dict, effect_value: str, action_to_check: str, resource_to_check: str, condition_keys_to_check: dict, debug: bool = False) -> bool: """Locally determine if a node is permitted by a resource policy for a given action/resource/condition""" dprint(debug, 'local resource policy check - principal: {}, effect: {}, action: {}, resource: {}, conditions: {}, ' 'resource_policy: {}'.format(principal.arn, effect_value, action_to_check, resource_to_check, condition_keys_to_check, resource_policy)) for statement in _listify_dictionary(resource_policy['Statement']): if statement['Effect'] != effect_value: continue matches_principal, matches_action, matches_resource, matches_condition = False, False, False, False if 'Principal' in statement: # should be a dictionary if 'AWS' in statement['Principal']: if _principal_matches_in_statement(principal, _listify_string(statement['Principal']['AWS'])): matches_principal = True else: # 'NotPrincipal' in statement: matches_principal = True if 'AWS' in statement['NotPrincipal']: if _principal_matches_in_statement(principal, _listify_string(statement['NotPrincipal']['AWS'])): matches_principal = False if not matches_principal: continue # if principal is good, proceed to check the Action if 'Action' in statement: for action in _listify_string(statement['Action']): matches_action = _matches_after_expansion(action_to_check, action, debug=debug) break else: # 'NotAction' in statement matches_action = True for notaction in _listify_string(statement['NotAction']): if _matches_after_expansion(action_to_check, notaction, debug=debug): matches_action = False break # finish looping if not matches_action: continue # if action is good, proceed to check resource if 'Resource' in statement: for resource in _listify_string(statement['Resource']): if _matches_after_expansion(resource_to_check, resource, debug=debug): matches_resource = True break elif 'NotResource' in statement: matches_resource = True for notresource in _listify_string(statement['NotResource']): if _matches_after_expansion(resource_to_check, notresource, debug=debug): matches_resource = False break else: # no resource element (seen in IAM role trust policies), treat as a match matches_resource = True # if resource is good, check condition matches_condition = True # TODO: handle this in local evaluation if matches_principal and matches_action and matches_resource and matches_condition: return True return False
def resource_policy_matching_statements(node_or_service: Union[Node, str], resource_policy: dict, action_to_check: str, resource_to_check: str, condition_keys_to_check: dict, debug: bool = False) -> list: """Returns if a resource policy has a matching statement for a given service (ec2.amazonaws.com for example).""" dprint(debug, 'local resource policy check - service: {}, action: {}, resource: {}, conditions: {}, ' 'resource_policy: {}'.format(node_or_service, action_to_check, resource_to_check, condition_keys_to_check, resource_policy)) results = [] for statement in _listify_dictionary(resource_policy['Statement']): matches_principal, matches_action, matches_resource, matches_condition = False, False, False, False if 'Principal' in statement: # should be a dictionary if isinstance(node_or_service, Node): if 'AWS' in statement['Principal']: if _principal_matches_in_statement(node_or_service, _listify_string(statement['Principal']['AWS'])): matches_principal = True else: if 'Service' in statement['Principal']: if node_or_service in _listify_string(statement['Principal']['Service']): matches_principal = True else: # 'NotPrincipal' in statement: matches_principal = True if isinstance(node_or_service, Node): if 'AWS' in statement['Principal']: if _principal_matches_in_statement(node_or_service, _listify_string(statement['Principal']['AWS'])): matches_principal = False else: if 'Service' in statement['NotPrincipal']: if node_or_service in _listify_string(statement['NotPrincipal']['Service']): matches_principal = False if not matches_principal: continue # if principal is good, proceed to check the Action if 'Action' in statement: for action in _listify_string(statement['Action']): matches_action = _matches_after_expansion(action_to_check, action, debug=debug) break else: # 'NotAction' in statement matches_action = True for notaction in _listify_string(statement['NotAction']): if _matches_after_expansion(action_to_check, notaction, debug=debug): matches_action = False break # finish looping if not matches_action: continue # if action is good, proceed to check resource if 'Resource' in statement: for resource in _listify_string(statement['Resource']): if _matches_after_expansion(resource_to_check, resource, debug=debug): matches_resource = True break elif 'NotResource' in statement: matches_resource = True for notresource in _listify_string(statement['NotResource']): if _matches_after_expansion(resource_to_check, notresource, debug=debug): matches_resource = False break else: # no resource element (seen in IAM role trust policies), treat as a match matches_resource = True # if resource is good, check condition matches_condition = True # TODO: implement local condition check in policy/statement loop if matches_principal and matches_action and matches_resource and matches_condition: results.append(statement) return results
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_condition_match(condition: Dict[str, Dict[str, Union[str, List]]], context: Dict, debug: bool = False) -> bool: """ Internal method. It digs through Null, Bool, DateX, NumericX, StringX conditions and returns false if any of them don't match what the context has. Also handles ForAnyValue and ForAllValues See: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html """ for block in condition.keys(): dprint(debug, 'Testing condition field: {}'.format(block)) # String operators if 'String' in block: # string comparison after expansion if block.startswith('ForAllValues:'): # fail to match unless all of the provided context values match for policy_key in condition[block]: if policy_key in context.keys(): for context_value in _listify_string(context[policy_key]): if context_value != '': if not _get_str_match( block, policy_key, condition[block][policy_key], {policy_key: context_value}, debug): return False elif block.startswith('ForAnyValue:'): # fail to match unless at least one of the provided context values match no_match = True for policy_key in condition[block]: if policy_key in context.keys(): for context_value in _listify_string(context[policy_key]): if context_value != '': if _get_str_match(block, policy_key, condition[block][policy_key], context, debug): no_match = False if no_match: return False else: for policy_context_key in condition[block]: if not _get_str_match(block, policy_context_key, condition[block][policy_context_key], context, debug): return False if 'Numeric' in block: # convert string to int and compare (floats allowed? how to handle?) if block.startswith('ForAllValues:'): # fail to match unless all of the provided context values match for policy_key in condition[block]: if policy_key in context.keys(): for context_value in _listify_string(context[policy_key]): if context_value != '': if not _get_num_match(block, policy_key, condition[block][policy_key], context, debug): return False elif block.startswith('ForAnyValue:'): # fail to match unless at least one of the provided context values match no_match = True for policy_key in condition[block]: if policy_key in context.keys(): for context_value in _listify_string(context[policy_key]): if context_value != '': if _get_num_match(block, policy_key, condition[block][policy_key], context, debug): no_match = False if no_match: return False else: for policy_context_key in condition[block]: if not _get_num_match(block, policy_context_key, condition[block][policy_context_key], context, debug): return False if 'Date' in block: # need the datetime and dateutil module to do this, do everything in UTC where undefined if block.startswith('ForAllValues:'): # fail to match unless all of the provided context values match for policy_key in condition[block]: if policy_key in context.keys(): for context_value in _listify_string(context[policy_key]): if context_value != '': if not _get_date_match(block, policy_key, condition[block][policy_key], context, debug): return False elif block.startswith('ForAnyValue:'): # fail to match unless at least one of the provided context values match no_match = True for policy_key in condition[block]: if policy_key in context.keys(): for context_value in _listify_string(context[policy_key]): if context_value != '': if _get_date_match(block, policy_key, condition[block][policy_key], context, debug): no_match = False if no_match: return False else: for policy_context_key in condition[block]: if not _get_date_match(block, policy_context_key, condition[block][policy_context_key], context, debug): return False if 'Bool' in block: # boolean comparison if block.startswith('ForAllValues:'): # fail to match unless all of the provided context values match for policy_key in condition[block]: if policy_key in context.keys(): for context_value in _listify_string(context[policy_key]): if context_value != '': if not _get_bool_match(block, policy_key, condition[block][policy_key], context, debug): return False elif block.startswith('ForAnyValue:'): # fail to match unless at least one of the provided context values match no_match = True for policy_key in condition[block]: if policy_key in context.keys(): for context_value in _listify_string(context[policy_key]): if context_value != '': if _get_bool_match(block, policy_key, condition[block][policy_key], context, debug): no_match = False if no_match: return False else: for policy_context_key in condition[block]: if not _get_bool_match(block, policy_context_key, condition[block][policy_context_key], context, debug): return False if 'BinaryEquals' in block: # straight string comparison if block.startswith('ForAllValues:'): # fail to match unless all of the provided context values match for policy_key in condition[block]: if policy_key in context.keys(): for context_value in _listify_string(context[policy_key]): if context_value != '': if not _get_straight_str_match(block, policy_key, condition[block][policy_key], context, debug): return False elif block.startswith('ForAnyValue:'): # fail to match unless at least one of the provided context values match no_match = True for policy_key in condition[block]: if policy_key in context.keys(): for context_value in _listify_string(context[policy_key]): if context_value != '': if _get_straight_str_match(block, policy_key, condition[block][policy_key], context, debug): no_match = False if no_match: return False else: for policy_context_key in condition[block]: if not _get_straight_str_match(block, policy_context_key, condition[block][policy_context_key], context, debug): return False if 'IpAddress' in block: # need ipaddress module, use ipaddress.ip_address in <ipaddress.ip_network obj> if block.startswith('ForAllValues:'): # fail to match unless all of the provided context values match for policy_key in condition[block]: if policy_key in context.keys(): for context_value in _listify_string(context[policy_key]): if context_value != '': if not _get_ipaddress_match(block, policy_key, condition[block][policy_key], context, debug): return False elif block.startswith('ForAnyValue:'): # fail to match unless at least one of the provided context values match no_match = True for policy_key in condition[block]: if policy_key in context.keys(): for context_value in _listify_string(context[policy_key]): if context_value != '': if _get_ipaddress_match(block, policy_key, condition[block][policy_key], context, debug): no_match = False if no_match: return False else: for policy_context_key in condition[block]: if not _get_ipaddress_match(block, policy_context_key, condition[block][policy_context_key], context, debug): return False if 'Arn' in block: # string comparison after expansion if block.startswith('ForAllValues:'): # fail to match unless all of the provided context values match for policy_key in condition[block]: if policy_key in context.keys(): for context_value in _listify_string(context[policy_key]): if context_value != '': if not _get_arn_match(block, policy_key, condition[block][policy_key], context, debug): return False elif block.startswith('ForAnyValue:'): # fail to match if none of the provided context values match no_match = True for policy_key in condition[block]: if policy_key in context.keys(): for context_value in _listify_string(context[policy_key]): if context_value != '': if _get_arn_match(block, policy_key, condition[block][policy_key], context, debug): no_match = False if no_match: return False else: for policy_context_key in condition[block]: if not _get_arn_match(block, policy_context_key, condition[block][policy_context_key], context, debug): return False # handle Null, ForAllValues:Null, ForAnyValue:Null if 'Null' in block: if block.startswith('ForAllValues:'): # fail to match unless all of the provided context values match for policy_key in condition[block]: if policy_key in context.keys(): for context_value in _listify_string(context[policy_key]): if context_value != '': if not _get_null_match(policy_key, condition[block][policy_key], context, debug): return False elif block.startswith('ForAnyValue:'): # fail to match unless at least one of the provided context values match no_match = True for policy_key in condition[block]: if policy_key in context.keys(): for context_value in _listify_string(context[policy_key]): if context_value != '': if _get_null_match(policy_key, condition[block][policy_key], context, debug): no_match = False if no_match: return False else: for policy_context_key in condition[block]: if not _get_null_match(policy_context_key, condition[block][policy_context_key], context, debug): return False return True