示例#1
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)
示例#2
0
def build_graph_with_one_admin() -> Graph:
    """Constructs and returns a Graph object with one node that is an admin"""
    admin_user_arn = 'arn:aws:iam::000000000000:user/admin'
    policy = Policy(admin_user_arn, 'InlineAdminPolicy', _get_admin_policy())
    node = Node(admin_user_arn, 'AIDA00000000000000000', [policy], [], None,
                None, 1, True, True, None, False, None)
    return Graph([node], [], [policy], [], _get_default_metadata())
示例#3
0
def build_playground_graph() -> Graph:
    """Constructs and returns a Graph objects with many nodes, edges, groups, and policies"""
    common_iam_prefix = 'arn:aws:iam::000000000000:'

    # policies to use and add
    admin_policy = Policy('arn:aws:iam::aws:policy/AdministratorAccess', 'AdministratorAccess', _get_admin_policy())
    ec2_for_ssm_policy = Policy('arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM', 'AmazonEC2RoleforSSM',
                                _get_ec2_for_ssm_policy())
    s3_full_access_policy = Policy('arn:aws:iam::aws:policy/AmazonS3FullAccess', 'AmazonS3FullAccess',
                                   _get_s3_full_access_policy())
    jump_policy = Policy('arn:aws:iam::000000000000:policy/JumpPolicy', 'JumpPolicy', _get_jump_policy())
    policies = [admin_policy, ec2_for_ssm_policy, s3_full_access_policy, jump_policy]

    # IAM role trust docs to be used
    ec2_trusted_policy_doc = _make_trust_document({'Service': 'ec2.amazonaws.com'})
    root_trusted_policy_doc = _make_trust_document({'AWS': 'arn:aws:iam::000000000000:root'})
    alt_root_trusted_policy_doc = _make_trust_document({'AWS': '000000000000'})
    other_acct_trusted_policy_doc = _make_trust_document({'AWS': '999999999999'})

    # nodes to add
    nodes = []
    # Regular admin user
    nodes.append(Node(common_iam_prefix + 'user/admin', 'AIDA00000000000000000', [admin_policy], [], None, None, 1, True, True))

    # Regular ec2 role
    nodes.append(Node(common_iam_prefix + 'role/ec2_ssm_role', 'AIDA00000000000000001', [ec2_for_ssm_policy], [],
                      ec2_trusted_policy_doc, common_iam_prefix + 'instance-profile/ec2_ssm_role', 0, False, False))

    # ec2 role with admin
    nodes.append(Node(common_iam_prefix + 'role/ec2_admin_role', 'AIDA00000000000000002', [ec2_for_ssm_policy], [], ec2_trusted_policy_doc,
                      common_iam_prefix + 'instance-profile/ec2_admin_role', 0, False, True))

    # assumable role with s3 access
    nodes.append(Node(common_iam_prefix + 'role/s3_access_role', 'AIDA00000000000000003', [s3_full_access_policy], [], root_trusted_policy_doc,
                      None, 0, False, False))

    # second assumable role with s3 access with alternative trust policy
    nodes.append(Node(common_iam_prefix + 'role/s3_access_role_alt', 'AIDA00000000000000004', [s3_full_access_policy], [],
                 alt_root_trusted_policy_doc, None, 0, False, False))

    # externally assumable role with s3 access
    nodes.append(Node(common_iam_prefix + 'role/external_s3_access_role', 'AIDA00000000000000005', [s3_full_access_policy], [],
                      other_acct_trusted_policy_doc, None, 0, False, False))

    # jump user with access to sts:AssumeRole
    nodes.append(Node(common_iam_prefix + 'user/jumpuser', 'AIDA00000000000000006', [jump_policy], [], None, None, 1, True, False))

    # user with S3 access, path in user's ARN
    nodes.append(Node(common_iam_prefix + 'user/somepath/some_other_jumpuser', 'AIDA00000000000000007', [jump_policy],
                      [], None, None, 1, True, False))

    # role with S3 access, path in role's ARN
    nodes.append(Node(common_iam_prefix + 'role/somepath/somerole', 'AIDA00000000000000008', [s3_full_access_policy],
                      [], alt_root_trusted_policy_doc, None, 0, False, False))

    # edges to add
    edges = obtain_edges(None, checker_map.keys(), nodes, sys.stdout, True)

    return Graph(nodes, edges, policies, [], _get_default_metadata())
示例#4
0
def handle_preset_query(graph: Graph, tokens: List[str], skip_admins: bool = False) -> None:
    """Handles a human-readable query that's been chunked into tokens, and writes the result to output."""
    source_target = tokens[2]
    dest_target = tokens[3]

    source_nodes = []
    dest_nodes = []
    if source_target == '*':
        source_nodes.extend(graph.nodes)
    else:
        source_nodes.append(graph.get_node_by_searchable_name(source_target))

    if dest_target == '*':
        dest_nodes.extend(graph.nodes)
    else:
        dest_nodes.append(graph.get_node_by_searchable_name(dest_target))

    print_connected_results(graph, source_nodes, dest_nodes, skip_admins)
