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 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())
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())
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)
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)
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)
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))
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)
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()
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)
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)
def build_empty_graph() -> Graph: """Constructs and returns a Graph object with no nodes, edges, policies, or groups""" return Graph([], [], [], [], _get_default_metadata())
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)
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 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)
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()
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)