def _get_admin_reason(node: Node) -> List[str]: """Return a list of reasons why this given node is an admin.""" result = [] logger.debug("Checking if {} is an admin".format(node.searchable_name())) node_type = arns.get_resource(node.arn).split('/')[0] # check if node can modify its own inline policies if node_type == 'user': action = 'iam:PutUserPolicy' else: # node_type == 'role' action = 'iam:PutRolePolicy' if query_interface.local_check_authorization_handling_mfa(node, action, node.arn, {})[0]: result.append('Can call {} to add/update their own inline policies'.format(action)) # check if node can attach the AdministratorAccess policy to itself if node_type == 'user': action = 'iam:AttachUserPolicy' else: action = 'iam:AttachRolePolicy' condition_keys = {'iam:PolicyARN': 'arn:aws:iam::aws:policy/AdministratorAccess'} if query_interface.local_check_authorization_handling_mfa(node, action, node.arn, condition_keys)[0]: result.append('Can call {} to attach the AdministratorAccess policy to itself'.format(action)) # check if node can create a role and attach the AdministratorAccess policy or an inline policy if query_interface.local_check_authorization_handling_mfa(node, 'iam:CreateRole', '*', {})[0]: if query_interface.local_check_authorization_handling_mfa(node, 'iam:AttachRolePolicy', '*', condition_keys)[0]: result.append('Can create an IAM Role (iam:CreateRole) and attach the AdministratorAccess policy to it (iam:AttachRolePolicy)'.format(action)) if query_interface.local_check_authorization_handling_mfa(node, 'iam:PutRolePolicy', '*', condition_keys)[0]: result.append('Can create an IAM Role (iam:CreateRole) and create an inline policy for it (iam:PutRolePolicy)'.format(action)) # check if node can update an attached customer-managed policy (assumes SetAsDefault is set to True) for attached_policy in node.attached_policies: if attached_policy.arn != node.arn and ':aws:policy/' not in attached_policy.arn: if query_interface.local_check_authorization_handling_mfa(node, 'iam:CreatePolicyVersion', attached_policy.arn, {})[0]: result.append('Can modify the attached managed policy {} (iam:CreatePolicyVersion)'.format(attached_policy.arn)) break # reduce output # check if node is a user, and if it can attach or modify any of its groups's policies if node_type == 'user': for group in node.group_memberships: group_name = group.arn.split('/')[-1] if query_interface.local_check_authorization_handling_mfa(node, 'iam:PutGroupPolicy', group.arn, {})[0]: result.append('Can add/update an inline policy for the group {} (iam:PutGroupPolicy)'.format(group_name)) if query_interface.local_check_authorization_handling_mfa(node, 'iam:AttachGroupPolicy', group.arn, condition_keys)[0]: result.append('Can attach the AdministratorAccess policy to the group {} (iam:AttachGroupPolicy)'.format(group_name)) for attached_policy in group.attached_policies: if attached_policy.arn != group.arn and ':aws:policy/' not in attached_policy.arn: if query_interface.local_check_authorization_handling_mfa(node, 'iam:CreatePolicyVersion', attached_policy.arn, {})[0]: result.append('Can update the managed policy {} that is attached to the group {} (iam:CreatePolicyVersion)'.format(attached_policy.arn, group_name)) break # reduce output 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 session object is None, runs 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 destination is a role with an instance profile if ':role/' not in node_destination.arn or node_destination.instance_profile is None: continue # 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', '*', {}, False): 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', '*', {}, False) 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)) sesh_auth_res, mfa_res_2 = query_interface.local_check_authorization_handling_mfa( node_source, 'ssm:StartSession', '*', {}, False) 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)) for edge in result: output.write("Found new edge: {}\n".format(edge.describe_edge())) return result
def _can_call_without_mfa(node: Node, actions: List[str]) -> bool: """Returns true if node can call sensitive action without MFA""" for action in actions: auth, needmfa = query_interface.local_check_authorization_handling_mfa( node, action, '*', {}) if auth and not needmfa: return True return False
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 update_admin_status(nodes: List[Node], output: io.StringIO = os.devnull, debug: bool = False) -> None: """Given a list of nodes, goes through and updates each node's is_admin data.""" for node in nodes: output.write("checking if {} is an admin\n".format( node.searchable_name())) node_type = arns.get_resource(node.arn).split('/')[0] # check if node can modify its own inline policies if node_type == 'user': action = 'iam:PutUserPolicy' else: # node_type == 'role' action = 'iam:PutRolePolicy' if query_interface.local_check_authorization_handling_mfa( node, action, node.arn, {}, debug)[0]: node.is_admin = True continue # check if node can attach the AdministratorAccess policy to itself if node_type == 'user': action = 'iam:AttachUserPolicy' else: action = 'iam:AttachRolePolicy' condition_keys = { 'iam:PolicyARN': 'arn:aws:iam::aws:policy/AdministratorAccess' } if query_interface.local_check_authorization_handling_mfa( node, action, node.arn, condition_keys, debug)[0]: node.is_admin = True continue # check if node can create a role and attach the AdministratorAccess policy or an inline policy if query_interface.local_check_authorization_handling_mfa( node, 'iam:CreateRole', '*', {}, debug)[0]: if query_interface.local_check_authorization_handling_mfa( node, 'iam:AttachRolePolicy', '*', condition_keys, debug)[0]: node.is_admin = True continue if query_interface.local_check_authorization_handling_mfa( node, 'iam:PutRolePolicy', '*', condition_keys, debug)[0]: node.is_admin = True continue # check if node can update an attached customer-managed policy (assumes SetAsDefault is set to True) for attached_policy in node.attached_policies: if attached_policy.arn != node.arn: if query_interface.local_check_authorization_handling_mfa( node, 'iam:CreatePolicyVersion', attached_policy.arn, {}, debug)[0]: node.is_admin = True continue # check if node is a user, and if it can attach or modify any of its groups's policies if node_type == 'user': for group in node.group_memberships: if query_interface.local_check_authorization_handling_mfa( node, 'iam:PutGroupPolicy', group.arn, {}, debug)[0]: node.is_admin = True break # break the loop through groups if query_interface.local_check_authorization_handling_mfa( node, 'iam:AttachGroupPolicy', group.arn, condition_keys, debug)[0]: node.is_admin = True break # as above for attached_policy in group.attached_policies: if attached_policy.arn != group.arn: if query_interface.local_check_authorization_handling_mfa( node, 'iam:CreatePolicyVersion', attached_policy.arn, {}, debug)[0]: node.is_admin = True break # break the loop through policies if node.is_admin: break # break the loop through groups if node.is_admin: continue # if we add more checks later, this optimizes them out when appropriate
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 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 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 if ':user/' in node_destination.arn: # Change the user's access keys access_keys_mfa = False create_auth_res, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:CreateAccessKey', node_destination.arn, {}, debug) if mfa_res: access_keys_mfa = True if node_destination.access_keys == 2: # can have a max of two access keys, need to delete before making a new one auth_res, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:DeleteAccessKey', node_destination.arn, {}, debug) if not auth_res: create_auth_res = False # can't delete target access key, can't generate a new one if mfa_res: access_keys_mfa = True if create_auth_res: reason = 'can create access keys to authenticate as' if access_keys_mfa: reason = '(MFA required) ' + reason result.append( Edge(node_source, node_destination, reason)) # Change the user's password if node_destination.active_password: pass_auth_res, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:UpdateLoginProfile', node_destination.arn, {}, debug) else: pass_auth_res, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:CreateLoginProfile', node_destination.arn, {}, debug) if pass_auth_res: reason = 'can set the password to authenticate as' if mfa_res: reason = '(MFA required) ' + reason result.append( Edge(node_source, node_destination, reason)) if ':role/' in node_destination.arn: # Change the role's trust doc update_role_res, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:UpdateAssumeRolePolicy', node_destination.arn, {}, debug) if update_role_res: reason = 'can update the trust document to access' if mfa_res: reason = '(MFA required) ' + reason result.append( Edge(node_source, node_destination, reason)) for edge in result: output.write("Found new edge: {}\n".format(edge.describe_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. 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], 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], 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_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 if ':user/' in node_destination.arn: # Change the user's access keys access_keys_mfa = False create_auth_res, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:CreateAccessKey', node_destination.arn, {}, service_control_policy_groups=scps ) if mfa_res: access_keys_mfa = True if node_destination.access_keys == 2: # can have a max of two access keys, need to delete before making a new one auth_res, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:DeleteAccessKey', node_destination.arn, {}, service_control_policy_groups=scps ) if not auth_res: create_auth_res = False # can't delete target access key, can't generate a new one if mfa_res: access_keys_mfa = True if create_auth_res: reason = 'can create access keys to authenticate as' if access_keys_mfa: reason = '(MFA required) ' + reason result.append( Edge( node_source, node_destination, reason, 'IAM' ) ) # Change the user's password if node_destination.active_password: pass_auth_res, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:UpdateLoginProfile', node_destination.arn, {}, service_control_policy_groups=scps ) else: pass_auth_res, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:CreateLoginProfile', node_destination.arn, {}, service_control_policy_groups=scps ) if pass_auth_res: reason = 'can set the password to authenticate as' if mfa_res: reason = '(MFA required) ' + reason result.append(Edge(node_source, node_destination, reason, 'IAM')) if ':role/' in node_destination.arn: # Change the role's trust doc update_role_res, mfa_res = query_interface.local_check_authorization_handling_mfa( node_source, 'iam:UpdateAssumeRolePolicy', node_destination.arn, {}, service_control_policy_groups=scps ) if update_role_res: reason = 'can update the trust document to access' if mfa_res: reason = '(MFA required) ' + reason result.append(Edge(node_source, node_destination, reason, 'IAM')) 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 = [] 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 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 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
def test_local_mfa_handling(self): mfa_policy = Policy( arn='arn:aws:iam::000000000000:policy/mfa_policy', name='mfa_policy', policy_doc={ "Version": "2012-10-17", "Statement": [{ "Sid": "AllowManageOwnUserMFA", "Effect": "Allow", "Action": [ "iam:DeactivateMFADevice", "iam:EnableMFADevice", "iam:GetUser", "iam:ListMFADevices", "iam:ResyncMFADevice" ], "Resource": "arn:aws:iam::*:user/${aws:username}" }, { "Sid": "DenyAllExceptListedIfNoMFA", "Effect": "Deny", "NotAction": [ "iam:CreateVirtualMFADevice", "iam:EnableMFADevice", "iam:GetUser", "iam:ListMFADevices", "iam:ListVirtualMFADevices", "iam:ResyncMFADevice", "sts:GetSessionToken" ], "Resource": "*", "Condition": { "BoolIfExists": { "aws:MultiFactorAuthPresent": "false" } } }] }) s3_policy = Policy( 'arn:aws:iam::000000000000:policy/s3access', 's3access', { "Version": "2012-10-17", "Statement": [{ "Sid": "AllowViewAccountInfo", "Effect": "Allow", "Action": "s3:ListAllMyBuckets", "Resource": "*" }] }) test_node = Node('arn:aws:iam::000000000000:user/uses_mfa', 'AIDA00000000000000000', [mfa_policy, s3_policy], [], None, None, 1, False, False, None, True, None) print(mfa_policy.to_dictionary()) print(s3_policy.to_dictionary()) print(test_node.to_dictionary()) # Test that lack of MFA conditions is not allowed (note the function call diff) # This matches policy sim feedback auth_result = local_check_authorization(test_node, 's3:ListAllMyBuckets', '*', {}) self.assertFalse(auth_result) # Test that MFA set to false is disallowed auth_result, mfa_result = local_check_authorization_handling_mfa( test_node, 's3:ListAllMyBuckets', '*', {'aws:MultiFactorAuthPresent': 'false'}) self.assertFalse(auth_result) self.assertFalse(mfa_result) # Test that testing both with and without MFA yields correct results auth_result, mfa_result = local_check_authorization_handling_mfa( test_node, 's3:ListAllMyBuckets', '*', {}) self.assertTrue(auth_result) self.assertTrue(mfa_result) # Test that iam:EnableMFADevice is allowed despite lack of MFA auth_result, mfa_result = local_check_authorization_handling_mfa( test_node, 'iam:EnableMFADevice', 'arn:aws:iam::000000000000:user/uses_mfa', {}) self.assertFalse(mfa_result) self.assertTrue(auth_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 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 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 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 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