示例#5
0
def create_graph(session: botocore.session.Session,
                 service_list: list,
                 region_allow_list: Optional[List[str]] = None,
                 region_deny_list: Optional[List[str]] = None,
                 scps: Optional[List[List[dict]]] = None) -> Graph:
    """Constructs a Graph object.

    Information about the graph as it's built will be written to the IO parameter `output`.

    The region allow/deny lists are mutually-exclusive (i.e. at least one of which has the value None) lists of
    allowed/denied regions to pull data from. Note that we don't do the same allow/deny list parameters for the
    service list, because that is a fixed property of what pmapper supports as opposed to an unknown/uncontrolled
    list of regions that AWS supports.
    """

    stsclient = session.create_client('sts')
    caller_identity = stsclient.get_caller_identity()
    logger.debug("Caller Identity: {}".format(caller_identity['Arn']))
    metadata = {
        'account_id': caller_identity['Account'],
        'pmapper_version': principalmapper.__version__
    }

    iamclient = session.create_client('iam')

    results = get_nodes_groups_and_policies(iamclient)
    nodes_result = results['nodes']
    groups_result = results['groups']
    policies_result = results['policies']

    # Determine which nodes are admins and update node objects
    update_admin_status(nodes_result)

    # Generate edges, generate Edge objects
    edges_result = edge_identification.obtain_edges(session, service_list,
                                                    nodes_result,
                                                    region_allow_list,
                                                    region_deny_list, scps)

    # Pull S3, SNS, SQS, KMS, and Secrets Manager resource policies
    policies_result.extend(get_s3_bucket_policies(session))
    policies_result.extend(
        get_sns_topic_policies(session, region_allow_list, region_deny_list))
    policies_result.extend(
        get_sqs_queue_policies(session, caller_identity['Account'],
                               region_allow_list, region_deny_list))
    policies_result.extend(
        get_kms_key_policies(session, region_allow_list, region_deny_list))
    policies_result.extend(
        get_secrets_manager_policies(session, region_allow_list,
                                     region_deny_list))

    return Graph(nodes_result, edges_result, policies_result, groups_result,
                 metadata)
示例#6
0
def handle_preset_query(graph: Graph, tokens: List[str], skip_admins: bool = False, output: io.StringIO = os.devnull,
                        debug: bool = False) -> None:
    """Handles a human-readable query that's been chunked into tokens, and writes the result to output."""
    # Get the nodes we're determining can privesc or not
    target = tokens[2]
    nodes = []
    if target == '*':
        nodes.extend(graph.nodes)
    else:
        nodes.append(graph.get_node_by_searchable_name(target))
    write_privesc_results(graph, nodes, skip_admins, output, debug)
示例#7
0
def pull_cached_resource_policy_by_arn(graph: Graph, arn: Optional[str], query: str = None) -> Union[Policy, dict]:
    """Function that pulls a resource policy that's cached on-disk from the given Graph object.

    Returns either a Policy object or a dictionary representing the resource policy. Caller is responsible
    for checking before sending it along to other components.

    Raises ValueError if it is not able to be retrieved.
    """
    if query is not None:
        if arn is not None:
            raise ValueError('Must specify either arn or query, not both.')
        pattern = re.compile(r'.*(arn:[^:]*:[^:]*:[^:]*:[^:]*:\S+).*')
        matches = pattern.match(query)
        if matches is None:
            raise ValueError('Resource policy retrieval error: could not extract resource ARN from query')
        arn = matches.group(1)
    if '?' in arn or '*' in arn:
        raise ValueError('Resource component from query must not have wildcard (? or *) when evaluating '
                         'resource policies.')

    logger.debug('Looking for cached policy for {}'.format(arn))

    # manipulate the ARN as needed
    service = arns.get_service(arn)
    if service == 's3':
        # we only need the ARN of the bucket
        search_arn = 'arn:{}:s3:::{}'.format(arns.get_partition(arn), arns.get_resource(arn).split('/')[0])
    elif service == 'iam':
        # special case: trust policies
        role_name = arns.get_resource(arn).split('/')[-1]  # get the last part of :role/path/to/role_name
        role_node = graph.get_node_by_searchable_name('role/{}'.format(role_name))
        return role_node.trust_policy
    elif service == 'sns':
        search_arn = arn
    elif service == 'sqs':
        search_arn = arn
    elif service == 'kms':
        search_arn = arn
    elif service == 'secretsmanager':
        search_arn = arn
    else:
        raise NotImplementedError('Service policies for {} are not (currently) cached.'.format(service))

    for policy in graph.policies:
        if search_arn == policy.arn:
            return policy

    raise ValueError('Unable to locate a cached policy for resource {}'.format(arn))
