Ejemplo n.º 1
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."""

    if parsed_args.account is None:
        session = botocore_tools.get_session(parsed_args.profile)
    else:
        session = None
    graph = graph_actions.get_existing_graph(session, parsed_args.account)
    logger.debug('Querying against graph {}'.format(
        graph.metadata['account_id']))

    # process condition args to generate input dict
    conditions = {}
    if parsed_args.condition is not None:
        for arg in parsed_args.condition:
            # split on equals-sign (=), assume first instance separates the key and value
            components = arg.split('=')
            if len(components) < 2:
                print('Format for condition args not matched: <key>=<value>')
                return 64
            key = components[0]
            value = '='.join(components[1:])
            conditions.update({key: value})

    if parsed_args.with_resource_policy:
        resource_policy = query_utils.pull_cached_resource_policy_by_arn(
            graph.policies, parsed_args.resource)
    elif parsed_args.resource_policy_text:
        resource_policy = json.loads(parsed_args.resource_policy_text)
    else:
        resource_policy = None

    if parsed_args.scps:
        if 'org-id' in graph.metadata and 'org-path' in graph.metadata:
            org_tree_path = os.path.join(get_storage_root(),
                                         graph.metadata['org-id'])
            org_tree = OrganizationTree.create_from_dir(org_tree_path)
            scps = query_orgs.produce_scp_list(graph, org_tree)
        else:
            raise ValueError(
                'Graph for account {} does not have an associated OrganizationTree mapped (need to run '
                '`pmapper orgs create/update` to get that.')
    else:
        scps = None

    query_actions.argquery(graph, parsed_args.principal, parsed_args.action,
                           parsed_args.resource, conditions,
                           parsed_args.preset, parsed_args.skip_admin,
                           resource_policy, parsed_args.resource_owner,
                           parsed_args.include_unauthorized,
                           parsed_args.session_policy, scps)

    return 0
Ejemplo n.º 2
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."""

    if parsed_args.account is None:
        session = botocore_tools.get_session(parsed_args.profile)
    else:
        session = None

    graph = graph_actions.get_existing_graph(session, parsed_args.account)
    logger.debug('Querying against graph {}'.format(graph.metadata['account_id']))

    if parsed_args.with_resource_policy:
        resource_policy = query_utils.pull_cached_resource_policy_by_arn(
            graph,
            arn=None,
            query=parsed_args.query
        )
    elif parsed_args.resource_policy_text:
        resource_policy = json.loads(parsed_args.resource_policy_text)
    else:
        resource_policy = None

    resource_owner = parsed_args.resource_owner
    if resource_policy is not None:
        if resource_owner is None:
            if arns.get_service(resource_policy.arn) == 's3':
                raise ValueError('Must supply resource owner (--resource-owner) when including S3 bucket policies '
                                 'in a query')
            else:
                resource_owner = arns.get_account_id(resource_policy.arn)
        if isinstance(resource_policy, Policy):
            resource_policy = resource_policy.policy_doc

    if parsed_args.scps:
        if 'org-id' in graph.metadata and 'org-path' in graph.metadata:
            org_tree_path = os.path.join(get_storage_root(), graph.metadata['org-id'])
            org_tree = OrganizationTree.create_from_dir(org_tree_path)
            scps = query_orgs.produce_scp_list(graph, org_tree)
        else:
            raise ValueError('Graph for account {} does not have an associated OrganizationTree mapped (need to run '
                             '`pmapper orgs create/update` to get that.')
    else:
        scps = None

    query_actions.query_response(
        graph, parsed_args.query, parsed_args.skip_admin, resource_policy, resource_owner,
        parsed_args.include_unauthorized, parsed_args.session_policy, scps
    )

    return 0
