def handle_graph(parsed_args) -> int: """Processes the arguments for the graph subcommand and executes related tasks""" session = _grab_session(parsed_args) if parsed_args.create: # --create graph = principalmapper.graphing.graph_actions.create_new_graph( session, checker_map.keys(), parsed_args.debug) principalmapper.graphing.graph_actions.print_graph_data(graph) graph.store_graph_as_json( os.path.join(get_storage_root(), graph.metadata['account_id'])) elif parsed_args.display: # --display graph = principalmapper.graphing.graph_actions.get_existing_graph( session, parsed_args.account, parsed_args.debug) principalmapper.graphing.graph_actions.print_graph_data(graph) elif parsed_args.list: # --list print("Account IDs:") print("---") storage_root = Path(get_storage_root()) for direct in storage_root.iterdir(): print(direct.name) elif parsed_args.update_edges: # --update-edges graph = principalmapper.graphing.graph_actions.get_existing_graph( session, parsed_args.account, parsed_args.debug) graph.edges = principalmapper.graphing.edge_identification.obtain_edges( session, checker_map.keys(), graph.nodes, sys.stdout, parsed_args.debug) principalmapper.graphing.graph_actions.print_graph_data(graph) graph.store_graph_as_json( os.path.join(get_storage_root(), graph.metadata['account_id'])) return 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 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
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
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
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