示例#8
0
def argquery_response(graph: Graph,
                      principal_param: Optional[str],
                      action_param: str,
                      resource_param: Optional[str],
                      condition_param: Optional[dict],
                      skip_admins: bool = False,
                      output: io.StringIO = os.devnull,
                      debug: bool = False) -> None:
    """Writes the output of an argquery to output."""
    result = []

    if resource_param is None:
        resource_param = '*'

    if condition_param is None:
        condition_param = {}

    if principal_param is None or principal_param == '*':
        for node in graph.nodes:
            if skip_admins:
                if not node.is_admin:
                    result.append(
                        search_authorization_for(graph, node, action_param,
                                                 resource_param,
                                                 condition_param, debug))
            else:
                result.append(
                    search_authorization_for(graph, node, action_param,
                                             resource_param, condition_param,
                                             debug))

    else:
        node = graph.get_node_by_searchable_name(principal_param)
        if skip_admins:
            if not node.is_admin:
                result.append(
                    search_authorization_for(graph, node, action_param,
                                             resource_param, condition_param,
                                             debug))
        else:
            result.append(
                search_authorization_for(graph, node, action_param,
                                         resource_param, condition_param,
                                         debug))

    for query_result in result:
        query_result.write_result(action_param, resource_param, output)
示例#9
0
def argquery_response(graph: Graph,
                      principal_param: Optional[str],
                      action_param: str,
                      resource_param: Optional[str],
                      condition_param: Optional[dict],
                      skip_admins: bool = False,
                      resource_policy: dict = None,
                      resource_owner: str = None,
                      include_unauthorized: bool = False,
                      session_policy: Optional[dict] = None,
                      scps: Optional[List[List[dict]]] = None) -> None:
    """Prints the output of a non-preset argquery"""
    result = []

    if resource_param is None:
        resource_param = '*'

    if condition_param is None:
        condition_param = {}

    # Collect together nodes
    if principal_param is None or principal_param == '*':
        if skip_admins:
            nodes = [x for x in graph.nodes if not x.is_admin]
        else:
            nodes = graph.nodes
    else:
        target_node = graph.get_node_by_searchable_name(principal_param)
        if skip_admins and target_node.is_admin:
            return
        else:
            nodes = [target_node]

    # go through all nodes
    for node in nodes:
        result.append(
            search_authorization_full(graph, node, action_param,
                                      resource_param, condition_param,
                                      resource_policy, resource_owner, scps,
                                      session_policy))

    for query_result in result:
        if query_result.allowed or include_unauthorized:
            query_result.print_result(action_param, resource_param)
            print()
示例#10
0
def get_graph_from_disk(location: str) -> Graph:
    """Returns a Graph object constructed from data stored on-disk at any location. This basically wraps around the
    static method in principalmapper.common.graph named Graph.create_graph_from_local_disk(...).
    """

    return Graph.create_graph_from_local_disk(location)
示例#11
0
def create_graph(session: botocore.session.Session,
                 service_list: list,
                 region_allow_list: Optional[List[str]] = None,
                 region_deny_list: Optional[List[str]] = None,
                 scps: Optional[List[List[dict]]] = None,
                 client_args_map: Optional[dict] = None) -> Graph:
    """Constructs a Graph object.

    Information about the graph as it's built will be written to the IO parameter `output`.

    The region allow/deny lists are mutually-exclusive (i.e. at least one of which has the value None) lists of
    allowed/denied regions to pull data from. Note that we don't do the same allow/deny list parameters for the
    service list, because that is a fixed property of what pmapper supports as opposed to an unknown/uncontrolled
    list of regions that AWS supports.

    The `client_args_map` is either None (default) or a dictionary containing a mapping of service -> keyword args for
    when the client is created for the service. For example, if you want to specify a different endpoint URL
    when calling IAM, your map should look like:

    ```
    client_args_map = {'iam': {'endpoint_url': 'http://localhost:4456'}}
    ```

    Later on, when calling create_client('iam', ...) the map will be added via kwargs
    """

    if client_args_map is None:
        client_args_map = {}

    stsargs = client_args_map.get('sts', {})
    stsclient = session.create_client('sts', **stsargs)
    logger.debug(stsclient.meta.endpoint_url)
    caller_identity = stsclient.get_caller_identity()
    logger.debug("Caller Identity: {}".format(caller_identity['Arn']))
    metadata = {
        'account_id': caller_identity['Account'],
        'pmapper_version': principalmapper.__version__
    }

    iamargs = client_args_map.get('iam', {})
    iamclient = session.create_client('iam', **iamargs)

    results = get_nodes_groups_and_policies(iamclient)
    nodes_result = results['nodes']
    groups_result = results['groups']
    policies_result = results['policies']

    # Determine which nodes are admins and update node objects
    update_admin_status(nodes_result, scps)

    # Generate edges, generate Edge objects
    edges_result = edge_identification.obtain_edges(session, service_list,
                                                    nodes_result,
                                                    region_allow_list,
                                                    region_deny_list, scps,
                                                    client_args_map)

    # Pull S3, SNS, SQS, KMS, and Secrets Manager resource policies
    try:
        policies_result.extend(get_s3_bucket_policies(session,
                                                      client_args_map))
        policies_result.extend(
            get_sns_topic_policies(session, region_allow_list,
                                   region_deny_list, client_args_map))
        policies_result.extend(
            get_sqs_queue_policies(session, caller_identity['Account'],
                                   region_allow_list, region_deny_list,
                                   client_args_map))
        policies_result.extend(
            get_kms_key_policies(session, region_allow_list, region_deny_list,
                                 client_args_map))
        policies_result.extend(
            get_secrets_manager_policies(session, region_allow_list,
                                         region_deny_list, client_args_map))
    except:
        pass

    return Graph(nodes_result, edges_result, policies_result, groups_result,
                 metadata)
