def get_interaccount_search_list(all_graphs: List[Graph], inter_account_edges: List[Edge], node: Node) -> List[List[Edge]]: """Returns a list of edge lists. Each edge list represents a path to a new unique node that's accessible from the initial node (passed as a param). This is a breadth-first search, and the returned list of lists of edges will represent the different available paths. """ result = [] nodes_found = [node] nodes_explored = [] account_id_graph_map = {} for graph in all_graphs: account_id_graph_map[graph.metadata['account_id']] = graph # Get initial list of edges first_set = get_edges_interaccount(account_id_graph_map[arns.get_account_id(node.arn)], inter_account_edges, node, nodes_found) for found_edge in first_set: nodes_found.append(found_edge.destination) result.append([found_edge]) nodes_explored.append(node) # dig through result list index = 0 while index < len(result): current_node = result[index][-1].destination if current_node not in nodes_explored: for edge in get_edges_interaccount(account_id_graph_map[arns.get_account_id(current_node.arn)], inter_account_edges, current_node, nodes_found): result.append(result[index][:] + [edge]) if edge.destination not in nodes_found: nodes_found.append(edge.destination) nodes_explored.append(current_node) index += 1 return result
def _principal_matches_in_statement(principal: Node, aws_principal_field: list): """Helper function for locally determining a principal matches a resource policy's statement""" for value in aws_principal_field: if principal.arn == value: return True elif principal.id_value == value: return True elif arns.get_account_id(principal.arn) == value: return True else: principal_root_str = 'arn:{}:iam::{}:root'.format(arns.get_partition(principal.arn), arns.get_account_id(principal.arn)) if principal_root_str == value: return True return False
def gen_overprivileged_function_findings(graph: Graph) -> List[Finding]: """Generates findings related to risk from Lambda functions being loaded with overprivileged roles""" result = [] affected_roles = [] for node in graph.nodes: if ':role/' in node.arn and node.is_admin: if query_interface.resource_policy_authorization('lambda.amazonaws.com', arns.get_account_id(node.arn), node.trust_policy, 'sts:AssumeRole', node.arn, {})\ == query_interface.ResourcePolicyEvalResult.SERVICE_MATCH: affected_roles.append(node) if len(affected_roles) > 0: description_preamble = 'In AWS, Lambda functions can be assigned an IAM Role to use during execution. These ' \ 'IAM Roles give the function access to call the AWS API with the permissions of the ' \ 'IAM Role, depending on the policies attached to it. If the Lambda function can be ' \ 'compromised, and the attacker can alter the code it executes, the attacker could ' \ 'make AWS API calls with the IAM Role\'s permissions. The following IAM Roles have ' \ 'administrative privileges, and can be passed to Lambda functions:\n\n' description_body = '' for node in affected_roles: description_body += '* {}\n'.format(node.searchable_name()) result.append(Finding( 'IAM Roles Available to Lambda Functions Have Administrative Privileges' if len(affected_roles) > 1 else 'IAM Role Available to Lambda Functions Has Administrative Privileges', 'Medium', 'If an attacker can inject code or commands into the function, or if a lower-privileged principal can ' 'alter the function, the AWS account as a whole could be compromised.', description_preamble + description_body, 'Reduce the scope of permissions attached to the noted IAM Role(s).' )) return result
def search_authorization_across_accounts(graph_scp_pairs: List[Tuple[Graph, Optional[List[List[Policy]]]]], inter_account_edges: List[Edge], principal: Node, action_to_check: str, resource_to_check: str, condition_keys_to_check: _UODict, resource_policy: Optional[dict] = None, resource_owner: Optional[str] = None, session_policy: Optional[dict] = None) -> QueryResult: """Determines if the passed principal, or any principals it can access, can perform a given action for a given resource/condition. Handles an optional resource policy, an optional SCP list, and an optional session policy. The session policy is discarded after checking if the passed principal has access (we assume it is discarded after each pivot, and that it does NOT affect the accessibility of the edges). In `local_check_authorization` we usually throw up our hands if the given principal is an admin. But, because of how SCPs work (even blocking the root user), we force the full search to give more accurate results. If the SCPs param is None, we assume no SCPs are in place and can make the same assumption as in `local_check_authorization`. If the resource_owner param is not None, and the resource_owner param is None, the `local_check_authorization_full` function that gets called will throw a ValueError, so make sure the resource ownership is sorted before calling this method. The graphs to include in the search have to be passed in tuples. The second element of the tuple is either the SCPs that affect that graph or None. If your graph belongs to an organization, remember that you can take the OrganizationTree object and produce the applicable SCPs by calling principalmapper.querying.query_orgs.produce_scp_list and passing the graph + org-tree objects.""" account_id_graph_scp_pair_map = {} for graph_scp_pair in graph_scp_pairs: account_id_graph_scp_pair_map[graph_scp_pair[0].metadata['account_id']] = graph_scp_pair source_graph_scp_pair = account_id_graph_scp_pair_map[arns.get_account_id(principal.arn)] if local_check_authorization_full(principal, action_to_check, resource_to_check, condition_keys_to_check, resource_policy, resource_owner, source_graph_scp_pair[1], session_policy): return QueryResult(True, [], principal) # now we have to check cross-account scenario for admin short-circuit if source_graph_scp_pair[1] is None and principal.is_admin and resource_owner == arns.get_account_id(principal.arn): return QueryResult(True, principal, principal) for edge_list in query_utils.get_interaccount_search_list([x[0] for x in graph_scp_pairs], inter_account_edges, principal): proxy_principal = edge_list[-1].destination proxy_principal_scps = account_id_graph_scp_pair_map[arns.get_account_id(proxy_principal.arn)][1] if local_check_authorization_full(edge_list[-1].destination, action_to_check, resource_to_check, condition_keys_to_check, resource_policy, resource_owner, proxy_principal_scps, None): return QueryResult(True, edge_list, principal) return QueryResult(False, [], principal)
def _check_assume_role(ga, na, gb, nb, scps) -> bool: logger.debug('Checking if {} can access {}'.format(na.arn, nb.arn)) # load up conditions: inspired by _infer_condition_keys conditions = {} conditions['aws:CurrentTime'] = dt.datetime.now( dt.timezone.utc).isoformat() conditions['aws:EpochTime'] = str( round(dt.datetime.now(dt.timezone.utc).timestamp())) conditions['aws:userid'] = na.id_value if ':user/' in na.arn: conditions['aws:username'] = na.searchable_name().split('/')[1] conditions['aws:SecureTransport'] = 'true' conditions['aws:PrincipalAccount'] = ga.metadata['account_id'] conditions['aws:PrincipalArn'] = na.arn if 'org-id' in ga.metadata: conditions['aws:PrincipalOrgID'] = ga.metadata['org-id'] if 'org-path' in ga.metadata: conditions['aws:PrincipalOrgPaths'] = ga.metadata['org-path'] for tag_key, tag_value in na.tags.items(): conditions['aws:PrincipalTag/{}'.format(tag_key)] = tag_value # check without MFA auth_result = local_check_authorization_full( na, 'sts:AssumeRole', nb.arn, conditions, nb.trust_policy, arns.get_account_id(nb.arn), scps) if auth_result: return True # check with MFA conditions.update({ 'aws:MultiFactorAuthAge': '1', 'aws:MultiFactorAuthPresent': 'true' }) auth_result = local_check_authorization_full( na, 'sts:AssumeRole', nb.arn, conditions, nb.trust_policy, arns.get_account_id(nb.arn), scps) return auth_result
def pull_resource_policy_by_arn(session: botocore.session.Session, arn: Optional[str], query: str = None) -> dict: """helper function for pulling the resource policy for a resource at the denoted ARN. raises ValueError if it cannot be retrieved, or a botocore ClientError if another issue arises """ 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.') service = arns.get_service(arn) if service == 'iam': # arn:aws:iam::<account_id>:role/<role_name> client = session.create_client('iam') role_name = arns.get_resource(arn).split('/')[-1] logger.debug('Calling IAM API to retrieve AssumeRolePolicyDocument of {}'.format(role_name)) trust_doc = client.get_role(RoleName=role_name)['Role']['AssumeRolePolicyDocument'] return trust_doc elif service == 's3': # arn:aws:s3:::<bucket>/<path_to_object_with_potential_colons> client = session.create_client('s3') bucket_name = arns.get_resource(arn).split('arn:aws:s3:::')[-1].split('/')[0] logger.debug('Calling S3 API to retrieve bucket policy of {}'.format(bucket_name)) bucket_policy = json.loads(client.get_bucket_policy(Bucket=bucket_name)['Policy']) return bucket_policy elif service == 'sns': region = arns.get_region(arn) client = session.create_client('sns', region_name=region) logger.debug('Calling SNS API to retrieve topic policy of {}'.format(arn)) policy_str = client.get_topic_attributes(TopicArn=arn)['Attributes']['Policy'] return json.loads(policy_str) elif service == 'sqs': region = arns.get_region(arn) client = session.create_client('sqs', region_name=region) logger.debug('Calling SQS API to retrieve queue policy of {}'.format(arn)) queue_url = 'https://sqs.{}.amazonaws.com/{}/{}'.format( arns.get_region(arn), arns.get_account_id(arn), arns.get_resource(arn) ) # TODO: future proof queue URL creation? this still work with FIFO queues? policy_str = client.get_queue_attributes(QueueUrl=queue_url, AttributeNames=['Policy'])['Policy'] return json.loads(policy_str) elif service == 'kms': region = arns.get_region(arn) client = session.create_client('kms', region_name=region) logger.debug('Calling KMS API to retrieve key policy of {}'.format(arn)) key_policy = json.loads(client.get_key_policy(KeyId=arn, PolicyName='default')['Policy']) return key_policy
def resource_policy_authorization( node_or_service: Union[Node, str], resource_owner: str, resource_policy: dict, action_to_check: str, resource_to_check: str, condition_keys_to_check: Union[dict, CaseInsensitiveDict] ) -> ResourcePolicyEvalResult: """Returns a ResourcePolicyEvalResult for a given request, based on the resource policy.""" if isinstance(condition_keys_to_check, dict): prepped_condition_keys = CaseInsensitiveDict(condition_keys_to_check) else: prepped_condition_keys = condition_keys_to_check matching_statements = resource_policy_matching_statements( node_or_service, resource_policy, action_to_check, resource_to_check, prepped_condition_keys) if len(matching_statements) == 0: return ResourcePolicyEvalResult.NO_MATCH # handle denies outright for statement in matching_statements: if statement['Effect'] == 'Deny': return ResourcePolicyEvalResult.DENY_MATCH # handle nodes (IAM Users or Roles) if isinstance(node_or_service, Node): # if in a different account, check for denies and wrap it up if arns.get_account_id(node_or_service.arn) != resource_owner: return ResourcePolicyEvalResult.DIFF_ACCOUNT_MATCH else: node_match = False for statement in matching_statements: if 'NotPrincipal' in statement: # NotPrincipal means a node match (tested with S3) node_match = True elif isinstance(statement['Principal'], str) and statement['Principal'] == '*': # Case: "Principal": "*" node_match = True else: # dig through 'AWS' element of Principal for node-matching if 'AWS' in statement['Principal']: for aws_principal in _listify_string( statement['Principal']['AWS']): if node_or_service.arn == aws_principal: node_match = True if node_match: return ResourcePolicyEvalResult.NODE_MATCH else: return ResourcePolicyEvalResult.ROOT_MATCH else: return ResourcePolicyEvalResult.SERVICE_MATCH
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 resource_policy_authorization(node_or_service: Union[Node, str], resource_owner: str, resource_policy: dict, action_to_check: str, resource_to_check: str, condition_keys_to_check: dict, debug: bool) -> ResourcePolicyEvalResult: """Returns a ResourcePolicyEvalResult for a given request, based on the resource policy.""" dprint(debug, "Local resource policy authorization check: Principal {}, Action {}, Resource {}, Condition Keys {}, " "Resource Owner {}".format(node_or_service, action_to_check, resource_to_check, condition_keys_to_check, resource_owner)) matching_statements = resource_policy_matching_statements(node_or_service, resource_policy, action_to_check, resource_to_check, condition_keys_to_check, debug) if len(matching_statements) == 0: return ResourcePolicyEvalResult.NO_MATCH # handle nodes (IAM Users or Roles) if isinstance(node_or_service, Node): # if in a different account, check for denies and wrap it up if arns.get_account_id(node_or_service.arn) != resource_owner: for statement in matching_statements: if statement['Effect'] == 'Deny': return ResourcePolicyEvalResult.DENY_MATCH return ResourcePolicyEvalResult.DIFF_ACCOUNT_MATCH else: # messy part: find denies, then determine if we send back ROOT or NODE match for statement in matching_statements: if statement['Effect'] == 'Deny': return ResourcePolicyEvalResult.DENY_MATCH node_match = False for statement in matching_statements: if 'NotPrincipal' in statement: # NotPrincipal means a node match (tested with S3) node_match = True else: for principal in _listify_string(statement['Principal']): if node_or_service.arn == principal: node_match = True if node_or_service.id_value == principal: node_match = True # 'AIDA.*' and co. can match here if node_match: return ResourcePolicyEvalResult.NODE_MATCH else: return ResourcePolicyEvalResult.ROOT_MATCH else: return ResourcePolicyEvalResult.SERVICE_MATCH
def _infer_condition_keys(principal: Node, current_keys: CaseInsensitiveDict) -> CaseInsensitiveDict: """Returns a dictionary with global condition context keys we can infer are set based on the input Node being checked. We exclude setting keys that are already set in current_keys. Using information from https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html Changes in v1.1.3: * Changed param current_keys type to CaseInsensitiveDict * Changed return type to CaseInsensitiveDict """ result = CaseInsensitiveDict() # Date and Time: aws:CurrentTime and aws:EpochTime # TODO: Examine if using datetime.isoformat() is good enough to avoid bugs if 'aws:CurrentTime' not in current_keys: result['aws:CurrentTime'] = dt.datetime.now(dt.timezone.utc).isoformat() if 'aws:EpochTime' not in current_keys: result['aws:EpochTime'] = str(round(dt.datetime.now(dt.timezone.utc).timestamp())) # UserID and Username: aws:userid and aws:username # TODO: Double-check how roles handle aws:username, IIRC it's not filled in if 'aws:userid' not in current_keys: result['aws:userid'] = principal.id_value if ':user/' in principal.arn and 'aws:username' not in current_keys: result['aws:username'] = principal.searchable_name().split('/')[1] # assumes API requests are made via secure channel (HTTPS) if 'aws:SecureTransport' not in current_keys: result['aws:SecureTransport'] = 'true' if 'aws:PrincipalAccount' not in current_keys: result['aws:PrincipalAccount'] = arns.get_account_id(principal.arn) if 'aws:PrincipalArn' not in current_keys: result['aws:PrincipalArn'] = principal.arn # NOTE: tag keys are checked for case-insensitive equality already, no worries about collisions for tag_key, tag_value in principal.tags.items(): if 'aws:PrincipalTag/{}'.format(tag_key) not in current_keys: result['aws:PrincipalTag/{}'.format(tag_key)] = tag_value return result
def _infer_condition_keys(principal: Node, current_keys: dict) -> dict: """Returns a dictionary with global condition context keys we can infer are set based on the input Node being checked. We exclude setting keys that are already set in current_keys. Using information from https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html """ result = {} # Date and Time: aws:CurrentTime and aws:EpochTime # TODO: Examine if using datetime.isoformat() is good enough to avoid bugs if 'aws:CurrentTime' not in current_keys: result['aws:CurrentTime'] = dt.datetime.now( dt.timezone.utc).isoformat() if 'aws:EpochTime' not in current_keys: result['aws:EpochTime'] = str( round(dt.datetime.now(dt.timezone.utc).timestamp())) # UserID and Username: aws:userid and aws:username # TODO: Double-check how roles handle aws:username, IIRC it's not filled in if 'aws:userid' not in current_keys: result['aws:userid'] = principal.id_value if ':user/' in principal.arn and 'aws:username' not in current_keys: result['aws:username'] = principal.searchable_name().split('/')[1] if 'aws:SecureTransport' not in current_keys: result['aws:SecureTransport'] = 'true' if 'aws:PrincipalAccount' not in current_keys: result['aws:PrincipalAccount'] = arns.get_account_id(principal.arn) if 'aws:PrincipalArn' not in current_keys: result['aws:PrincipalArn'] = principal.arn for tag_key, tag_value in principal.tags.items(): if 'aws:PrincipalTag/{}'.format(tag_key) not in current_keys: result['aws:PrincipalTag/{}'.format(tag_key)] = tag_value return result
def gen_overprivileged_stack_findings(graph: Graph) -> List[Finding]: """Generates findings related to risk from CloudFormation stacks being loaded with overprivileged roles""" result = [] affected_roles = [] for node in graph.nodes: if ':role/' in node.arn and node.is_admin: if query_interface.resource_policy_authorization('cloudformation.amazonaws.com', arns.get_account_id(node.arn), node.trust_policy, 'sts:AssumeRole', node.arn, {}) == \ query_interface.ResourcePolicyEvalResult.SERVICE_MATCH: affected_roles.append(node) if len(affected_roles) > 0: description_preamble = 'In AWS, CloudFormation stacks can be given an IAM Role. When a stack has an IAM ' \ 'Role, it can use that IAM Role to make AWS API calls to create the resources ' \ 'defined in the template for that stack. If the IAM Role has administrator access ' \ 'to the account, and an attacker is able to make the right CloudFormation API calls, ' \ 'they would be able to use the IAM Role to escalate privileges and compromise the ' \ 'account as a whole. The following IAM Roles can be used in CloudFormation and ' \ 'have administrative privileges:\n\n' description_body = '' for node in affected_roles: description_body += '* {}\n'.format(node.searchable_name()) result.append( Finding( 'IAM Roles Available to CloudFormation Stacks Have Administrative Privileges' if len(affected_roles) > 1 else 'IAM Role Available to CloudFormation Stacks Has Administrative Privileges', 'Low', 'If an attacker has the right permissions in the AWS Account, they can grant themselves adminstrative ' 'access to the account to compromise the account.', description_preamble + description_body, 'Reduce the scope of permissions attached to the noted IAM Role(s).' )) return result
def generate_edges_locally( nodes: List[Node], scps: Optional[List[List[dict]]] = None, codebuild_projects: Optional[List[dict]] = None) -> List[Edge]: """Generates and returns Edge objects related to AWS CodeBuild. It is possible to use this method if you are operating offline (infra-as-code). The `codebuild_projects` param should be a list of dictionary objects with the following expected structure: ``` { 'project_arn': <str: ARN of a project>, 'project_role': <str: ARN of a role attached to a project> 'project_tags': <list[dict]: tags for the project as in [{'Key': <Key>, 'Value': <Value>}]> } ``` All elements are required, tags must point to an empty list if there are no tags attached to the project """ result = [] # we wanna create a role -> [{proj_arn: <>, proj_tags: <>}] map to make eventual lookups faster if codebuild_projects is None: codebuild_map = {} else: codebuild_map = {} # type: Dict[str, List[dict]] for project in codebuild_projects: if project['project_role'] not in codebuild_map: codebuild_map[project['project_role']] = [{ 'proj_arn': project['project_arn'], 'proj_tags': project['project_tags'] }] else: codebuild_map[project['project_role']].append({ 'proj_arn': project['project_arn'], 'proj_tags': project['project_tags'] }) for node_destination in nodes: # check if destination is a user, skip if so if ':role/' not in node_destination.arn: continue # check that the destination role can be assumed by CodeBuild sim_result = resource_policy_authorization( 'codebuild.amazonaws.com', arns.get_account_id(node_destination.arn), node_destination.trust_policy, 'sts:AssumeRole', node_destination.arn, {}, ) if sim_result != ResourcePolicyEvalResult.SERVICE_MATCH: continue # CodeBuild wasn't auth'd to assume the role for node_source in nodes: # skip self-access checks if node_source == node_destination: continue # check if source is an admin: if so, it can access destination but this is not tracked via an Edge if node_source.is_admin: continue # check if source can use existing projects if node_destination.arn in codebuild_map: projects = codebuild_map[node_destination.arn] for project in projects: startproj_auth, startproj_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'codebuild:StartBuild', project['proj_arn'], _gen_resource_tag_conditions(project['proj_tags']), service_control_policy_groups=scps) if startproj_auth: result.append( Edge( node_source, node_destination, '(MFA Required) can use CodeBuild with an existing project to access' if startproj_mfa else 'can use CodeBuild with an existing project to access', 'CodeBuild')) break # break out of iterating through projects batchstartproj_auth, batchstartproj_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'codebuild:StartBuildBatch', project['proj_arn'], _gen_resource_tag_conditions(project['proj_tags']), service_control_policy_groups=scps) if batchstartproj_auth: result.append( Edge( node_source, node_destination, '(MFA Required) can use CodeBuild with an existing project to access' if startproj_mfa else 'can use CodeBuild with an existing project to access', 'CodeBuild')) break # break out of iterating through projects # check if source can create/update a project, pass this role, then start a build condition_keys = {'iam:PassedToService': 'codebuild.amazonaws.com'} pass_role_auth, pass_role_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:PassRole', node_destination.arn, condition_keys, service_control_policy_groups=scps) if not pass_role_auth: continue # if we can't pass this role, then we're done # check if the source can create a project and start a build create_proj_auth, create_proj_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'codebuild:CreateProject', '*', {}, service_control_policy_groups=scps) if create_proj_auth: startproj_auth, startproj_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'codebuild:StartBuild', '*', {}, service_control_policy_groups=scps) if startproj_auth: result.append( Edge( node_source, node_destination, '(MFA Required) can create a project in CodeBuild to access' if create_proj_mfa or pass_role_mfa else 'can create a project in CodeBuild to access', 'CodeBuild')) else: batchstartproj_auth, batchstartproj_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'codebuild:StartBuildBatch', '*', {}, service_control_policy_groups=scps) if batchstartproj_auth: result.append( Edge( node_source, node_destination, '(MFA Required) can create a project in CodeBuild to access' if create_proj_mfa or pass_role_mfa else 'can create a project in CodeBuild to access', 'CodeBuild')) # check if the source can update a project and start a build for project in codebuild_projects: update_proj_auth, update_proj_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'codebuild:UpdateProject', project['project_arn'], _gen_resource_tag_conditions(project['project_tags']), service_control_policy_groups=scps) if update_proj_auth: startproj_auth, startproj_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'codebuild:StartBuild', project['project_arn'], _gen_resource_tag_conditions(project['project_tags']), service_control_policy_groups=scps) if startproj_auth: result.append( Edge( node_source, node_destination, '(MFA Required) can update a project in CodeBuild to access' if create_proj_mfa or pass_role_mfa else 'can update a project in CodeBuild to access', 'CodeBuild')) break # just wanna find that there exists one updatable/usable project else: batchstartproj_auth, batchstartproj_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'codebuild:StartBuildBatch', project['project_arn'], _gen_resource_tag_conditions( project['project_tags']), service_control_policy_groups=scps) if batchstartproj_auth: result.append( Edge( node_source, node_destination, '(MFA Required) can update a project in CodeBuild to access' if create_proj_mfa or pass_role_mfa else 'can update a project in CodeBuild to access', 'CodeBuild')) break # just wanna find that there exists one updatable/usable project return result
def generate_edges_locally(nodes: List[Node], stack_list: List[dict], scps: Optional[List[List[dict]]] = None) -> List[Edge]: """Generates and returns Edge objects. Works on the assumption that the param `stack_list` is the collected outputs from calling `cloudformation:DescribeStacks`. Thus, it is possible to create a similar output and feed it to this method if you are operating offline (infra-as-code). """ result = [] for node_destination in nodes: # check if the destination is a role if ':role/' not in node_destination.arn: continue # check that the destination role can be assumed by CloudFormation sim_result = resource_policy_authorization( 'cloudformation.amazonaws.com', arns.get_account_id(node_destination.arn), node_destination.trust_policy, 'sts:AssumeRole', node_destination.arn, {} ) if sim_result != ResourcePolicyEvalResult.SERVICE_MATCH: continue # CloudFormation wasn't auth'd to assume the role for node_source in nodes: # skip self-access checks if node_source == node_destination: continue # check if source is an admin: if so, it can access destination but this is not tracked via an Edge if node_source.is_admin: continue # Get iam:PassRole info can_pass_role, need_mfa_passrole = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:PassRole', node_destination.arn, { 'iam:PassedToService': 'cloudformation.amazonaws.com' }, service_control_policy_groups=scps ) # See if source can make a new stack and pass the destination role if can_pass_role: can_create, need_mfa_create = query_interface.local_check_authorization_handling_mfa( node_source, 'cloudformation:CreateStack', '*', {'cloudformation:RoleArn': node_destination.arn}, service_control_policy_groups=scps ) if can_create: reason = 'can create a stack in CloudFormation to access' if need_mfa_passrole or need_mfa_create: reason = '(MFA required) ' + reason result.append(Edge(node_source, node_destination, reason, 'Cloudformation')) relevant_stacks = [] # we'll reuse this for *ChangeSet for stack in stack_list: if 'RoleArn' in stack: if stack['RoleARN'] == node_destination.arn: relevant_stacks.append(stack) # See if source can call UpdateStack to use the current role of a stack (setting a new template) for stack in relevant_stacks: can_update, need_mfa_update = query_interface.local_check_authorization_handling_mfa( node_source, 'cloudformation:UpdateStack', stack['StackId'], {'cloudformation:RoleArn': node_destination.arn}, service_control_policy_groups=scps ) if can_update: reason = 'can update the CloudFormation stack {} to access'.format( stack['StackId'] ) if need_mfa_update: reason = '(MFA required) ' + reason result.append(Edge(node_source, node_destination, reason, 'Cloudformation')) break # let's save ourselves having to dig into every CF stack edge possible # See if source can call UpdateStack to pass a new role to a stack and use it if can_pass_role: for stack in stack_list: can_update, need_mfa_update = query_interface.local_check_authorization_handling_mfa( node_source, 'cloudformation:UpdateStack', stack['StackId'], {'cloudformation:RoleArn': node_destination.arn}, service_control_policy_groups=scps ) if can_update: reason = 'can update the CloudFormation stack {} and pass the role to access'.format( stack['StackId'] ) if need_mfa_update or need_mfa_passrole: reason = '(MFA required) ' + reason result.append(Edge(node_source, node_destination, reason, 'Cloudformation')) break # save ourselves from digging into all CF stack edges possible # See if source can call CreateChangeSet and ExecuteChangeSet to alter a stack with a given role for stack in relevant_stacks: can_make_cs, need_mfa_make = query_interface.local_check_authorization_handling_mfa( node_source, 'cloudformation:CreateChangeSet', stack['StackId'], {'cloudformation:RoleArn': node_destination.arn}, service_control_policy_groups=scps ) if not can_make_cs: continue can_exe_cs, need_mfa_exe = query_interface.local_check_authorization_handling_mfa( node_source, 'cloudformation:ExecuteChangeSet', stack['StackId'], {}, # docs say no RoleArn context here service_control_policy_groups=scps ) if can_exe_cs: reason = 'can create and execute a changeset in CloudFormation for stack {} to access'.format( stack['StackId'] ) if need_mfa_make or need_mfa_exe: reason = '(MFA required) ' + reason result.append(Edge(node_source, node_destination, reason, 'Cloudformation')) break # save ourselves from digging into all CF stack edges possible return result
def generate_edges_locally( nodes: List[Node], scps: Optional[List[List[dict]]] = None) -> List[Edge]: """Generates and returns Edge objects. It is possible to use this method if you are operating offline (infra-as-code). """ result = [] for node_destination in nodes: # check if destination is a user, skip if so if ':role/' not in node_destination.arn: continue # check that the destination role can be assumed by EC2 sim_result = resource_policy_authorization( 'ec2.amazonaws.com', arns.get_account_id(node_destination.arn), node_destination.trust_policy, 'sts:AssumeRole', node_destination.arn, {}, ) if sim_result != ResourcePolicyEvalResult.SERVICE_MATCH: continue # EC2 wasn't auth'd to assume the role for node_source in nodes: # skip self-access checks if node_source == node_destination: continue # check if source is an admin: if so, it can access destination but this is not tracked via an Edge if node_source.is_admin: continue # check if source can pass the destination role mfa_needed = False condition_keys = {'iam:PassedToService': 'ec2.amazonaws.com'} pass_role_auth, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:PassRole', node_destination.arn, condition_keys, service_control_policy_groups=scps) if not pass_role_auth: continue # source can't pass the role to use it # check if destination has an instance profile, if not: check if source can create it if node_destination.instance_profile is None: create_ip_auth, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:CreateInstanceProfile', '*', {}, service_control_policy_groups=scps) if not create_ip_auth: continue # node_source can't make the instance profile if mfa_res: mfa_needed = True create_ip_auth, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:AddRoleToInstanceProfile', node_destination.arn, {}, service_control_policy_groups=scps) if not create_ip_auth: continue # node_source can't attach a new instance profile to node_destination if mfa_res: mfa_needed = True # check if source can run an instance with the instance profile condition, add edge if so and continue if node_destination.instance_profile is not None and len( node_destination.instance_profile) > 0: iprofile = node_destination.instance_profile[0] condition_keys = {'ec2:InstanceProfile': iprofile} else: iprofile = '*' condition_keys = {} create_instance_res, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'ec2:RunInstances', '*', condition_keys, service_control_policy_groups=scps) if mfa_res: mfa_needed = True if create_instance_res: if iprofile is not '*': reason = 'can use EC2 to run an instance with an existing instance profile to access' else: reason = 'can use EC2 to run an instance with a newly created instance profile to access' if mfa_needed: reason = '(MFA required) ' + reason new_edge = Edge(node_source, node_destination, reason, 'EC2') result.append(new_edge) # check if source can run an instance without an instance profile then add the profile, add edge if so create_instance_res, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'ec2:RunInstances', '*', {}, service_control_policy_groups=scps) if mfa_res: mfa_needed = True if create_instance_res: attach_ip_res, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'ec2:AssociateIamInstanceProfile', '*', condition_keys, service_control_policy_groups=scps) if iprofile is not '*': reason = 'can use EC2 to run an instance and then associate an existing instance profile to ' \ 'access' else: reason = 'can use EC2 to run an instance and then attach a newly created instance profile to ' \ 'access' if mfa_res or mfa_needed: reason = '(MFA required) ' + reason if attach_ip_res: new_edge = Edge(node_source, node_destination, reason, 'EC2') result.append(new_edge) return result
def generate_edges_locally( nodes: List[Node], scps: Optional[List[List[dict]]] = None) -> List[Edge]: """Generates and returns Edge objects. It is possible to use this method if you are operating offline (infra-as-code). """ result = [] for node_destination in nodes: if ':role/' not in node_destination.arn: continue # skip if destination is a user and not a role sim_result = resource_policy_authorization( 'sagemaker.amazonaws.com', arns.get_account_id(node_destination.arn), node_destination.trust_policy, 'sts:AssumeRole', node_destination.arn, {}) if sim_result != ResourcePolicyEvalResult.SERVICE_MATCH: continue # SageMaker is not authorized to assume the role for node_source in nodes: if node_source == node_destination: continue # skip self-access checks if node_source.is_admin: continue # skip if source is already admin, not tracked via edges mfa_needed = False conditions = {'iam:PassedToService': 'sagemaker.amazonaws.com'} pass_role_auth, needs_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:PassRole', node_destination.arn, conditions, service_control_policy_groups=scps) if not pass_role_auth: continue # source node is not authorized to pass the role if needs_mfa: mfa_needed = True create_notebook_auth, needs_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'sagemaker:CreateNotebookInstance', '*', {}, service_control_policy_groups=scps) if not create_notebook_auth: continue # source node is not authorized to launch the sagemaker notebook if needs_mfa: mfa_needed = True new_edge = Edge( node_source, node_destination, '(MFA required) can use SageMaker to launch a notebook and access' if mfa_needed else 'can use SageMaker to launch a notebook and access', 'SageMaker') result.append(new_edge) return result
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 return_edges(self, nodes: List[Node], output: io.StringIO = os.devnull, debug: bool = False) -> List[Edge]: """Fulfills expected method return_edges.""" result = [] for node_source in nodes: for node_destination in nodes: # skip self-access checks if node_source == node_destination: continue # check if source is an admin: if so, it can access destination but this is not tracked via an Edge if node_source.is_admin: continue # check if destination is a user, skip if so if ':user/' in node_destination.arn: continue # check that the destination role can be assumed by EC2 sim_result = resource_policy_authorization( 'ec2.amazonaws.com', arns.get_account_id(node_source.arn), node_destination.trust_policy, 'sts:AssumeRole', node_destination.arn, {}, debug) if sim_result != ResourcePolicyEvalResult.SERVICE_MATCH: continue # EC2 wasn't auth'd to assume the role # check if source can pass the destination role mfa_needed = False condition_keys = {'iam:PassedToService': 'ec2.amazonaws.com'} pass_role_auth, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:PassRole', node_destination.arn, condition_keys, debug) if not pass_role_auth: continue # source can't pass the role to use it # check if destination has an instance profile, if not: check if source can create it if node_destination.instance_profile is None: create_ip_auth, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:CreateInstanceProfile', '*', {}, debug) if not create_ip_auth: continue # node_source can't make the instance profile if mfa_res: mfa_needed = True create_ip_auth, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:AddRoleToInstanceProfile', node_destination.arn, {}, debug) if not create_ip_auth: continue # node_source can't attach a new instance profile to node_destination if mfa_res: mfa_needed = True # check if source can run an instance with the instance profile condition, add edge if so and continue if node_destination.instance_profile is not None: iprofile = node_destination.instance_profile condition_keys = {'ec2:InstanceProfile': iprofile} else: iprofile = '*' condition_keys = {} create_instance_res, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'ec2:RunInstances', '*', condition_keys, debug) if mfa_res: mfa_needed = True if create_instance_res: if iprofile is not '*': reason = 'can use EC2 to run an instance with an existing instance profile to access' else: reason = 'can use EC2 to run an instance with a newly created instance profile to access' if mfa_needed: reason = '(MFA required) ' + reason new_edge = Edge(node_source, node_destination, reason) output.write('Found new edge: {}\n'.format( new_edge.describe_edge())) result.append(new_edge) # check if source can run an instance without an instance profile then add the profile, add edge if so create_instance_res, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'ec2:RunInstances', '*', {}, debug) if mfa_res: mfa_needed = True if create_instance_res: attach_ip_res, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'ec2:AssociateIamInstanceProfile', '*', condition_keys, debug) if iprofile is not '*': reason = 'can use EC2 to run an instance and then associate an existing instance profile to ' \ 'access' else: reason = 'can use EC2 to run an instance and then attach a newly created instance profile to ' \ 'access' if mfa_res or mfa_needed: reason = '(MFA required) ' + reason if attach_ip_res: new_edge = Edge(node_source, node_destination, reason) output.write('Found new edge: {}\n'.format( new_edge.describe_edge())) result.append(new_edge) return result
def return_edges(self, nodes: List[Node], output: io.StringIO = os.devnull, debug: bool = False) -> List[Edge]: """Fulfills expected method return_edges.""" result = [] # Grab existing stacks in each region cloudformation_clients = [] if self.session is not None: print( 'Searching through CloudFormation-supported regions for existing functions.' ) cf_regions = self.session.get_available_regions('cloudformation') for region in cf_regions: cloudformation_clients.append( self.session.create_client('cloudformation', region_name=region)) # grab existing cloudformation stacks stack_list = [] for cf_client in cloudformation_clients: try: paginator = cf_client.get_paginator('describe_stacks') for page in paginator.paginate(): for stack in page['Stacks']: if stack['StackStatus'] not in [ 'CREATE_FAILED', 'DELETE_COMPLETE', 'DELETE_FAILED', 'DELETE_IN_PROGRESS' ]: # ignore unusable stacks stack_list.append(stack) except ClientError: output.write( 'Encountered an exception when listing stacks in the region {}\n' .format(cf_client.meta.region_name)) # For each node... for node_source in nodes: for node_destination in nodes: # skip self-access checks if node_source == node_destination: continue # check if source is an admin: if so, it can access destination but this is not tracked via an Edge if node_source.is_admin: continue # check if the destination is a role if ':role/' not in node_destination.arn: continue # check that the destination role can be assumed by CloudFormation sim_result = resource_policy_authorization( 'cloudformation.amazonaws.com', arns.get_account_id(node_source.arn), node_destination.trust_policy, 'sts:AssumeRole', node_destination.arn, {}, debug) if sim_result != ResourcePolicyEvalResult.SERVICE_MATCH: continue # CloudFormation wasn't auth'd to assume the role # Get iam:PassRole info can_pass_role, need_mfa_passrole = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:PassRole', node_destination.arn, {'iam:PassedToService': 'cloudformation.amazonaws.com'}, debug) # See if source can make a new stack and pass the destination role if can_pass_role: can_create, need_mfa_create = query_interface.local_check_authorization_handling_mfa( node_source, 'cloudformation:CreateStack', '*', {'cloudformation:RoleArn': node_destination.arn}, debug) if can_create: reason = 'can create a stack in CloudFormation to access' if need_mfa_passrole or need_mfa_create: reason = '(MFA required) ' + reason result.append( Edge(node_source, node_destination, reason)) relevant_stacks = [] # we'll reuse this for *ChangeSet for stack in stack_list: if stack['RoleARN'] == node_destination.arn: relevant_stacks.append(stack) # See if source can call UpdateStack to use the current role of a stack (setting a new template) for stack in relevant_stacks: can_update, need_mfa_update = query_interface.local_check_authorization_handling_mfa( node_source, 'cloudformation:UpdateStack', stack['StackId'], {'cloudformation:RoleArn': node_destination.arn}, debug) if can_update: reason = 'can update the CloudFormation stack {} to access'.format( stack['StackId']) if need_mfa_update: reason = '(MFA required) ' + reason result.append( Edge(node_source, node_destination, reason)) break # let's save ourselves having to dig into every CF stack edge possible # See if source can call UpdateStack to pass a new role to a stack and use it if can_pass_role: for stack in stack_list: can_update, need_mfa_update = query_interface.local_check_authorization_handling_mfa( node_source, 'cloudformation:UpdateStack', stack['StackId'], {'cloudformation:RoleArn': node_destination.arn}, debug) if can_update: reason = 'can update the CloudFormation stack {} and pass the role to access'.format( stack['StackId']) if need_mfa_update or need_mfa_passrole: reason = '(MFA required) ' + reason result.append( Edge(node_source, node_destination, reason)) break # save ourselves from digging into all CF stack edges possible # See if source can call CreateChangeSet and ExecuteChangeSet to alter a stack with a given role for stack in relevant_stacks: can_make_cs, need_mfa_make = query_interface.local_check_authorization_handling_mfa( node_source, 'cloudformation:CreateChangeSet', stack['StackId'], {'cloudformation:RoleArn': node_destination.arn}, debug) if not can_make_cs: continue can_exe_cs, need_mfa_exe = query_interface.local_check_authorization_handling_mfa( node_source, 'cloudformation:ExecuteChangeSet', stack['StackId'], {}, # docs say no RoleArn context here debug) if can_exe_cs: reason = 'can create and execute a changeset in CloudFormation for stack {} to access'.format( stack['StackId']) if need_mfa_make or need_mfa_exe: reason = '(MFA required) ' + reason result.append( Edge(node_source, node_destination, reason)) break # save ourselves from digging into all CF stack edges possible for edge in result: output.write("Found new edge: {}\n".format(edge.describe_edge())) return result
def _describe_edge(na, nb) -> str: """Quick method for generating strings describing edges.""" return '{} -> {}'.format( '{}/{}'.format(arns.get_account_id(na.arn), na.searchable_name()), '{}/{}'.format(arns.get_account_id(nb.arn), nb.searchable_name()))
def return_edges(self, nodes: List[Node], output: io.StringIO = os.devnull, debug: bool = False) -> List[Edge]: """Fulfills expected method return_edges. If session object is None, runs checks in offline mode.""" result = [] lambda_clients = [] if self.session is not None: print( 'Searching through Lambda-supported regions for existing functions.' ) lambda_regions = self.session.get_available_regions('lambda') for region in lambda_regions: lambda_clients.append( self.session.create_client('lambda', region_name=region)) # grab existing lambda functions function_list = [] for lambda_client in lambda_clients: try: paginator = lambda_client.get_paginator('list_functions') for page in paginator.paginate( PaginationConfig={'PageSize': 25}): for func in page['Functions']: function_list.append(func) except ClientError: output.write( 'Encountered an exception when listing functions in the region {}\n' .format(lambda_client.meta.region_name)) for node_source in nodes: for node_destination in nodes: # skip self-access checks if node_source == node_destination: continue # check if source is an admin, if so it can access destination but this is not tracked via an Edge if node_source.is_admin: continue # check that destination is a role if ':role/' not in node_destination.arn: continue # check that the destination role can be assumed by Lambda sim_result = resource_policy_authorization( 'lambda.amazonaws.com', arns.get_account_id(node_source.arn), node_destination.trust_policy, 'sts:AssumeRole', node_destination.arn, {}, debug) if sim_result != ResourcePolicyEvalResult.SERVICE_MATCH: continue # Lambda wasn't auth'd to assume the role # check that source can pass the destination role (store result for future reference) can_pass_role, need_mfa_passrole = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:PassRole', node_destination.arn, {'iam:PassedToService': 'lambda.amazonaws.com'}, debug) # check that source can create a Lambda function and pass it an execution role if can_pass_role: can_create_function, need_mfa_0 = query_interface.local_check_authorization_handling_mfa( node_source, 'lambda:CreateFunction', '*', {}, debug) if can_create_function: if need_mfa_0 or need_mfa_passrole: reason = '(requires MFA) can use Lambda to create a new function with arbitrary code, ' \ 'then pass and access' else: reason = 'can use Lambda to create a new function with arbitrary code, then pass and access' new_edge = Edge(node_source, node_destination, reason) output.write('Found new edge: {}\n'.format( new_edge.describe_edge())) result.append(new_edge) # List of (<function>, bool, bool, bool) func_data = [] for func in function_list: can_change_code, need_mfa_1 = query_interface.local_check_authorization_handling_mfa( node_source, 'lambda:UpdateFunctionCode', func['FunctionArn'], {}, debug) can_change_config, need_mfa_2 = query_interface.local_check_authorization_handling_mfa( node_source, 'lambda:UpdateFunctionConfiguration', func['FunctionArn'], {}, debug) func_data.append( (func, can_change_code, can_change_config, need_mfa_passrole or need_mfa_1 or need_mfa_2)) # check that source can modify a Lambda function and use its existing role for func, can_change_code, can_change_config, need_mfa in func_data: if node_destination.arn == func['Role']: if can_change_code: if need_mfa: reason = '(requires MFA) can use Lambda to edit an existing function ({}) to access'.format( func['FunctionArn']) else: reason = 'can use Lambda to edit an existing function ({}) to access'.format( func['FunctionArn']) new_edge = Edge(node_source, node_destination, reason) output.write('Found new edge: {}\n'.format( new_edge.describe_edge())) break # check that source can modify a Lambda function and pass it another execution role for func, can_change_code, can_change_config, need_mfa in func_data: if can_change_config and can_change_code and can_pass_role: if need_mfa: reason = '(requires MFA) can use Lambda to edit an existing function ({}) to access'.format( func['FunctionArn']) else: reason = 'can use Lambda to edit an existing function ({}) to access'.format( func['FunctionArn']) new_edge = Edge(node_source, node_destination, reason) output.write('Found new edge: {}\n'.format( new_edge.describe_edge())) break return result
def generate_edges_locally( nodes: List[Node], function_list: List[dict], scps: Optional[List[List[dict]]] = None) -> List[Edge]: """Generates and returns Edge objects. It is possible to use this method if you are operating offline (infra-as-code), but you must provide a `function_list` that is a list of dictionaries that mimic the output of calling `lambda:ListFunctions`. """ result = [] for node_destination in nodes: # check that destination is a role if ':role/' not in node_destination.arn: continue # check that the destination role can be assumed by Lambda sim_result = resource_policy_authorization( 'lambda.amazonaws.com', arns.get_account_id(node_destination.arn), node_destination.trust_policy, 'sts:AssumeRole', node_destination.arn, {}) if sim_result != ResourcePolicyEvalResult.SERVICE_MATCH: continue # Lambda wasn't auth'd to assume the role for node_source in nodes: # skip self-access checks if node_source == node_destination: continue # check if source is an admin, if so it can access destination but this is not tracked via an Edge if node_source.is_admin: continue # check that source can pass the destination role (store result for future reference) can_pass_role, need_mfa_passrole = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:PassRole', node_destination.arn, {'iam:PassedToService': 'lambda.amazonaws.com'}, service_control_policy_groups=scps) # check that source can create a Lambda function and pass it an execution role if can_pass_role: can_create_function, need_mfa_0 = query_interface.local_check_authorization_handling_mfa( node_source, 'lambda:CreateFunction', '*', {}, service_control_policy_groups=scps) if can_create_function: if need_mfa_0 or need_mfa_passrole: reason = '(requires MFA) can use Lambda to create a new function with arbitrary code, ' \ 'then pass and access' else: reason = 'can use Lambda to create a new function with arbitrary code, then pass and access' new_edge = Edge(node_source, node_destination, reason, 'Lambda') result.append(new_edge) continue # TODO: reexamine if it is appropriate to skip the next checks, which can be O(n^2) in some accounts func_data = [] # List[Tuple[dict, bool, bool]] for func in function_list: can_change_code, need_mfa_1 = query_interface.local_check_authorization_handling_mfa( node_source, 'lambda:UpdateFunctionCode', func['FunctionArn'], {}, service_control_policy_groups=scps) func_data.append((func, can_change_code, need_mfa_passrole or need_mfa_1)) # check that source can modify a Lambda function and use its existing role for func, can_change_code, need_mfa in func_data: if node_destination.arn == func['Role']: if can_change_code: if need_mfa: reason = '(requires MFA) can use Lambda to edit an existing function ({}) to access'.format( func['FunctionArn']) else: reason = 'can use Lambda to edit an existing function ({}) to access'.format( func['FunctionArn']) new_edge = Edge(node_source, node_destination, reason, 'Lambda') result.append(new_edge) break can_change_config, need_mfa_2 = query_interface.local_check_authorization_handling_mfa( node_source, 'lambda:UpdateFunctionConfiguration', func['FunctionArn'], {}, service_control_policy_groups=scps) if can_change_config and can_change_code and can_pass_role: if need_mfa or need_mfa_2: reason = '(requires MFA) can use Lambda to edit an existing function ({}) to access'.format( func['FunctionArn']) else: reason = 'can use Lambda to edit an existing function ({}) to access'.format( func['FunctionArn']) new_edge = Edge(node_source, node_destination, reason, 'Lambda') result.append(new_edge) break return result
def generate_edges_locally( nodes: List[Node], scps: Optional[List[List[dict]]] = None, launch_configs: Optional[List[dict]] = None) -> List[Edge]: """Generates and returns Edge objects related to EC2 AutoScaling. It is possible to use this method if you are operating offline (infra-as-code). The `launch_configs` param should be a list of dictionary objects with the following expected structure: ~~~ { 'lc_arn': <Launch Configurations ARN>, 'lc_iip': <IAM Instance Profile> } ~~~ All elements are required, but if there is no instance profile then set the field to None. """ result = [] # iterate through nodes, setting up the map as well as identifying if the service role is available role_lc_map = {} service_role_available = False for node in nodes: # this should catch the normal service role + custom ones with the suffix if ':role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling' in node.arn: service_role_available = True if node.instance_profile is not None: for launch_config in launch_configs: if launch_config['lc_iip'] in node.instance_profile: if node in role_lc_map: role_lc_map[node].append(launch_config['lc_arn']) else: role_lc_map[node] = [launch_config['lc_arn']] for node_destination in nodes: # check if destination is a user, skip if so if ':role/' not in node_destination.arn: continue # check that the destination role can be assumed by EC2 sim_result = resource_policy_authorization( 'ec2.amazonaws.com', arns.get_account_id(node_destination.arn), node_destination.trust_policy, 'sts:AssumeRole', node_destination.arn, {}, ) if sim_result != ResourcePolicyEvalResult.SERVICE_MATCH: continue # EC2 wasn't auth'd to assume the role for node_source in nodes: # skip self-access checks if node_source == node_destination: continue # check if source is an admin: if so, it can access destination but this is not tracked via an Edge if node_source.is_admin: continue csr_mfa = False # stash for later ref if not service_role_available: create_service_role_auth, csr_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:CreateServiceLinkedRole', '*', {'iam:AWSServiceName': 'autoscaling.amazonaws.com'}, service_control_policy_groups=scps) if not create_service_role_auth: continue # service role can't be used if it doesn't exist or be created create_auto_scaling_group_auth, casg_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'autoscaling:CreateAutoScalingGroup', '*', {}, service_control_policy_groups=scps) if not create_auto_scaling_group_auth: continue # can't create an auto-scaling group -> move along if node_destination in role_lc_map: if service_role_available: reason = 'can use the EC2 Auto Scaling service role and an existing Launch Configuration to access' else: reason = 'can create the EC2 Auto Scaling service role and an existing Launch Configuration to access' if csr_mfa or casg_mfa: reason = '(MFA Required) ' + reason result.append( Edge(node_source, node_destination, reason, 'EC2 Auto Scaling')) create_launch_config_auth, clc_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'autoscaling:CreateLaunchConfiguration', '*', {}, service_control_policy_groups=scps) if not create_launch_config_auth: continue # we're done here pass_role_auth, pr_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:PassRole', node_destination.arn, {'iam:PassedToService': 'ec2.amazonaws.com'}, service_control_policy_groups=scps) if pass_role_auth: if service_role_available: reason = 'can use the EC2 Auto Scaling service role and create a launch configuration to access' else: reason = 'can create the EC2 Auto Scaling service role and create a launch configuration to access' if clc_mfa or pr_mfa: reason = '(MFA Required) ' + reason result.append( Edge(node_source, node_destination, reason, 'EC2 Auto Scaling')) return result
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, parsed_args.resource) 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 parsed_args.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.argquery(graph, parsed_args.principal, parsed_args.action, parsed_args.resource, conditions, parsed_args.preset, parsed_args.skip_admin, resource_policy, resource_owner, parsed_args.include_unauthorized, parsed_args.session_policy, scps) return 0
def generate_edges_locally( nodes: List[Node], scps: Optional[List[List[dict]]] = None) -> List[Edge]: """Generates and returns Edge objects. It is possible to use this method if you are operating offline (infra-as-code). """ result = [] for node_destination in nodes: if ':role/' not in node_destination.arn: continue # skip non-roles for node_source in nodes: # skip self-access checks if node_source == node_destination: continue # check if source is an admin, if so it can access destination but this is not tracked via an Edge if node_source.is_admin: continue # Check against resource policy sim_result = resource_policy_authorization( node_source, arns.get_account_id(node_source.arn), node_destination.trust_policy, 'sts:AssumeRole', node_destination.arn, {}, ) if sim_result == ResourcePolicyEvalResult.DENY_MATCH: continue # Node was explicitly denied from assuming the role if sim_result == ResourcePolicyEvalResult.NO_MATCH: continue # Resource policy must match for sts:AssumeRole, even in same-account scenarios assume_auth, need_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'sts:AssumeRole', node_destination.arn, {}, service_control_policy_groups=scps) policy_denies = has_matching_statement( node_source, 'Deny', 'sts:AssumeRole', node_destination.arn, {}, ) policy_denies_mfa = has_matching_statement( node_source, 'Deny', 'sts:AssumeRole', node_destination.arn, { 'aws:MultiFactorAuthAge': '1', 'aws:MultiFactorAuthPresent': 'true' }, ) if assume_auth: if need_mfa: reason = '(requires MFA) can access via sts:AssumeRole' else: reason = 'can access via sts:AssumeRole' new_edge = Edge(node_source, node_destination, reason, 'AssumeRole') result.append(new_edge) elif not (policy_denies_mfa and policy_denies ) and sim_result == ResourcePolicyEvalResult.NODE_MATCH: # testing same-account scenario, so NODE_MATCH will override a lack of an allow from iam policy new_edge = Edge(node_source, node_destination, 'can access via sts:AssumeRole', 'AssumeRole') result.append(new_edge) return result
def local_check_authorization_full( principal: Node, action_to_check: str, resource_to_check: str, condition_keys_to_check: dict, resource_policy: Optional[dict] = None, resource_owner: Optional[str] = None, service_control_policy_groups: Optional[List[List[Policy]]] = None, session_policy: Optional[dict] = None) -> bool: """Determine if a given node is authorized to make an API call. It will perform a full local policy evaluation, which includes: * Checking for any matching Deny statements in all policies that are given * Checking Organization SCPs (if given) * Checking the resource policy (if given) * Checking the principal's permission boundaries (if the caller has any attached) * Checking the session policy (if given) * Checking the principal's policies This will add condition keys that may be inferred, assuming they are not already set, such as the aws:username or aws:userid keys. If the resource_policy param is not None but the resource_owner is None, this raises a ValueError, so that must be sorted beforehand by any code calling this function.""" if resource_policy is not None and resource_owner is None: raise ValueError( 'Must specify the AWS Account ID of the owner of the resource when specifying a resource policy' ) conditions_keys_copy = copy.deepcopy(condition_keys_to_check) conditions_keys_copy.update( _infer_condition_keys(principal, conditions_keys_copy)) logger.debug( 'Testing authorization for: principal: {}, action: {}, resource: {}, conditions: {}, Resource Policy: {}, SCPs: {}, Session Policy: {}' .format(principal.arn, action_to_check, resource_to_check, conditions_keys_copy, resource_policy, service_control_policy_groups, session_policy)) # Check all policies for a matching deny for policy in principal.attached_policies: if policy_has_matching_statement(policy, 'Deny', action_to_check, resource_to_check, conditions_keys_copy): logger.debug('Explicit Deny: Principal\'s attached policies.') return False if service_control_policy_groups is not None: for service_control_policy_group in service_control_policy_groups: for service_control_policy in service_control_policy_group: if policy_has_matching_statement(service_control_policy, 'Deny', action_to_check, resource_to_check, conditions_keys_copy): logger.debug('Explicit Deny: SCPs') return False if resource_policy is not None: rp_matching_statements = resource_policy_matching_statements( principal, resource_policy, action_to_check, resource_to_check, conditions_keys_copy) for statement in rp_matching_statements: if statement['Effect'] == 'Deny': logger.debug('Explicit Deny: Resource Policy') return False if session_policy is not None: if policy_has_matching_statement(session_policy, 'Deny', action_to_check, resource_to_check, conditions_keys_copy): logger.debug('Explict Deny: Session policy') return False if principal.permissions_boundary is not None: if policy_has_matching_statement(principal.permissions_boundary, 'Deny', action_to_check, resource_to_check, conditions_keys_copy): logger.debug('Explicit Deny: Permission Boundary') return False # Check SCPs if service_control_policy_groups is not None: for service_control_policy_group in service_control_policy_groups: # For every group of SCPs (policies attached to the ancestors of the account and the current account), the # group of SCPs have to have a matching allow statement scp_group_result = False for service_control_policy in service_control_policy_group: if policy_has_matching_statement(service_control_policy, 'Allow', action_to_check, resource_to_check, conditions_keys_copy): scp_group_result = True break if not scp_group_result: logger.debug('Implicit Deny: SCP group') return False # Check resource policy if resource_policy is not None: rp_auth_result = resource_policy_authorization( principal, resource_owner, resource_policy, action_to_check, resource_to_check, conditions_keys_copy) if arns.get_account_id(principal.arn) == resource_owner: # resource is owned by account if arns.get_service(resource_to_check) in ( 'iam', 'kms'): # TODO: tuple or list? # IAM and KMS require the trust/key policy to match if rp_auth_result is not ResourcePolicyEvalResult.NODE_MATCH and rp_auth_result is not ResourcePolicyEvalResult.ROOT_MATCH: logger.debug( 'IAM/KMS Denial: RP must authorize even with same account' ) return False if rp_auth_result is ResourcePolicyEvalResult.NODE_MATCH: # If the specific IAM User/Role is given in the resource policy's Principal element and from the same # account as the resource, we're done since we've already done deny-checks and the permission boundaries # + session policy + principal policies aren't necessary to grant authorization logger.debug('RP approval: skip further evaluation') return True else: # resource is owned by another account if rp_auth_result is ResourcePolicyEvalResult.NO_MATCH: logger.debug('Cross-Account authorization denied') return False # Check permission boundary if principal.permissions_boundary is not None: if not policy_has_matching_statement( principal.permissions_boundary, 'Allow', action_to_check, resource_to_check, conditions_keys_copy): logger.debug('Implicit Deny: Permission Boundary') return False # Check session policy if session_policy is not None: if not policy_has_matching_statement( session_policy, 'Allow', action_to_check, resource_to_check, conditions_keys_copy): logger.debug('Implicit Deny: Session Policy') return False # Check principal's policies for policy in principal.attached_policies: if policy_has_matching_statement(policy, 'Allow', action_to_check, resource_to_check, conditions_keys_copy): return True # already did Deny statement checks, so we're done logger.debug('Implicit Deny: Principal\'s Attached Policies') return False
def generate_edges_locally( nodes: List[Node], scps: Optional[List[List[dict]]] = None) -> List[Edge]: """Generates and returns Edge objects. It is possible to use this method if you are operating offline (infra-as-code). """ result = [] for node_destination in nodes: # check if destination is a role with an instance profile if ':role/' not in node_destination.arn or node_destination.instance_profile is None: continue # check if the destination can be assumed by EC2 sim_result = resource_policy_authorization( 'ec2.amazonaws.com', arns.get_account_id(node_destination.arn), node_destination.trust_policy, 'sts:AssumeRole', node_destination.arn, {}, ) if sim_result != ResourcePolicyEvalResult.SERVICE_MATCH: continue # EC2 wasn't auth'd to assume the role # at this point, we make an assumption that some instance is operating with the given instance profile # we assume if the role can call ssmmessages:CreateControlChannel, anyone with ssm perms can access it if not query_interface.local_check_authorization( node_destination, 'ssmmessages:CreateControlChannel', '*', {}): continue for node_source in nodes: # skip self-access checks if node_source == node_destination: continue # check if source is an admin, if so it can access destination but this is not tracked via an Edge if node_source.is_admin: continue # so if source can call ssm:SendCommand or ssm:StartSession, it's an edge cmd_auth_res, mfa_res_1 = query_interface.local_check_authorization_handling_mfa( node_source, 'ssm:SendCommand', '*', {}, ) if cmd_auth_res: reason = 'can call ssm:SendCommand to access an EC2 instance with access to' if mfa_res_1: reason = '(Requires MFA) ' + reason result.append( Edge(node_source, node_destination, reason, 'SSM')) sesh_auth_res, mfa_res_2 = query_interface.local_check_authorization_handling_mfa( node_source, 'ssm:StartSession', '*', {}, ) if sesh_auth_res: reason = 'can call ssm:StartSession to access an EC2 instance with access to' if mfa_res_2: reason = '(Requires MFA) ' + reason result.append( Edge(node_source, node_destination, reason, 'SSM')) return result
def return_edges(self, nodes: List[Node], output: io.StringIO = os.devnull, debug: bool = False) -> List[Edge]: """Fulfills expected method return_edges. If the session object is None, performs checks in offline-mode""" result = [] for node_source in nodes: for node_destination in nodes: # skip self-access checks if node_source == node_destination: continue # check if source is an admin, if so it can access destination but this is not tracked via an Edge if node_source.is_admin: continue # check if source can call sts:AssumeRole to access the destination if destination is a role if ':role/' in node_destination.arn: # Check against resource policy sim_result = resource_policy_authorization( node_source, arns.get_account_id(node_source.arn), node_destination.trust_policy, 'sts:AssumeRole', node_destination.arn, {}, debug) if sim_result == ResourcePolicyEvalResult.DENY_MATCH: continue # Node was explicitly denied from assuming the role if sim_result == ResourcePolicyEvalResult.NO_MATCH: continue # Resource policy must match for sts:AssumeRole, even in same-account scenarios assume_auth, need_mfa = query_interface.local_check_authorization_handling_mfa( node_source, 'sts:AssumeRole', node_destination.arn, {}, debug) policy_denies = has_matching_statement( node_source, 'Deny', 'sts:AssumeRole', node_destination.arn, {}, debug) policy_denies_mfa = has_matching_statement( node_source, 'Deny', 'sts:AssumeRole', node_destination.arn, { 'aws:MultiFactorAuthAge': '1', 'aws:MultiFactorAuthPresent': 'true' }, debug) if assume_auth: if need_mfa: reason = '(requires MFA) can access via sts:AssumeRole' else: reason = 'can access via sts:AssumeRole' new_edge = Edge(node_source, node_destination, reason) output.write('Found new edge: {}\n'.format( new_edge.describe_edge())) result.append(new_edge) elif not ( policy_denies_mfa and policy_denies ) and sim_result == ResourcePolicyEvalResult.NODE_MATCH: # testing same-account scenario, so NODE_MATCH will override a lack of an allow from iam policy new_edge = Edge(node_source, node_destination, 'can access via sts:AssumeRole') output.write('Found new edge: {}\n'.format( new_edge.describe_edge())) result.append(new_edge) return result