Ejemplo n.º 3
0
def get_organizations_data(
        session: botocore.session.Session) -> OrganizationTree:
    """Given a botocore Session object, generate an OrganizationTree object. This throws a RuntimeError if the session
    is for an Account that is not able to gather Organizations data, along with the reason why.

    The edge_list field of the OrganizationTree object is not populated. """

    # grab account data
    stsclient = session.create_client('sts')
    account_data = stsclient.get_caller_identity()

    # try to grab org data, raising RuntimeError if appropriate
    try:
        orgsclient = session.create_client('organizations')
        organization_data = orgsclient.describe_organization()
    except botocore.exceptions.ClientError as ex:
        if 'AccessDeniedException' in str(ex):
            raise RuntimeError(
                'Encountered a permission error. Either the current principal ({}) is not authorized to '
                'interact with AWS Organizations, or the current account ({}) is not the '
                'management account'.format(account_data['Arn'],
                                            account_data['Account']))
        else:
            raise ex

    # compose the OrganizationTree object
    logger.info(
        'Generating data for organization {} through management account {}'.
        format(organization_data['Organization']['Id'],
               organization_data['Organization']['MasterAccountId']))
    result = OrganizationTree(
        organization_data['Organization']['Id'],
        organization_data['Organization']['MasterAccountId'],
        None,  # fill in `root_ous` later
        None,  # get SCPs later
        None,  # get account list later
        [],  # caller is responsible for creating and setting the edge list
        {'pmapper_version': principalmapper.__version__})

    scp_list = []
    root_ous = []
    account_ids = []

    # get root IDs to start
    logger.info('Going through roots of organization')
    root_ids_and_names = []
    list_roots_paginator = orgsclient.get_paginator('list_roots')
    for page in list_roots_paginator.paginate():
        root_ids_and_names.extend([(x['Id'], x['Name'])
                                   for x in page['Roots']])

    def _get_scps_for_target(target_id: str) -> List[Policy]:
        """This method takes an ID for a target (root, OU, or account), then composes and returns a list of Policy
        objects for that target."""
        scps_result = []
        policy_name_arn_list = []
        list_policies_paginator = orgsclient.get_paginator(
            'list_policies_for_target')
        for lpp_page in list_policies_paginator.paginate(
                TargetId=target_id, Filter='SERVICE_CONTROL_POLICY'):
            for policy in lpp_page['Policies']:
                policy_name_arn_list.append((policy['Name'], policy['Arn']))

        for name_arn_pair in policy_name_arn_list:
            policy_name, policy_arn = name_arn_pair
            desc_policy_resp = orgsclient.describe_policy(
                PolicyId=policy_arn.split('/')[-1])
            scps_result.append(
                Policy(policy_arn, policy_name,
                       json.loads(desc_policy_resp['Policy']['Content'])))

        logger.debug('SCPs of {}: {}'.format(target_id,
                                             [x.arn for x in scps_result]))

        scp_list.extend(scps_result)
        return scps_result

    def _get_tags_for_target(target_id: str) -> dict:
        """This method takes an ID for a target (root/OU/account) then composes and returns a dictionary for the
        tags for that target"""
        target_tags = {}
        list_tags_paginator = orgsclient.get_paginator(
            'list_tags_for_resource')
        for ltp_page in list_tags_paginator.paginate(ResourceId=target_id):
            for tag in ltp_page['Tags']:
                target_tags[tag['Key']] = tag['Value']

        logger.debug('Tags for {}: {}'.format(target_id, target_tags))
        return target_tags

    # for each root, recursively grab child OUs while filling out OrganizationNode/OrganizationAccount objects
    # need to get tags, SCPs too
    def _compose_ou(parent_id: str, parent_name: str) -> OrganizationNode:
        """This method takes an OU's ID and Name to compose and return an OrganizationNode object for that OU. This
        grabs the accounts in the OU, tags for the OU, SCPs for the OU, and then gets the child OUs to recursively
        compose those OrganizationNode objects."""

        logger.info('Composing data for "{}" ({})'.format(
            parent_name, parent_id))

        # Get tags for the OU
        ou_tags = _get_tags_for_target(parent_id)

        # Get SCPs for the OU
        ou_scps = _get_scps_for_target(parent_id)

        # Get accounts under the OU
        org_account_objs = []  # type: List[OrganizationAccount]
        list_accounts_paginator = orgsclient.get_paginator(
            'list_accounts_for_parent')
        ou_child_account_list = []
        for lap_page in list_accounts_paginator.paginate(ParentId=parent_id):
            for child_account_data in lap_page['Accounts']:
                ou_child_account_list.append(child_account_data['Id'])
        logger.debug('Accounts: {}'.format(ou_child_account_list))

        account_ids.extend(ou_child_account_list)
        for ou_child_account_id in ou_child_account_list:
            child_account_tags = _get_tags_for_target(ou_child_account_id)
            child_account_scps = _get_scps_for_target(ou_child_account_id)
            org_account_objs.append(
                OrganizationAccount(ou_child_account_id, child_account_scps,
                                    child_account_tags))

        # get child OUs (pairs of Ids and Names)
        child_ou_ids = []
        list_children_paginator = orgsclient.get_paginator('list_children')
        for lcp_page in list_children_paginator.paginate(
                ParentId=parent_id, ChildType='ORGANIZATIONAL_UNIT'):
            for child in lcp_page['Children']:
                child_ou_ids.append(child['Id'])

        child_ous = []  # type: List[OrganizationNode]
        for child_ou_id in child_ou_ids:
            desc_ou_resp = orgsclient.describe_organizational_unit(
                OrganizationalUnitId=child_ou_id)
            child_ous.append(
                _compose_ou(child_ou_id,
                            desc_ou_resp['OrganizationalUnit']['Name']))

        return OrganizationNode(parent_id, parent_name, org_account_objs,
                                child_ous, ou_scps, ou_tags)

    for root_id_and_name in root_ids_and_names:
        root_ou_id, root_ou_name = root_id_and_name
        root_ous.append(_compose_ou(root_ou_id, root_ou_name))

    # apply root OUs to result
    result.root_ous = root_ous

    # apply collected SCPs to result
    filtered_scp_list = []
    filtered_arns = []
    for scp in scp_list:
        if scp.arn in filtered_arns:
            continue
        filtered_scp_list.append(scp)
        filtered_arns.append(scp.arn)

    result.all_scps = filtered_scp_list

    # apply collected account IDs to result
    result.accounts = account_ids

    return result