def main():
    """Body of the script."""

    # handle arguments
    parser = argparse.ArgumentParser()
    parser.add_argument('--account',
                        default='000000000000',
                        help='The account ID to assign the simulated Graph')

    file_arg_group = parser.add_mutually_exclusive_group(required=True)
    file_arg_group.add_argument(
        '--json', help='The CloudFormation JSON template file to read from')
    file_arg_group.add_argument(
        '--yaml', help='The CloudFormation YAML template file to read from')
    parsed_args = parser.parse_args()

    # Parse file
    if parsed_args.json:
        print('[+] Loading file {}'.format(parsed_args.json))
        fd = open(parsed_args.json)
        data = json.load(fd)
    else:
        print('[+] Loading file {}'.format(parsed_args.yaml))
        fd = open(parsed_args.yaml)
        data = yaml.safe_load(fd)
    fd.close()

    # Create metadata
    metadata = {
        'account_id': parsed_args.account,
        'pmapper_version': principalmapper.__version__
    }

    print('[+] Building a Graph object for an account with ID {}'.format(
        metadata['account_id']))

    if 'Resources' not in data:
        print('[!] Missing required template element "Resources"')
        return -1

    # Create space to stash all the data we generate
    groups = []
    policies = []
    nodes = []

    # Handle data from IAM
    iam_id_counter = 0
    template_resources = data['Resources']
    # TODO: Handle policies to start
    # TODO: Handle groups
    for logical_id, contents in template_resources.items():
        # Get data on IAM Users and Roles
        if contents['Type'] == 'AWS::IAM::User':
            properties = contents['Properties']
            node_path = '/' if 'Path' not in properties else properties['Path']
            node_arn = 'arn:aws:iam::{}:user{}'.format(
                metadata['account_id'], '{}{}'.format(node_path,
                                                      properties['UserName']))
            print('[+] Adding user {}'.format(node_arn))
            nodes.append(
                Node(
                    node_arn,
                    _generate_iam_id('user', iam_id_counter),
                    [],  # TODO: add policy handling
                    [],  # TODO: add group handling
                    None,
                    None,
                    0,  # TODO: fix access keys stuff
                    False,  # TODO: fix password handling
                    False,  # TODO: implement admin checks
                    None,  # TODO: handle permission boundaries
                    False,  # TODO: handle MFA stuff in CF template reading
                    {}  # TODO: add tag handling
                ))
            iam_id_counter += 1

        elif contents['Type'] == 'AWS::IAM::Role':
            properties = contents['Properties']
            # TODO: finish out roles

    # TODO: update access keys for users

    # Sort out administrative principals
    gathering.update_admin_status(nodes)

    # Create Edges
    edges = iam_edges.generate_edges_locally(
        nodes) + sts_edges.generate_edges_locally(nodes)

    # Create our graph and finish
    graph = Graph(nodes, edges, policies, groups, metadata)
    graph_actions.print_graph_data(graph)
示例#13
0
def build_empty_graph() -> Graph:
    """Constructs and returns a Graph object with no nodes, edges, policies, or groups"""
    return Graph([], [], [], [], _get_default_metadata())
