def test_gh_106_excluded_actions_should_not_show_in_results(self): """test_gh_106_excluded_actions_should_not_show_in_results: Make sure that autoscaling:SetDesiredCapacity does not show in results when it is excluded""" test_policy = { "Version": "2012-10-17", "Statement": [{ "Sid": "Something", "Effect": "Allow", "Action": [ "autoscaling:SetDesiredCapacity", "autoscaling:TerminateInstanceInAutoScalingGroup", "autoscaling:UpdateAutoScalingGroup" ], "Resource": "*", }] } exclusions_cfg = { "policies": ["aws-service-role*"], "roles": ["aws-service-role*"], "users": [""], "include-actions": ["s3:GetObject"], "exclude-actions": [ "autoscaling:SetDesiredCapacity", "autoscaling:TerminateInstanceInAutoScalingGroup", "autoscaling:UpdateAutoScalingGroup" ] } exclusions = Exclusions(exclusions_cfg) policy_document = PolicyDocument(test_policy, exclusions) print(policy_document.infrastructure_modification) self.assertEqual(policy_document.infrastructure_modification, []) exclusions_cfg_2 = { "policies": ["aws-service-role*"], "roles": ["aws-service-role*"], "users": [""], "include-actions": ["s3:GetObject"], "exclude-actions": [ "autoscaling:SetDesiredCapacity", "autoscaling:TerminateInstanceInAutoScalingGroup", ] } exclusions_2 = Exclusions(exclusions_cfg_2) policy_document_2 = PolicyDocument(test_policy, exclusions_2) # Should still include one result print(policy_document_2.infrastructure_modification) self.assertEqual(policy_document_2.infrastructure_modification, ["autoscaling:UpdateAutoScalingGroup"])
def __init__( self, policy_details: List[Dict[str, Any]], exclusions: Exclusions = DEFAULT_EXCLUSIONS, flag_conditional_statements: bool = False, flag_resource_arn_statements: bool = False, ) -> None: self.policy_details = [] if not isinstance(exclusions, Exclusions): raise Exception( "The exclusions provided is not an Exclusions type object. " "Please supply an Exclusions object and try again.") self.exclusions = exclusions self.flag_conditional_statements = flag_conditional_statements self.flag_resource_arn_statements = flag_resource_arn_statements for policy_detail in policy_details: this_policy_name = policy_detail["PolicyName"] this_policy_id = policy_detail["PolicyId"] this_policy_path = policy_detail["Path"] # Always exclude the AWS service role policies if is_name_excluded(this_policy_path, "aws-service-role*") or is_name_excluded( this_policy_path, "/aws-service-role*"): logger.debug( "The %s Policy with the policy ID %s is excluded because it is " "an immutable AWS Service role with a path of %s", this_policy_name, this_policy_id, this_policy_path, ) continue # Exclude the managed policies if (exclusions.is_policy_excluded(this_policy_name) or exclusions.is_policy_excluded(this_policy_id) or exclusions.is_policy_excluded(this_policy_path)): logger.debug( "The %s Managed Policy with the policy ID %s and %s path is excluded.", this_policy_name, this_policy_id, this_policy_path, ) continue self.policy_details.append( ManagedPolicy(policy_detail, exclusions=exclusions, flag_resource_arn_statements=self. flag_resource_arn_statements, flag_conditional_statements=self. flag_conditional_statements))
def test_finding_actions_excluded(self): test_policy = { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["s3:GetObject", "logs:CreateLogStream", "logs:PutLogEvents"], "Resource": "*" }] } policy_document = PolicyDocument(test_policy) # (1) EXCLUDE actions exclusions_cfg = { "users": ["obama"], "groups": ["admin"], "roles": ["MyRole"], "policies": ["someOtherName"], "exclude-actions": ["logs:CreateLogStream", "logs:PutLogEvents"] } exclusions = Exclusions(exclusions_cfg) finding = Finding( policy_name="MyPolicy", arn="arn:aws:iam::123456789012:group/SNSNotifications", actions=["s3:GetObject"], policy_document=policy_document, exclusions=exclusions) # print(finding.actions) self.assertListEqual(finding.actions, ["s3:GetObject"])
def test_excluded_actions_scan_policy_file_v2(self): """test_excluded_actions_scan_policy_file_v2: Test the scan_policy command when we have excluded actions""" test_policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:GetObject", "iam:CreateAccessKey"], "Resource": "*" }, ] } expected_results_after_exclusion = {} exclusions_cfg_custom = { "users": ["MyRole"], "groups": ["obama"], "roles": ["admin"], "exclude-actions": ["s3:GetObject", "iam:CreateAccessKey"] } exclusions = Exclusions(exclusions_cfg_custom) results = scan_policy(test_policy, "test", exclusions) # print(json.dumps(results, indent=4)) self.maxDiff = None self.assertDictEqual(results, expected_results_after_exclusion)
def test_policy_finding_for_resource_exposure(self): test_policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObjectAcl" ], "Resource": "*" } ] } policy_document = PolicyDocument(test_policy) exclusions_cfg = dict() exclusions = Exclusions(exclusions_cfg) policy_finding = PolicyFinding(policy_document, exclusions) results = policy_finding.results expected_results = { "ServicesAffected": ["s3"], "PrivilegeEscalation": [], "ResourceExposure": ["s3:PutObjectAcl"], "DataExfiltration": [], "ServiceWildcard": [], "CredentialsExposure": [], "InfrastructureModification": ["s3:PutObjectAcl"], } # print(json.dumps(results, indent=4)) self.assertDictEqual(results, expected_results)
def scan(input, exclusions_file, output, all_access_levels, skip_open_report): # pragma: no cover """ Given the path to account authorization details files and the exclusions config file, scan all inline and managed policies in the account to identify actions that do not leverage resource constraints. """ if exclusions_file: # Get the exclusions configuration with open(exclusions_file, "r") as yaml_file: try: exclusions_cfg = yaml.safe_load(yaml_file) except yaml.YAMLError as exc: logger.critical(exc) exclusions = Exclusions(exclusions_cfg) else: exclusions = DEFAULT_EXCLUSIONS if os.path.isfile(input): scan_account_authorization_file(input, exclusions, output, all_access_levels, skip_open_report) if os.path.isdir(input): logger.info( "The path given is a directory. Scanning for account authorization files and generating report." ) input_files = get_authorization_files_in_directory(input) for file in input_files: logger.info(f"Scanning file: {file}") scan_account_authorization_file(file, exclusions, output, all_access_levels, skip_open_report)
def test_group_membership_finding(self): test_policy = { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "*" }] } policy_document = PolicyDocument(test_policy) exclusions_cfg = dict(users=["obama"], groups=["admin"], roles=["MyRole"], policies=["AWSLambdaFullAccess"]) exclusions = Exclusions(exclusions_cfg) group_finding = GroupFinding( policy_name="MyPolicy", arn="arn:aws:iam::123456789012:group/GroupShouldBeEmpty", actions=["s3:GetObject"], policy_document=policy_document, exclusions=exclusions, members=["obama"]) result = group_finding.is_excluded(exclusions) self.assertListEqual(result, []) group_finding = GroupFinding( policy_name="MyPolicy", arn="arn:aws:iam::123456789012:group/GroupShouldBeEmpty", actions=["s3:GetObject"], policy_document=policy_document, exclusions=exclusions, members=["yolo"]) self.assertFalse(group_finding.is_excluded(exclusions))
def test_new_exclusions_approach(self): exclusions_cfg = { "policies": ["aws-service-role*"], "roles": ["aws-service-role*"], "users": [""], "include-actions": ["s3:GetObject"], "exclude-actions": ["kms:Decrypt"] } exclusions = Exclusions(exclusions_cfg) test_actions_list = [ "s3:GetObject", "kms:decrypt", "ssm:GetParameter", "ec2:DescribeInstances" ] result = exclusions.get_allowed_actions(test_actions_list) self.assertListEqual( result, ['s3:GetObject', 'ssm:GetParameter', 'ec2:DescribeInstances'])
def test_policy_name_finding(self): """output.new_findings.PolicyFinding""" test_policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:GetObject" ], "Resource": "*" } ] } policy_document = PolicyDocument(test_policy) exclusions_cfg = dict( users=["obama"], groups=["admin"], roles=["MyRole"], policies=["AWSLambdaFullAccess"] ) exclusions = Exclusions(exclusions_cfg) # (1) If the policy attached is excluded policy_finding = PolicyFinding( policy_name="AWSLambdaFullAccess", arn="arn:aws:iam::aws:policy/AWSLambdaFullAccess", # Obama is excluded actions=["s3:GetObject"], policy_document=policy_document, exclusions=exclusions ) result = policy_finding.is_excluded(exclusions) expected_result = ["AWSLambdaFullAccess"] self.assertListEqual(result, expected_result) # (2) Non-exclusion exclusions_cfg = dict( users=["obama"], groups=["admin"], roles=["MyRole"], policies=["someOtherName"] ) exclusions = Exclusions(exclusions_cfg) result = policy_finding.is_excluded(exclusions) expected_result = False self.assertEqual(result, expected_result)
def test_is_principal_excluded(self): """scan.principals.Principal.is_principal_excluded""" example_authz_details_file = os.path.abspath( os.path.join( os.path.dirname(__file__), os.path.pardir, "files", "example-authz-details.json", )) with open(example_authz_details_file) as f: contents = f.read() auth_details_json = json.loads(contents) user_principal_detail = PrincipalDetail( auth_details_json["UserDetailList"][0]) group_principal_detail = PrincipalDetail( auth_details_json["GroupDetailList"][0]) role_principal_detail = PrincipalDetail( auth_details_json["RoleDetailList"][0]) exclusions_cfg = { "users": ["obama"], "groups": ["admin"], "roles": [ "MyRole", ], } exclusions = Exclusions(exclusions_cfg) self.assertTrue( user_principal_detail.is_principal_excluded(exclusions)) self.assertTrue( group_principal_detail.is_principal_excluded(exclusions)) self.assertTrue( role_principal_detail.is_principal_excluded(exclusions)) # Testing these with mismatched categories exclusions_cfg = dict(users=["MyRole"], groups=["obama"], roles=["admin"]) exclusions = Exclusions(exclusions_cfg) self.assertFalse( user_principal_detail.is_principal_excluded(exclusions)) self.assertFalse( group_principal_detail.is_principal_excluded(exclusions)) self.assertFalse( role_principal_detail.is_principal_excluded(exclusions))
def scan_policy(policy_json, exclusions_config=DEFAULT_EXCLUSIONS_CONFIG): """ Scan a policy document for missing resource constraints. :param policy_json: Dictionary containing the IAM policy. :param exclusions_config: Exclusions configuration. If none, just send an empty dictionary. Defaults to the contents of cloudsplaining.shared.default-exclusions.yml :return: """ policy_document = PolicyDocument(policy_json) exclusions = Exclusions(exclusions_config) policy_finding = PolicyFinding(policy_document, exclusions) return policy_finding.results
def get_exclusions(exclusions_file: Optional[str] = None) -> Exclusions: """Get the exclusions configuration from a file""" # Get the exclusions configuration if exclusions_file: with open(exclusions_file, "r") as yaml_file: try: exclusions_cfg = yaml.safe_load(yaml_file) except yaml.YAMLError as exc: logger.critical(exc) exclusions = Exclusions(exclusions_cfg) else: exclusions = DEFAULT_EXCLUSIONS return exclusions
def missing_resource_constraints( self, exclusions: Exclusions = DEFAULT_EXCLUSIONS) -> List[str]: """Return a list of any actions - regardless of access level - allowed by the statement that do not leverage resource constraints.""" if not isinstance(exclusions, Exclusions): raise Exception( # pragma: no cover "The provided exclusions is not the Exclusions object type. " "Please use the Exclusions object.") actions_missing_resource_constraints = [] if len(self.resources) == 1 and self.resources[0] == "*": actions_missing_resource_constraints = self.restrictable_actions return exclusions.get_allowed_actions( actions_missing_resource_constraints)
def test_finding_actions_included(self): test_policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:GetObject", "ec2:DescribeInstances", # This is a bad thing to include, but just for the hell of it ], "Resource": "*" } ] } policy_document = PolicyDocument(test_policy) # (2) INCLUDE actions exclusions_cfg = { "users": ["obama"], "groups": ["admin"], "roles": ["MyRole"], "policies": ["someOtherName"], "include-actions": [ "ec2:DescribeInstances" ], "exclude-actions": [ "s3:GetObject", ], } exclusions = Exclusions(exclusions_cfg) finding = Finding( policy_name="MyPolicy", arn="arn:aws:iam::123456789012:group/SNSNotifications", actions=["s3:GetObject", "ec2:DescribeInstances"], policy_document=policy_document, exclusions=exclusions ) # print(finding.actions) expected_results = [ "ec2:DescribeInstances", ] self.assertListEqual(finding.actions, expected_results) group_finding = GroupFinding( policy_name="MyPolicy", arn="arn:aws:iam::123456789012:group/SNSNotifications", actions=["s3:GetObject", "ec2:DescribeInstances"], policy_document=policy_document, exclusions=exclusions, members=None ) self.assertListEqual(group_finding.actions, expected_results)
def scan_policy_file(input, exclusions_file, high_priority_only): # pragma: no cover """Scan a single policy file to identify missing resource constraints.""" file = input # Get the exclusions configuration with open(exclusions_file, "r") as yaml_file: try: exclusions_cfg = yaml.safe_load(yaml_file) except yaml.YAMLError as exc: logger.critical(exc) exclusions = Exclusions(exclusions_cfg) # Get the Policy with open(file) as json_file: logger.debug(f"Opening {file}") policy = json.load(json_file) policy_name = Path(file).stem # Run the scan and get the raw data. results = scan_policy(policy, policy_name, exclusions) # There will only be one finding in the results but it is in a list. results_exist = 0 for finding in results: if finding["PrivilegeEscalation"]: print(f"{RED}Issue found: Privilege Escalation{END}") results_exist += 1 for item in finding["PrivilegeEscalation"]: print(f"- Method: {item['type']}") print(f" Actions: {', '.join(item['PrivilegeEscalation'])}\n") if finding["DataExfiltrationActions"]: results_exist += 1 print(f"{RED}Issue found: Data Exfiltration{END}") print( f"{BOLD}Actions{END}: {', '.join(finding['DataExfiltrationActions'])}\n" ) if finding["PermissionsManagementActions"]: results_exist += 1 print(f"{RED}Issue found: Resource Exposure{END}") print( f"{BOLD}Actions{END}: {', '.join(finding['PermissionsManagementActions'])}\n" ) if not high_priority_only: results_exist += 1 print(f"{RED}Issue found: Unrestricted Infrastructure Modification{END}") print(f"{BOLD}Actions{END}: {', '.join(finding['Actions'])}") if results_exist == 0: print("There were no results found.")
def scan_policy( policy_json: Dict[str, Any], exclusions_config: Dict[str, List[str]] = DEFAULT_EXCLUSIONS_CONFIG, flag_conditional_statements: bool = False, flag_resource_arn_statements: bool = False, ) -> Dict[str, Any]: """ Scan a policy document for missing resource constraints. :param policy_json: Dictionary containing the IAM policy. :param exclusions_config: Exclusions configuration. If none, just send an empty dictionary. Defaults to the contents of cloudsplaining.shared.default-exclusions.yml :param flag_resource_arn_statements: :param flag_conditional_statements: :return: """ exclusions = Exclusions(exclusions_config) policy_document = PolicyDocument(policy_json, exclusions=exclusions, flag_resource_arn_statements=flag_resource_arn_statements, flag_conditional_statements=flag_conditional_statements) policy_finding = PolicyFinding(policy_document, exclusions) return policy_finding.results
def missing_resource_constraints( self, exclusions: Exclusions = DEFAULT_EXCLUSIONS) -> List[str]: """Return a list of any actions - regardless of access level - allowed by the statement that do not leverage resource constraints.""" if not isinstance(exclusions, Exclusions): raise Exception( # pragma: no cover "The provided exclusions is not the Exclusions object type. " "Please use the Exclusions object.") actions_missing_resource_constraints = [] if len(self.resources) == 1 and self.resources[0] == "*": actions_missing_resource_constraints = self.restrictable_actions # Fix #390 - if flag_resource_arn_statements is True, then let's treat this as missing resource constraints so we can flag the action anyway. elif self.flag_resource_arn_statements: actions_missing_resource_constraints = self.restrictable_actions else: pass result = exclusions.get_allowed_actions( actions_missing_resource_constraints) result.sort() return result
def test_policy_finding_for_data_exfiltration(self): test_policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:GetObject" ], "Resource": "*" } ] } policy_document = PolicyDocument(test_policy) # (1) If the user is a member of an excluded group, return True exclusions_cfg = dict( users=["obama"], groups=["exclude-group"], roles=["MyRole"], policies=["exclude-policy"] ) exclusions = Exclusions(exclusions_cfg) policy_finding = PolicyFinding(policy_document, exclusions) results = policy_finding.results expected_results = { "ServicesAffected": ["s3"], "PrivilegeEscalation": [], "ResourceExposure": [], "DataExfiltration": [ "s3:GetObject" ], "ServiceWildcard": [], "CredentialsExposure": [], "InfrastructureModification": [], } # print(json.dumps(results, indent=4)) self.assertDictEqual(results, expected_results)
def scan(input_file, exclusions_file, output, skip_open_report): # pragma: no cover """ Given the path to account authorization details files and the exclusions config file, scan all inline and managed policies in the account to identify actions that do not leverage resource constraints. """ if exclusions_file: # Get the exclusions configuration with open(exclusions_file, "r") as yaml_file: try: exclusions_cfg = yaml.safe_load(yaml_file) except yaml.YAMLError as exc: logger.critical(exc) exclusions = Exclusions(exclusions_cfg) else: exclusions = DEFAULT_EXCLUSIONS if os.path.isfile(input_file): account_name = Path(input_file).stem with open(input_file) as f: contents = f.read() account_authorization_details_cfg = json.loads(contents) rendered_html_report = scan_account_authorization_details( account_authorization_details_cfg, exclusions, account_name, output, write_data_files=True) html_output_file = os.path.join(output, f"iam-report-{account_name}.html") logger.info("Saving the report to %s", html_output_file) if os.path.exists(html_output_file): os.remove(html_output_file) with open(html_output_file, "w") as f: f.write(rendered_html_report) print(f"Wrote HTML results to: {html_output_file}") # Open the report by default if not skip_open_report: print("Opening the HTML report") url = "file://%s" % os.path.abspath(html_output_file) webbrowser.open(url, new=2) if os.path.isdir(input_file): logger.info( "The path given is a directory. Scanning for account authorization files and generating report." ) input_files = get_authorization_files_in_directory(input_file) for file in input_files: logger.info(f"Scanning file: {file}") with open(file) as f: contents = f.read() account_authorization_details_cfg = json.loads(contents) account_name = Path(file).stem # Scan the Account Authorization Details config rendered_html_report = scan_account_authorization_details( account_authorization_details_cfg, exclusions, account_name, output, write_data_files=True) html_output_file = os.path.join(output, f"iam-report-{account_name}.html") logger.info("Saving the report to %s", html_output_file) if os.path.exists(html_output_file): os.remove(html_output_file) with open(html_output_file, "w") as f: f.write(rendered_html_report) print(f"Wrote HTML results to: {html_output_file}") # Open the report by default if not skip_open_report: print("Opening the HTML report") url = "file://%s" % os.path.abspath(html_output_file) webbrowser.open(url, new=2)
def _is_excluded(self, exclusions: Exclusions) -> bool: """Determine whether the principal name or principal ID is excluded""" return (exclusions.is_principal_excluded(self.group_name, "Group") or exclusions.is_principal_excluded(self.group_id, "Group") or exclusions.is_principal_excluded(self.path, "Group"))
def __init__( self, group_detail: Dict[str, Any], policy_details: ManagedPolicyDetails, exclusions: Exclusions = DEFAULT_EXCLUSIONS, flag_conditional_statements: bool = False, flag_resource_arn_statements: bool = False, ): """ Initialize the GroupDetail object. :param group_detail: Details about a particular group :param policy_details: The ManagedPolicyDetails object - i.e., details about all managed policies in the account so the group can inherit those attributes """ self.create_date = group_detail.get("CreateDate") self.arn = group_detail.get("Arn") self.path = group_detail["Path"] self.group_id = group_detail["GroupId"] self.group_name = group_detail["GroupName"] # Fix Issue #254 - Allow flagging risky actions even when there are resource constraints self.flag_conditional_statements = flag_conditional_statements self.flag_resource_arn_statements = flag_resource_arn_statements if not isinstance(exclusions, Exclusions): raise Exception( "The exclusions provided is not an Exclusions type object. " "Please supply an Exclusions object and try again.") self.is_excluded = self._is_excluded(exclusions) # Inline Policies self.inline_policies = [] # If the group itself is NOT excluded, add its inline policies if not self.is_excluded: for policy_detail in group_detail.get("GroupPolicyList", []): policy_name = policy_detail.get("PolicyName") policy_document = policy_detail.get("PolicyDocument") policy_id = get_non_provider_id(json.dumps(policy_document)) if not (exclusions.is_policy_excluded(policy_name) or exclusions.is_policy_excluded(policy_id)): # NOTE: The Exclusions were not here before the #254 fix (which was an unfiled bug I just discovered) so the presence of this might break some older unit tests. Might need to fix that. inline_policy = InlinePolicy( policy_detail, exclusions=exclusions, flag_conditional_statements=flag_conditional_statements, flag_resource_arn_statements= flag_resource_arn_statements) self.inline_policies.append(inline_policy) # Managed Policies (either AWS-managed or Customer managed) self.attached_managed_policies = [] # If the group itself is NOT excluded, add its AWS-managed or Customer-managed policies if not self.is_excluded: for policy in group_detail.get("AttachedManagedPolicies", []): arn = policy.get("PolicyArn") if not (exclusions.is_policy_excluded(arn) or exclusions.is_policy_excluded( get_full_policy_path(arn)) or exclusions.is_policy_excluded(get_policy_name(arn))): attached_managed_policy_details = policy_details.get_policy_detail( arn) self.attached_managed_policies.append( attached_managed_policy_details)
def __init__( self, user_detail: Dict[str, Any], policy_details: ManagedPolicyDetails, all_group_details: GroupDetailList, exclusions: Exclusions = DEFAULT_EXCLUSIONS, flag_conditional_statements: bool = False, flag_resource_arn_statements: bool = False, ) -> None: """ Initialize the UserDetail object. :param user_detail: Details about a particular user :param policy_details: The ManagedPolicyDetails object - i.e., details about all managed policies in the account so the user can inherit those attributes :param all_group_details: """ self.create_date = user_detail.get("CreateDate") self.arn = user_detail.get("Arn") self.path = user_detail["Path"] self.user_id = user_detail["UserId"] self.user_name = user_detail["UserName"] if not isinstance(exclusions, Exclusions): raise Exception( "The exclusions provided is not an Exclusions type object. " "Please supply an Exclusions object and try again.") self.is_excluded = self._is_excluded(exclusions) # Fix Issue #254 - Allow flagging risky actions even when there are resource constraints self.flag_conditional_statements = flag_conditional_statements self.flag_resource_arn_statements = flag_resource_arn_statements # Groups self.groups: List[GroupDetail] = [] group_list = user_detail.get("GroupList") if group_list: self._add_group_details(group_list, all_group_details) # self.inline_policies = user_detail.get("UserPolicyList") # self.groups = user_detail.get("GroupList") # Inline Policies self.inline_policies = [] # If the user itself is NOT excluded, add its inline policies if not self.is_excluded: for policy_detail in user_detail.get("UserPolicyList", []): policy_name = policy_detail.get("PolicyName") policy_document = policy_detail.get("PolicyDocument") policy_id = get_non_provider_id(json.dumps(policy_document)) if not (exclusions.is_policy_excluded(policy_name) or exclusions.is_policy_excluded(policy_id)): inline_policy = InlinePolicy( policy_detail, exclusions=exclusions, flag_conditional_statements=flag_conditional_statements, flag_resource_arn_statements= flag_resource_arn_statements) self.inline_policies.append(inline_policy) # Managed Policies (either AWS-managed or Customer managed) self.attached_managed_policies = [] # If the user itself is NOT excluded, add its AWS-managed or Customer-managed policies if not self.is_excluded: for policy in user_detail.get("AttachedManagedPolicies", []): arn = policy.get("PolicyArn") if not (exclusions.is_policy_excluded(arn) or exclusions.is_policy_excluded( get_full_policy_path(arn)) or exclusions.is_policy_excluded(get_policy_name(arn))): attached_managed_policy_details = policy_details.get_policy_detail( arn) self.attached_managed_policies.append( attached_managed_policy_details)
def _is_excluded(self, exclusions: Exclusions) -> bool: """Determine whether the principal name or principal ID is excluded""" return bool( exclusions.is_principal_excluded(self.user_name, "User") or exclusions.is_principal_excluded(self.user_id, "User") or exclusions.is_principal_excluded(self.path, "User"))
"files", "new_data_file.json", )) with open(example_data_file, 'r') as json_file: expected_data_file = json.load(json_file) exclusions_cfg = { "policies": [ "AWSServiceRoleFor*", "*ServiceRolePolicy", "*ServiceLinkedRolePolicy", "AdministratorAccess", "service-role*", "aws-service-role*", "MyRole" ], "roles": ["service-role*", "aws-service-role*", "MyRole"], "users": ["obama"], "groups": [""], "include-actions": [ "s3:GetObject", "ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath", "secretsmanager:GetSecretValue", "rds:CopyDBSnapshot", "rds:CreateDBSnapshot" ], "exclude-actions": [""] } exclusions = Exclusions(exclusions_cfg) class TestNewDataFilePolicyDetail(unittest.TestCase): def test_new_principal_policy_mapping(self): authorization_details = AuthorizationDetails(cfg, exclusions) results = authorization_details.results # print(json.dumps(results)) self.assertDictEqual(expected_data_file, results)
def _is_excluded(self, exclusions: Exclusions) -> bool: """Determine whether the policy name or policy ID is excluded""" return (exclusions.is_policy_excluded(self.policy_name) or exclusions.is_policy_excluded(self.policy_id) or exclusions.is_policy_excluded(self.path) or is_name_excluded(self.path, "/aws-service-role*"))
def test_exclusions_for_service_roles(self): """test_exclusions_for_service_roles: Ensuring that exclusions config of service roles are specifically skipped, as designed""" authz_file = { "UserDetailList": [], "GroupDetailList": [], "RoleDetailList": [ { "Path": "/aws-service-role/cloudwatch-crossaccount.amazonaws.com/", "RoleName": "AWSServiceRoleForCloudWatchCrossAccount", "RoleId": "LALALALALAALALA", "Arn": "arn:aws:iam::115657980943:role/aws-service-role/cloudwatch-crossaccount.amazonaws.com/AWSServiceRoleForCloudWatchCrossAccount", "CreateDate": "2019-11-07 20:21:23+00:00", "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": { "Service": "cloudwatch-crossaccount.amazonaws.com" }, "Action": "sts:AssumeRole" }] }, "InstanceProfileList": [], "RolePolicyList": [], "AttachedManagedPolicies": [{ "PolicyName": "CloudWatch-CrossAccountAccess", "PolicyArn": "arn:aws:iam::aws:policy/aws-service-role/CloudWatch-CrossAccountAccess" }] }, ], "Policies": [{ "PolicyName": "CloudWatch-CrossAccountAccess", "PolicyId": "LALALALALAALALA", "Arn": "arn:aws:iam::aws:policy/aws-service-role/CloudWatch-CrossAccountAccess", # "Arn": "arn:aws:iam::aws:policy/Sup", "Path": "/aws-service-role/", "DefaultVersionId": "v1", "AttachmentCount": 1, "PermissionsBoundaryUsageCount": None, "IsAttachable": True, "CreateDate": "2019-07-23 09:59:27+00:00", "UpdateDate": "2019-07-23 09:59:27+00:00", "PolicyVersionList": [{ "Document": { "Version": "2012-10-17", # This is fake, I'm just trying to trigger a response "Statement": [{ "Action": ["iam:*"], "Resource": ["*"], "Effect": "Allow" }] }, "VersionId": "v1", "IsDefaultVersion": True, "CreateDate": "2019-07-23 09:59:27+00:00" }] }] } exclusions_cfg = {"policies": ["aws-service-role*"]} exclusions = Exclusions(exclusions_cfg) authorization_details = AuthorizationDetails(authz_file, exclusions) results = authorization_details.results expected_results = { "groups": {}, "users": {}, "roles": {}, "aws_managed_policies": {}, "customer_managed_policies": {}, "inline_policies": {}, "exclusions": { "policies": ["aws-service-role*"] }, "links": {} } self.assertDictEqual(results, expected_results)
def _is_excluded(self, exclusions: Exclusions) -> bool: """Determine whether the principal name or principal ID is excluded""" return (exclusions.is_principal_excluded(self.role_name, "Role") or exclusions.is_principal_excluded(self.role_id, "Role") or exclusions.is_principal_excluded(self.path, "Role") or is_name_excluded(self.path, "/aws-service-role*"))
def _is_excluded(self, exclusions: Exclusions) -> bool: """Determine whether the policy name or policy ID is excluded""" return bool( exclusions.is_policy_excluded(self.policy_name) or exclusions.is_policy_excluded(self.policy_id))
def scan_policy_file(input_file, exclusions_file, high_priority_only): # pragma: no cover """Scan a single policy file to identify missing resource constraints.""" if input_file: # Get the Policy with open(input_file) as json_file: logger.debug(f"Opening {input_file}") policy = json.load(json_file) policy_name = Path(input_file).stem else: try: policy = json.load(sys.stdin) except json.decoder.JSONDecodeError as j_e: logger.critical(j_e) sys.exit() policy_name = "StdinPolicy" # Get the exclusions configuration with open(exclusions_file, "r") as yaml_file: try: exclusions_cfg = yaml.safe_load(yaml_file) except yaml.YAMLError as exc: logger.critical(exc) exclusions = Exclusions(exclusions_cfg) # Run the scan and get the raw data. results = scan_policy(policy, policy_name, exclusions) # There will only be one finding in the results but it is in a list. results_exist = 0 if results: if results.get("PrivilegeEscalation"): print( f"{RED}Potential Issue found: Policy is capable of Privilege Escalation{END}" ) results_exist += 1 for item in results.get("PrivilegeEscalation"): print(f"- Method: {item.get('type')}") print( f" Actions: {', '.join(item.get('PrivilegeEscalation'))}\n" ) if results.get("DataExfiltrationActions"): results_exist += 1 print( f"{RED}Potential Issue found: Policy is capable of Data Exfiltration{END}" ) print( f"{BOLD}Actions{END}: {', '.join(results.get('DataExfiltrationActions'))}\n" ) if results.get("PermissionsManagementActions"): results_exist += 1 print( f"{RED}Potential Issue found: Policy is capable of Resource Exposure{END}" ) print( f"{BOLD}Actions{END}: {', '.join(results.get('PermissionsManagementActions'))}\n" ) if not high_priority_only: results_exist += 1 print( f"{RED}Potential Issue found: Policy is capable of Unrestricted Infrastructure Modification{END}" ) print(f"{BOLD}Actions{END}: {', '.join(results.get('Actions'))}") if results_exist == 0: print("There were no results found.") else: print("There were no results found.")
def test_new_findings(self): """output.new_findings.Findings""" self.maxDiff = None test_policy = { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "*" }] } policy_document = PolicyDocument(test_policy) # (1) If the user is a member of an excluded group, return True exclusions_cfg = dict(users=["obama"], groups=["exclude-group"], roles=["MyRole"], policies=["exclude-policy"]) exclusions = Exclusions(exclusions_cfg) attached_managed_policies = [{ "PolicyArn": "arn:aws:iam::aws:policy/AWSLambdaFullAccess", "PolicyName": "AWSLambdaFullAccess" }] # Let's just re-use the same policy for users groups and roles user_finding = UserFinding( policy_name="MyPolicy", arn="arn:aws:iam::123456789012:user/SomeUser", actions=["s3:GetObject"], policy_document=policy_document, group_membership=["admin"], attached_managed_policies=attached_managed_policies, exclusions=exclusions) group_finding = GroupFinding( policy_name="MyPolicy", arn="arn:aws:iam::123456789012:group/SomeGroup", actions=["s3:GetObject"], policy_document=policy_document, members=["obama"], exclusions=exclusions) trust_policy_from_compute_service_ecs_tasks = { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["sts:AssumeRole"], "Principal": { "Service": "ecs-tasks.amazonaws.com", "AWS": "arn:aws:iam::012345678910:root", } }] } assume_role_policy_document = AssumeRolePolicyDocument( trust_policy_from_compute_service_ecs_tasks) role_finding = RoleFinding( policy_name="MyPolicy", arn="arn:aws:iam::123456789012:role/SomeRole", actions=["s3:GetObject"], policy_document=policy_document, assume_role_policy_document=assume_role_policy_document, exclusions=exclusions) policy_finding = PolicyFinding( policy_name="AWSLambdaFullAccess", arn="arn:aws:iam::aws:policy/AWSLambdaFullAccess", actions=["s3:GetObject"], policy_document=policy_document, exclusions=exclusions) all_findings = Findings(exclusions) all_findings.add_user_finding(user_finding) result = all_findings.users[0] expected_user_result = { "AccountID": "123456789012", "ManagedBy": "Customer", "Name": "SomeUser", "PolicyName": "MyPolicy", "Type": "User", "Arn": "arn:aws:iam::123456789012:user/SomeUser", "ActionsCount": 1, "ServicesCount": 1, "Services": ["s3"], "Actions": ["s3:GetObject"], "PolicyDocument": { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "*" }] }, "AssumeRolePolicyDocument": None, "AssumableByComputeService": [], "PrivilegeEscalation": [], "DataExfiltrationActions": ["s3:GetObject"], "PermissionsManagementActions": [] } self.assertDictEqual(result.json, expected_user_result) principal_policy_mapping = [ { "Principal": "SomeUser", "Type": "User", "PolicyType": "Managed", "ManagedBy": "AWS", "PolicyName": "MyPolicy", "GroupMembership": None, }, { "Principal": "SomeUser", "Type": "User", "PolicyType": "Managed", "ManagedBy": "AWS", "PolicyName": "AWSLambdaFullAccess", "GroupMembership": None, }, { "Principal": "SomeGroup", "Type": "Group", "PolicyType": "Managed", "ManagedBy": "AWS", "PolicyName": "MyPolicy", "GroupMembership": None, }, { "Principal": "SomeRole", "Type": "Role", "PolicyType": "Managed", "ManagedBy": "AWS", "PolicyName": "MyPolicy", "GroupMembership": None, }, ] all_findings.principal_policy_mapping = principal_policy_mapping all_findings.add_group_finding(group_finding) all_findings.add_role_finding(role_finding) all_findings.add_policy_finding(policy_finding) print(len(all_findings)) self.assertEqual(len(all_findings), 4) results = all_findings.json expected_results = [{ "AccountID": "N/A", "ManagedBy": "AWS", "Name": "AWSLambdaFullAccess", "PolicyName": "AWSLambdaFullAccess", "Type": "Policy", "Arn": "arn:aws:iam::aws:policy/AWSLambdaFullAccess", "ActionsCount": 1, "ServicesCount": 1, "Services": ["s3"], "Actions": ["s3:GetObject"], "PolicyDocument": { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "*" }] }, "AssumeRolePolicyDocument": None, "AssumableByComputeService": [], "PrivilegeEscalation": [], "DataExfiltrationActions": ["s3:GetObject"], "PermissionsManagementActions": [] }, { "AccountID": "123456789012", "ManagedBy": "Customer", "Name": "SomeUser", "PolicyName": "MyPolicy", "Type": "User", "Arn": "arn:aws:iam::123456789012:user/SomeUser", "ActionsCount": 1, "ServicesCount": 1, "Services": ["s3"], "Actions": ["s3:GetObject"], "PolicyDocument": { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "*" }] }, "AssumeRolePolicyDocument": None, "AssumableByComputeService": [], "PrivilegeEscalation": [], "DataExfiltrationActions": ["s3:GetObject"], "PermissionsManagementActions": [] }, { "AccountID": "123456789012", "ManagedBy": "Customer", "Name": "SomeGroup", "PolicyName": "MyPolicy", "Type": "Group", "Arn": "arn:aws:iam::123456789012:group/SomeGroup", "ActionsCount": 1, "ServicesCount": 1, "Services": ["s3"], "Actions": ["s3:GetObject"], "PolicyDocument": { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "*" }] }, "AssumeRolePolicyDocument": None, "AssumableByComputeService": [], "PrivilegeEscalation": [], "DataExfiltrationActions": ["s3:GetObject"], "PermissionsManagementActions": [] }, { "AccountID": "123456789012", "ManagedBy": "Customer", "Name": "SomeRole", "PolicyName": "MyPolicy", "Type": "Role", "Arn": "arn:aws:iam::123456789012:role/SomeRole", "ActionsCount": 1, "ServicesCount": 1, "Services": ["s3"], "Actions": ["s3:GetObject"], "PolicyDocument": { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "*" }] }, "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["sts:AssumeRole"], "Principal": { "Service": "ecs-tasks.amazonaws.com", "AWS": "arn:aws:iam::012345678910:root" } }] }, "AssumableByComputeService": ["ecs-tasks"], "PrivilegeEscalation": [], "DataExfiltrationActions": ["s3:GetObject"], "PermissionsManagementActions": [] }] # print(json.dumps(all_findings.json, indent=4)) self.assertListEqual(results, expected_results)