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
示例#3
0
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
示例#5
0
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
示例#11
0
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
示例#12
0
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
示例#16
0
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
示例#18
0
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
示例#20
0
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
示例#23
0
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