示例#14
0
def argquery(graph: Graph,
             principal_param: Optional[str],
             action_param: Optional[str],
             resource_param: Optional[str],
             condition_param: Optional[dict],
             preset_param: Optional[str],
             skip_admins: bool = False,
             resource_policy: dict = None,
             resource_owner: str = None,
             include_unauthorized: bool = False,
             session_policy: Optional[dict] = None,
             scps: Optional[List[List[dict]]] = None) -> None:
    """Splits between running a normal argquery and the presets."""
    if preset_param is not None:
        if preset_param == 'privesc':
            # Validate params
            if action_param is not None:
                raise ValueError(
                    'For the privesc preset query, the --action parameter should not be set.'
                )
            if resource_param is not None and resource_param != '*':
                raise ValueError(
                    'For the privesc preset query, the --resource parameter should not be set or set to \'*\'.'
                )

            nodes = []
            if principal_param is None or principal_param == '*':
                nodes.extend(graph.nodes)
            else:
                nodes.append(
                    graph.get_node_by_searchable_name(principal_param))

            privesc.print_privesc_results(graph, nodes, skip_admins)
        elif preset_param == 'connected':
            # Validate params
            if action_param is not None:
                raise ValueError(
                    'For the privesc preset query, the --action parameter should not be set.'
                )

            source_nodes = []
            dest_nodes = []
            if principal_param is None or principal_param == '*':
                source_nodes.extend(graph.nodes)
            else:
                source_nodes.append(
                    graph.get_node_by_searchable_name(principal_param))

            if resource_param is None or resource_param == '*':
                dest_nodes.extend(graph.nodes)
            else:
                dest_nodes.append(
                    graph.get_node_by_searchable_name(resource_param))

            connected.write_connected_results(graph, source_nodes, dest_nodes,
                                              skip_admins)
        elif preset_param == 'clusters':
            # validate params
            if action_param is not None:
                raise ValueError(
                    'For the clusters preset query, the --action parameter should not be set.'
                )

            if resource_param is None:
                raise ValueError(
                    'For the clusters preset query, the --resource parameter must be set.'
                )

            clusters.handle_preset_query(graph, ['', '', resource_param],
                                         skip_admins)
        elif preset_param == 'endgame':
            # validate params
            if action_param is not None:
                raise ValueError(
                    'For the clusters preset query, the --action parameter should not be set.'
                )

            if resource_param is None:
                raise ValueError(
                    'For the endgame preset query, the --resource parameter must be set.'
                )

            endgame.handle_preset_query(graph, ['', '', resource_param],
                                        skip_admins)
        elif preset_param == 'serviceaccess':
            serviceaccess.handle_preset_query(graph, [], skip_admins)
        elif preset_param == 'wrongadmin':
            wrongadmin.handle_preset_query(graph, [], skip_admins)
        else:
            raise ValueError(
                'Parameter for "preset" is not valid. Expected values: "privesc", "connected", '
                '"clusters", "endgame", "serviceaccess", or "wrongadmin".')

    else:
        argquery_response(graph, principal_param, action_param, resource_param,
                          condition_param, skip_admins, resource_policy,
                          resource_owner, include_unauthorized, session_policy,
                          scps)