Ejemplo n.º 4
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
Ejemplo n.º 5
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."""

    if parsed_args.picked_graph_cmd == 'create':
        logger.debug('Called create subcommand of graph')

        # filter the args first
        if parsed_args.account is not None:
            print(
                'Cannot specify offline-mode param `--account` when calling `pmapper graph 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

        service_list_base = list(checker_map.keys())
        if parsed_args.include_services is not None:
            service_list = [
                x for x in service_list_base
                if x in parsed_args.include_services
            ]
        elif parsed_args.exclude_services is not None:
            service_list = [
                x for x in service_list_base
                if x not in parsed_args.exclude_services
            ]
        else:
            service_list = service_list_base
        logger.debug(
            'Service list after processing args: {}'.format(service_list))

        # need to know account ID to search potential SCPs
        if parsed_args.localstack_endpoint is not None:
            session = botocore_tools.get_session(
                parsed_args.profile,
                {'endpoint_url': parsed_args.localstack_endpoint})
        else:
            session = botocore_tools.get_session(parsed_args.profile)

        scps = None
        if not parsed_args.ignore_orgs:
            if parsed_args.localstack_endpoint is not None:
                stsclient = session.create_client(
                    'sts', endpoint_url=parsed_args.localstack_endpoint)
            else:
                stsclient = session.create_client('sts')
            caller_identity = stsclient.get_caller_identity()
            caller_account = caller_identity['Account']
            logger.debug("Caller Identity: {}".format(caller_identity))

            org_tree_search_dir = Path(get_storage_root())
            org_id_pattern = re.compile(r'/o-\w+')
            for subdir in org_tree_search_dir.iterdir():
                if org_id_pattern.search(str(subdir)) is not None:
                    logger.debug(
                        'Checking {} to see if account {} is a member'.format(
                            str(subdir), caller_account))
                    org_tree = OrganizationTree.create_from_dir(str(subdir))
                    if caller_account in org_tree.accounts:
                        logger.info(
                            'Account {} is a member of Organization {}'.format(
                                caller_account, org_tree.org_id))
                        if caller_account == org_tree.management_account_id:
                            logger.info(
                                'Account {} is the management account, SCPs do not apply'
                                .format(caller_account))
                        else:
                            logger.info(
                                'Identifying and applying SCPs for the graphing process'
                            )
                            scps = query_orgs.produce_scp_list_by_account_id(
                                caller_account, org_tree)
                        break

        if parsed_args.localstack_endpoint is not None:
            full_service_list = ('autoscaling', 'cloudformation', 'codebuild',
                                 'ec2', 'iam', 'kms', 'lambda', 'sagemaker',
                                 's3', 'ssm', 'secretsmanager', 'sns', 'sts',
                                 'sqs')

            client_args_map = {
                x: {
                    'endpoint_url': parsed_args.localstack_endpoint
                }
                for x in full_service_list
            }
        else:
            client_args_map = None

        graph = graph_actions.create_new_graph(session, service_list,
                                               parsed_args.include_regions,
                                               parsed_args.exclude_regions,
                                               scps, client_args_map)
        graph_actions.print_graph_data(graph)
        graph.store_graph_as_json(
            os.path.join(get_storage_root(), graph.metadata['account_id']))

    elif parsed_args.picked_graph_cmd == 'display':
        if parsed_args.account is None:
            session = botocore_tools.get_session(parsed_args.profile)
        else:
            session = None

        graph = graph_actions.get_existing_graph(session, parsed_args.account)
        graph_actions.print_graph_data(graph)

    elif parsed_args.picked_graph_cmd == 'list':
        print("Account IDs:")
        print("---")
        storage_root = Path(get_storage_root())
        account_id_pattern = re.compile(r'\d{12}')
        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:
                    account_metadata = json.load(fd)
                version = account_metadata['pmapper_version']
                print("{} (PMapper Version {})".format(direct.name, version))

    return 0