示例#15
0
def process_arguments(parsed_args: Namespace):
    """Given a namespace object generated from parsing args, perform the appropriate tasks. Returns an int
    matching expectations set by /usr/include/sysexits.h for command-line utilities."""

    # new args for handling AWS Organizations
    if parsed_args.picked_orgs_cmd == 'create':
        logger.debug('Called create subcommand for organizations')

        # filter the args first
        if parsed_args.account is not None:
            print(
                'Cannot specify offline-mode param `--account` when calling `pmapper graph org_create`. If you have '
                'credentials for a specific account to graph, you can use those credentials similar to how the '
                'AWS CLI works (environment variables, profiles, EC2 instance metadata). In the case of using '
                'a profile, use the `--profile [PROFILE]` argument before specifying the `graph` subcommand.'
            )
            return 64

        # get the botocore session and go to work creating the OrganizationTree obj
        session = botocore_tools.get_session(parsed_args.profile)
        org_tree = get_organizations_data(session)
        logger.info('Generated initial organization data for {}'.format(
            org_tree.org_id))

        # create the account -> OU path map and apply to all accounts (same as org_update operation)
        account_ou_map = _map_account_ou_paths(org_tree)
        logger.debug('account_ou_map: {}'.format(account_ou_map))
        _update_accounts_with_ou_path_map(org_tree.org_id, account_ou_map,
                                          get_storage_root())
        logger.info(
            'Updated currently stored Graphs with applicable AWS Organizations data'
        )

        # create and cache a list of edges between all the accounts we have data for
        edge_list = []
        graph_objs = []
        for account in org_tree.accounts:
            try:
                potential_path = os.path.join(get_storage_root(), account)
                logger.debug(
                    'Trying to load a Graph from {}'.format(potential_path))
                graph_obj = Graph.create_graph_from_local_disk(potential_path)
                graph_objs.append(graph_obj)
            except Exception as ex:
                logger.warning(
                    'Unable to load a Graph object for account {}, possibly because it is not mapped yet. '
                    'Please map all accounts and then update the Organization Tree '
                    '(`pmapper graph org_update`).'.format(account))
                logger.debug(str(ex))

        for graph_obj_a in graph_objs:
            for graph_obj_b in graph_objs:
                if graph_obj_a == graph_obj_b:
                    continue
                graph_a_scps = produce_scp_list(graph_obj_a, org_tree)
                graph_b_scps = produce_scp_list(graph_obj_b, org_tree)
                edge_list.extend(
                    get_edges_between_graphs(graph_obj_a, graph_obj_b,
                                             graph_a_scps, graph_b_scps))

        org_tree.edge_list = edge_list
        logger.info('Compiled cross-account edges')

        org_tree.save_organization_to_disk(
            os.path.join(get_storage_root(), org_tree.org_id))
        logger.info('Stored organization data to disk')

    elif parsed_args.picked_orgs_cmd == 'update':
        # pull the existing data from disk
        org_filepath = os.path.join(get_storage_root(), parsed_args.org)
        org_tree = OrganizationTree.create_from_dir(org_filepath)

        # create the account -> OU path map and apply to all accounts
        account_ou_map = _map_account_ou_paths(org_tree)
        logger.debug('account_ou_map: {}'.format(account_ou_map))
        _update_accounts_with_ou_path_map(org_tree.org_id, account_ou_map,
                                          get_storage_root())
        logger.info(
            'Updated currently stored Graphs with applicable AWS Organizations data'
        )

        # create and cache a list of edges between all the accounts we have data for
        edge_list = []
        graph_objs = []
        for account in org_tree.accounts:
            try:
                potential_path = os.path.join(get_storage_root(), account)
                logger.debug(
                    'Trying to load a Graph from {}'.format(potential_path))
                graph_obj = Graph.create_graph_from_local_disk(potential_path)
                graph_objs.append(graph_obj)
            except Exception as ex:
                logger.warning(
                    'Unable to load a Graph object for account {}, possibly because it is not mapped yet. '
                    'Please map all accounts and then update the Organization Tree '
                    '(`pmapper graph org_update`).'.format(account))
                logger.debug(str(ex))

        for graph_obj_a in graph_objs:
            for graph_obj_b in graph_objs:
                if graph_obj_a == graph_obj_b:
                    continue
                graph_a_scps = produce_scp_list(graph_obj_a, org_tree)
                graph_b_scps = produce_scp_list(graph_obj_b, org_tree)
                edge_list.extend(
                    get_edges_between_graphs(graph_obj_a, graph_obj_b,
                                             graph_a_scps, graph_b_scps))

        org_tree.edge_list = edge_list
        logger.info('Compiled cross-account edges')

        org_tree.save_organization_to_disk(
            os.path.join(get_storage_root(), org_tree.org_id))
        logger.info('Stored organization data to disk')

    elif parsed_args.picked_orgs_cmd == 'display':
        # pull the existing data from disk
        org_filepath = os.path.join(get_storage_root(), parsed_args.org)
        org_tree = OrganizationTree.create_from_dir(org_filepath)

        def _print_account(org_account: OrganizationAccount, indent_level: int,
                           inherited_scps: List[Policy]):
            print('{} {}:'.format(' ' * indent_level, org_account.account_id))
            print('{}  Directly Attached SCPs: {}'.format(
                ' ' * indent_level, [x.name for x in org_account.scps]))
            print('{}  Inherited SCPs:         {}'.format(
                ' ' * indent_level, [x.name for x in inherited_scps]))

        def _walk_and_print_ou(org_node: OrganizationNode, indent_level: int,
                               inherited_scps: List[Policy]):
            print('{}"{}" ({}):'.format(' ' * indent_level, org_node.ou_name,
                                        org_node.ou_id))
            print('{}  Accounts:'.format(' ' * indent_level))
            for o_account in org_node.accounts:
                _print_account(o_account, indent_level + 2, inherited_scps)
            print('{}  Directly Attached SCPs: {}'.format(
                ' ' * indent_level, [x.name for x in org_node.scps]))
            print('{}  Inherited SCPs:         {}'.format(
                ' ' * indent_level, [x.name for x in inherited_scps]))
            print('{}  Child OUs:'.format(' ' * indent_level))
            for child_node in org_node.child_nodes:
                new_inherited_scps = inherited_scps.copy()
                new_inherited_scps.extend(
                    [x for x in org_node.scps if x not in inherited_scps])
                _walk_and_print_ou(child_node, indent_level + 4,
                                   new_inherited_scps)

        print('Organization {}:'.format(org_tree.org_id))
        for root_ou in org_tree.root_ous:
            _walk_and_print_ou(root_ou, 0, [])

    elif parsed_args.picked_orgs_cmd == 'list':
        print("Organization IDs:")
        print("---")
        storage_root = Path(get_storage_root())
        account_id_pattern = re.compile(r'o-\w+')
        for direct in storage_root.iterdir():
            if account_id_pattern.search(str(direct)) is not None:
                metadata_file = direct.joinpath(Path('metadata.json'))
                with open(str(metadata_file)) as fd:
                    version = json.load(fd)['pmapper_version']
                print("{} (PMapper Version {})".format(direct.name, version))

    return 0
示例#16
0
def query_response(graph: Graph,
                   query: str,
                   skip_admins: bool = False,
                   output: io.StringIO = os.devnull,
                   debug: bool = False) -> None:
    """Interprets, executes, and outputs the results to a query."""
    result = []

    # Parse
    tokens = re.split(r'\s+', query, flags=re.UNICODE)
    if len(tokens) < 3:
        _write_query_help(output)
        return

    nodes = []

    # first form: "can X do Y with Z when A B C" (principal, action, resource, conditionA, etc.)
    if tokens[0] == 'can' and tokens[2] == 'do':  # can <X> do <Y>
        nodes.append(graph.get_node_by_searchable_name(tokens[1]))
        action = tokens[3]

        if len(tokens) > 5:  # can <X> do <Y> with <Z>
            if tokens[4] != 'with':
                _write_query_help(output)
                return
            resource = tokens[5]
        else:
            resource = '*'

        if len(tokens) > 7:  # can <X> do <Y> with <Z> when <A> and <B> and <C>
            if tokens[6] != 'when':
                _write_query_help(output)
                return

            # doing this funky stuff in case condition values can have spaces
            # we make the (bad, but good enough?) assumption that condition values don't have ' and ' in them
            condition_str = ' '.join(tokens[7:])
            condition_tokens = re.split(r'\s+and\s+',
                                        condition_str,
                                        flags=re.UNICODE)
            condition = {}
            for condition_token in condition_tokens:
                # split on equals-sign (=), assume first instance separates the key and value
                components = condition_token.split('=')
                if len(components) < 2:
                    raise ValueError(
                        'Format for condition args not matched: <key>=<value>')
                key = components[0]
                value = '='.join(components[1:])
                condition.update({key: value})
        else:
            condition = {}

    # second form: who can do X with Y when Z and A and B and C
    elif tokens[0] == 'who' and tokens[1] == 'can' and tokens[
            2] == 'do':  # who can do X
        nodes.extend(graph.nodes)
        action = tokens[3]

        if len(tokens) > 5:  # who can do X with Y
            if tokens[4] != 'with':
                _write_query_help(output)
                return
            resource = tokens[5]
        else:
            resource = '*'

        if len(tokens) > 7:  # who can do X with Y when A and B and C
            if tokens[6] != 'when':
                _write_query_help(output)
                return

            # doing this funky stuff in case condition values can have spaces
            condition_str = ' '.join(tokens[7:])
            condition_tokens = re.split(r'\s+and\s+',
                                        condition_str,
                                        flags=re.UNICODE)
            condition = {}
            for condition_token in condition_tokens:
                # split on equals-sign (=), assume first instance separates the key and value
                components = condition_token.split('=')
                if len(components) < 2:
                    raise ValueError(
                        'Format for condition args not matched: <key>=<value>')
                key = components[0]
                value = '='.join(components[1:])
                condition.update({key: value})
        else:
            condition = {}

    elif tokens[0] == 'preset':
        handle_preset(graph, query, skip_admins, output, debug)
        return

    else:
        _write_query_help(output)
        return

    # Execute
    for node in nodes:
        if not skip_admins or not node.is_admin:
            result.append(
                (search_authorization_for(graph, node, action, resource,
                                          condition, debug), action, resource))

    # Print
    for query_result, action, resource in result:
        query_result.write_result(action, resource, output)
示例#17
0
def query_response(graph: Graph,
                   query: str,
                   skip_admins: bool = False,
                   resource_policy: Optional[dict] = None,
                   resource_owner: Optional[str] = None,
                   include_unauthorized: bool = False,
                   session_policy: Optional[dict] = None,
                   scps: Optional[List[List[dict]]] = None) -> None:
    """Interprets, executes, and outputs the results to a query."""
    result = []

    # Parse
    tokens = re.split(r'\s+', query, flags=re.UNICODE)
    logger.debug('Query tokens: {}'.format(tokens))
    if len(tokens) < 2:
        _print_query_help()
        return

    nodes = []

    # first form: "can X do Y with Z when A B C" (principal, action, resource, conditionA, etc.)
    if tokens[0] == 'can' and tokens[2] == 'do':  # can <X> do <Y>
        nodes.append(graph.get_node_by_searchable_name(tokens[1]))
        action = tokens[3]

        if len(tokens) > 5:  # can <X> do <Y> with <Z>
            if tokens[4] != 'with':
                _print_query_help()
                return
            resource = tokens[5]
        else:
            resource = '*'

        if len(tokens) > 7:  # can <X> do <Y> with <Z> when <A> and <B> and <C>
            if tokens[6] != 'when':
                _print_query_help()
                return

            # doing this funky stuff in case condition values can have spaces
            # we make the (bad, but good enough?) assumption that condition values don't have ' and ' in them
            condition_str = ' '.join(tokens[7:])
            condition_tokens = re.split(r'\s+and\s+',
                                        condition_str,
                                        flags=re.UNICODE)
            condition = {}
            for condition_token in condition_tokens:
                # split on equals-sign (=), assume first instance separates the key and value
                components = condition_token.split('=')
                if len(components) < 2:
                    raise ValueError(
                        'Format for condition args not matched: <key>=<value>')
                key = components[0]
                value = '='.join(components[1:])
                condition.update({key: value})
            logger.debug('Conditions: {}'.format(condition))
        else:
            condition = {}

    # second form: who can do X with Y when Z and A and B and C
    elif tokens[0] == 'who' and tokens[1] == 'can' and tokens[
            2] == 'do':  # who can do X
        nodes.extend(graph.nodes)
        action = tokens[3]

        if len(tokens) > 5:  # who can do X with Y
            if tokens[4] != 'with':
                _print_query_help()
                return
            resource = tokens[5]
        else:
            resource = '*'

        if len(tokens) > 7:  # who can do X with Y when A and B and C
            if tokens[6] != 'when':
                _print_query_help()
                return

            # doing this funky stuff in case condition values can have spaces
            condition_str = ' '.join(tokens[7:])
            condition_tokens = re.split(r'\s+and\s+',
                                        condition_str,
                                        flags=re.UNICODE)
            condition = {}
            for condition_token in condition_tokens:
                # split on equals-sign (=), assume first instance separates the key and value
                components = condition_token.split('=')
                if len(components) < 2:
                    raise ValueError(
                        'Format for condition args not matched: <key>=<value>')
                key = components[0]
                value = '='.join(components[1:])
                condition.update({key: value})
            logger.debug('Conditions: {}'.format(condition))
        else:
            condition = {}

    elif tokens[0] == 'preset':
        handle_preset(graph, query, skip_admins)
        return

    else:
        _print_query_help()
        return

    # pull resource owner from arg or ARN
    if resource_policy is not None:
        if resource_owner is None:
            arn_owner = arns.get_account_id(resource)
            if '*' in arn_owner or '?' in arn_owner:
                raise ValueError(
                    'Resource arg in query cannot have wildcards (? and *) unless setting '
                    '--resource-owner')
            if arn_owner == '':
                raise ValueError(
                    'Param --resource-owner must be set if resource param does not include the '
                    'account ID.')

    # Execute
    for node in nodes:
        if not skip_admins or not node.is_admin:
            result.append(
                (search_authorization_full(graph, node, action, resource,
                                           condition, resource_policy,
                                           resource_owner, scps,
                                           session_policy), action, resource))

    # Print
    for query_result, action, resource in result:
        if query_result.allowed or include_unauthorized:
            query_result.print_result(action, resource)
            print()
示例#18
0
def argquery(graph: Graph,
             principal_param: Optional[str],
             action_param: Optional[str],
             resource_param: Optional[str],
             condition_param: Optional[dict],
             preset_param: Optional[str],
             skip_admins: bool = False,
             output: io.StringIO = os.devnull,
             debug: bool = False) -> None:
    """Splits between running a normal argquery and the presets."""
    if preset_param is not None:
        if preset_param == 'privesc':
            # Validate params
            if action_param is not None:
                raise ValueError(
                    'For the privesc preset query, the --action parameter should not be set.'
                )
            if resource_param is not None:
                raise ValueError(
                    'For the privesc preset query, the --resource parameter should not be set.'
                )

            nodes = []
            if principal_param is None or principal_param == '*':
                nodes.extend(graph.nodes)
            else:
                nodes.append(
                    graph.get_node_by_searchable_name(principal_param))

            privesc.write_privesc_results(graph, nodes, skip_admins, output,
                                          debug)
        elif preset_param == 'connected':
            # Validate params
            if action_param is not None:
                raise ValueError(
                    'For the privesc preset query, the --action parameter should not be set.'
                )

            source_nodes = []
            dest_nodes = []
            if principal_param is None or principal_param == '*':
                source_nodes.extend(graph.nodes)
            else:
                source_nodes.append(
                    graph.get_node_by_searchable_name(principal_param))

            if resource_param is None or resource_param == '*':
                dest_nodes.extend(graph.nodes)
            else:
                dest_nodes.append(
                    graph.get_node_by_searchable_name(resource_param))

            connected.write_connected_results(graph, source_nodes, dest_nodes,
                                              skip_admins, output, debug)
        else:
            raise ValueError(
                'Parameter for "preset" is not valid. Expected values: "privesc" and "connected".'
            )

    else:
        argquery_response(graph, principal_param, action_param, resource_param,
                          condition_param, skip_admins, output, debug)