def test_stack_whitelist_joins_all_whitelisted_matching_stack_names(self): mock_whitelist = { "stackname": [ "S3CrossAccountTrustRule", ], "notstackname": [ "IAMRolesOverprivilegedRule", ], "stackname_withmorethings": [ "CrossAccountTrustRule", "ManagedPolicyOnUserRule", ] } config = Config( project_name="project_mock", service_name="service_mock", stack_name="stackname_withmorethings", stack_whitelist=mock_whitelist, rules=DEFAULT_RULES.keys(), ) whitelisted_rules = config.get_whitelisted_rules() assert set(whitelisted_rules) == { "CrossAccountTrustRule", "ManagedPolicyOnUserRule", "S3CrossAccountTrustRule", }
def test_load_filters_file_invalid_file(test_files_location): mock_rules = [ "RuleThatUsesResourceAllowlist", "SecurityGroupOpenToWorldRule" ] config = Config(stack_name="test_stack", rules=mock_rules) with pytest.raises(ValidationError): config.add_filters_from_dir(f"{test_files_location}/invalid_filters")
def test_load_rules_config_file_no_file(test_files_location): mock_rules = [ "RuleThatUsesResourceAllowlist", "SecurityGroupOpenToWorldRule" ] config = Config(stack_name="test_stack", rules=mock_rules) with pytest.raises(FileNotFoundError): config.load_rules_config_file( open(f"{test_files_location}/config/non_existing_file.py"))
def test_load_rules_config_file_invalid_file(test_files_location): mock_rules = [ "RuleThatUsesResourceAllowlist", "SecurityGroupOpenToWorldRule" ] config = Config(stack_name="test_stack", rules=mock_rules) with pytest.raises(ValidationError): config.load_rules_config_file( open(f"{test_files_location}/config/rules_config_invalid.py"))
def test_stack_to_resource_whitelist_rule_not_in_whitelist(self, mock_rule_to_resource_whitelist): mock_rules = ["RuleThatUsesResourceWhitelists", "SecurityGroupOpenToWorldRule"] config = Config( stack_name="test_stack", rules=mock_rules, stack_whitelist={}, rule_to_resource_whitelist=mock_rule_to_resource_whitelist ) assert config.get_whitelisted_resources("SecurityGroupOpenToWorldRule") == []
def test_stack_to_action_whitelist_stack_without_resources( mock_rule_to_action_whitelist): mock_rules = [ "RuleThatUsesResourceWhitelists", "SecurityGroupOpenToWorldRule" ] config = Config( stack_name="stack_without_whitelisted_resources", rules=mock_rules, stack_whitelist={}, rule_to_action_whitelist=mock_rule_to_action_whitelist, ) assert config.get_whitelisted_actions("SecurityGroupOpenToWorldRule") == []
def test_stack_to_action_whitelist_normal_behavior(self, mock_rule_to_action_whitelist): mock_rules = ["RuleThatUsesResourceWhitelists", "SecurityGroupOpenToWorldRule"] config = Config( stack_name="stack_2", rules=mock_rules, stack_whitelist={}, rule_to_action_whitelist=mock_rule_to_action_whitelist ) assert config.get_whitelisted_actions("RuleThatUsesActionWhitelists") == [ "s3:GetItem", "kms:*", "dynamodb:CreateTable", ]
def test_stack_to_resource_whitelist_normal_behavior(self, mock_rule_to_resource_whitelist): mock_rules = ["RuleThatUsesResourceWhitelists", "SecurityGroupOpenToWorldRule"] config = Config( stack_name="test_stack", rules=mock_rules, stack_whitelist={}, rule_to_resource_whitelist=mock_rule_to_resource_whitelist ) assert config.get_whitelisted_resources("RuleThatUsesResourceWhitelists") == [ "resource_5", "resource_1", "another_resource", ]
def test_no_failures_are_raised(good_template): rule = EBSVolumeHasSSERule(Config(aws_account_id="123456789")) result = rule.invoke(good_template) assert result.valid assert len(result.failed_rules) == 0 assert len(result.failed_monitored_rules) == 0
def test_generic_cross_account_rule_for_resources_with_set_principals( template, is_valid, failures): rule = GenericCrossAccountTrustRule( Config(aws_account_id="123456789", aws_principals=["999999999"])) result = rule.invoke(template) assert result.valid == is_valid assert compare_lists_of_failures(result.failures, failures)
def test_generic_cross_account_for_opensearch_domain_different_principals( principal): rule = GenericCrossAccountTrustRule( Config(aws_account_id="123456789", aws_principals=["999999999"])) model = get_cfmodel_from( "rules/CrossAccountTrustRule/opensearch_domain_basic.yml").resolve( extra_params={"Principal": principal}) result = rule.invoke(model) assert not result.valid assert compare_lists_of_failures( result.failures, [ Failure( granularity=RuleGranularity.RESOURCE, reason= f"TestDomain has forbidden cross-account with {principal}", risk_value=RuleRisk.MEDIUM, rule="GenericCrossAccountTrustRule", rule_mode=RuleMode.BLOCKING, actions=None, resource_ids={"TestDomain"}, resource_types={"AWS::OpenSearchService::Domain"}, ) ], )
def test_s3_bucket_cross_account_from_aws_service_with_generic( s3_bucket_cross_account_from_aws_service): rule = GenericCrossAccountTrustRule(Config(aws_account_id="123456789")) result = rule.invoke(s3_bucket_cross_account_from_aws_service) assert result.valid assert compare_lists_of_failures(result.failures, [])
def test_sts_valid(template_valid_with_sts): rule = KMSKeyCrossAccountTrustRule(Config(aws_account_id="123456789", aws_principals=["999999999"])) result = rule.invoke(template_valid_with_sts) assert result.valid assert len(result.failed_rules) == 0 assert len(result.failed_monitored_rules) == 0
def test_canonical_id_is_not_blocked(template_valid_with_canonical_id): rule = CrossAccountTrustRule(Config()) result = rule.invoke(template_valid_with_canonical_id) assert result.valid assert len(result.failed_rules) == 0 assert len(result.failed_monitored_rules) == 0
def test_filter_do_not_report_anything(single_security_group_one_cidr_ingress): mock_config = Config( rules=["EC2SecurityGroupMissingEgressRule"], aws_account_id="123456789", stack_name="mockstack", rules_config={ "EC2SecurityGroupMissingEgressRule": RuleConfig(filters=[ Filter( rule_mode=RuleMode.WHITELISTED, eval={"eq": [{ "ref": "config.stack_name" }, "mockstack"]}, ) ], ) }, ) rules = [ DEFAULT_RULES.get(rule)(mock_config) for rule in mock_config.rules ] processor = RuleProcessor(*rules) result = processor.process_cf_template( single_security_group_one_cidr_ingress, mock_config) assert result.valid
def remove_failures_of_whitelisted_resources(config: Config, result: Result): if not result.failed_rules: return clean_failures = [] for failure in result.failed_rules: if failure.granularity != RuleGranularity.RESOURCE: clean_failures.append(failure) continue if not failure.resource_ids: logger.warning(f"Failure with resource granularity doesn't have resources: {failure}") continue whitelisted_resources = { resource for resource in failure.resource_ids if any( [ re.match(whitelisted_resource_regex, resource) for whitelisted_resource_regex in config.get_whitelisted_resources(failure.rule) ] ) } failure.resource_ids = failure.resource_ids - whitelisted_resources if failure.resource_ids: clean_failures.append(failure) result.failed_rules = clean_failures
def default_allow_all_config(): return Config( rules=DEFAULT_RULES, aws_account_id="123456789012", stack_name="mockstack", rules_filters=[ Filter( rule_mode=RuleMode.ALLOWED, eval={ "and": [ { "exists": { "ref": "config.stack_name" } }, { "eq": [{ "ref": "config.stack_name" }, "mockstack"] }, ] }, rules=set(DEFAULT_RULES.keys()), ), ], )
def test_remove_failures_from_whitelisted_actions_failure_no_actions_is_removed( mock_logger, mock_rule_to_action_whitelist): config = Config( stack_name="teststack", rules=["S3CrossAccountTrustRule"], rule_to_action_whitelist=mock_rule_to_action_whitelist, ) result = Result() failure = Failure( rule="S3CrossAccountTrustRule", reason= "rolething has forbidden cross-account policy allow with 123456789 for an S3 bucket.", rule_mode=RuleMode.BLOCKING, risk_value=RuleRisk.HIGH, actions=set(), granularity=RuleGranularity.ACTION, ) result.failed_rules = [failure] RuleProcessor.remove_failures_of_whitelisted_actions(config=config, result=result) assert result.failed_rules == [] mock_logger.assert_called_once_with( f"Failure with action granularity doesn't have actions: {failure}")
def test_s3_bucket_cross_account_from_aws_service(s3_bucket_cross_account_from_aws_service): rule = S3CrossAccountTrustRule(Config(aws_account_id="123456789")) result = rule.invoke(s3_bucket_cross_account_from_aws_service) assert result.valid assert len(result.failed_rules) == 0 assert len(result.failed_monitored_rules) == 0
def test_remove_failures_from_whitelisted_resources_only_removes_resource_granularity(mock_rule_to_resource_whitelist): config = Config( stack_name="otherstack", rules=["S3CrossAccountTrustRule"], rule_to_resource_whitelist=mock_rule_to_resource_whitelist, ) result = Result() failed_rules = [ { "rule": "S3CrossAccountTrustRule", "reason": "rolething has forbidden cross-account policy allow with 123456789 for an S3 bucket.", "rule_mode": RuleMode.BLOCKING, "risk_value": RuleRisk.HIGH, "resource_ids": {"rolething"}, "actions": None, "granularity": RuleGranularity.ACTION, }, { "rule": "S3CrossAccountTrustRule", "reason": "anotherthing has forbidden cross-account policy allow with 123456789 for an S3 bucket.", "rule_mode": RuleMode.BLOCKING, "risk_value": RuleRisk.HIGH, "resource_ids": {"anotherthing"}, "actions": None, "granularity": RuleGranularity.RESOURCE, } ] result.failed_rules = failed_rules RuleProcessor.remove_failures_of_whitelisted_resources(config=config, result=result) assert result.failed_rules == failed_rules
def test_report_format_is_the_one_expected(template_one_role): result = Result() rule = CrossAccountTrustRule(Config(aws_account_id="123456789"), result) rule.invoke(template_one_role) assert not result.valid assert result.failed_rules == [ Failure( rule="CrossAccountTrustRule", reason= "RootRole has forbidden cross-account trust relationship with arn:aws:iam::123456789:root", rule_mode=RuleMode.BLOCKING, risk_value=RuleRisk.MEDIUM, resource_ids={"RootRole"}, actions=set(), granularity=RuleGranularity.RESOURCE, ), Failure( rule="CrossAccountTrustRule", reason= ("RootRole has forbidden cross-account trust relationship with arn:aws:iam::999999999:role/" "*****@*****.**"), rule_mode=RuleMode.BLOCKING, risk_value=RuleRisk.MEDIUM, resource_ids={"RootRole"}, actions=set(), granularity=RuleGranularity.RESOURCE, ), ]
def test_failures_for_correct_account_ids(intra_account_root_access): rule = GenericResourcePartialWildcardPrincipalRule( Config(aws_account_id="123456789012")) result = rule.invoke(intra_account_root_access) assert not result.valid assert compare_lists_of_failures( result.failures, [ Failure( granularity=RuleGranularity.RESOURCE, reason= "AccLoadBalancerAccessLogBucketPolicy should not allow wildcard in principals or account-wide principals (principal: 'arn:aws:iam::123456789012:root')", risk_value=RuleRisk.MEDIUM, rule="GenericResourcePartialWildcardPrincipalRule", rule_mode=RuleMode.BLOCKING, actions=None, resource_ids={"AccLoadBalancerAccessLogBucketPolicy"}, resource_types={"AWS::S3::BucketPolicy"}, ), Failure( granularity=RuleGranularity.RESOURCE, reason= "AccLoadBalancerAccessLogBucketPolicy should not allow wildcard in principals or account-wide principals (principal: '987654321012')", risk_value=RuleRisk.MEDIUM, rule="GenericResourcePartialWildcardPrincipalRule", rule_mode=RuleMode.BLOCKING, actions=None, resource_ids={"AccLoadBalancerAccessLogBucketPolicy"}, resource_types={"AWS::S3::BucketPolicy"}, ), ], )
def test_failures_are_raised(bad_template): rule = EC2SecurityGroupIngressOpenToWorldRule(Config()) result = rule.invoke(bad_template) assert not result.valid assert compare_lists_of_failures( result.failures, [ Failure( granularity=RuleGranularity.RESOURCE, reason= "Port(s) 46 open to public IPs: (11.0.0.0/8) in security group 'securityGroupIngress1'", risk_value=RuleRisk.MEDIUM, rule="EC2SecurityGroupIngressOpenToWorldRule", rule_mode=RuleMode.BLOCKING, actions=None, resource_ids={"securityGroupIngress1"}, resource_types={"AWS::EC2::SecurityGroupIngress"}, ), Failure( granularity=RuleGranularity.RESOURCE, reason= "Port(s) 46 open to public IPs: (::/0) in security group 'securityGroupIngress2'", risk_value=RuleRisk.MEDIUM, rule="EC2SecurityGroupIngressOpenToWorldRule", rule_mode=RuleMode.BLOCKING, actions=None, resource_ids={"securityGroupIngress2"}, resource_types={"AWS::EC2::SecurityGroupIngress"}, ), ], )
def test_action_whitelist_keeps_non_whitelisted_actions(): whitelist_for_all_stacks = {"MockRule": {".*": {"s3:List"}}} config = Config(stack_name="abcd", rules=["MockRule"], rule_to_action_whitelist=whitelist_for_all_stacks) result = Result() failed_rules = [ Failure( rule="MockRule", reason="MockRule is invalid for some actions", rule_mode=RuleMode.BLOCKING, risk_value=RuleRisk.HIGH, actions={"s3:ListBucket", "s3:GetBucket"}, granularity=RuleGranularity.ACTION, ) ] result.failed_rules = failed_rules RuleProcessor.remove_failures_of_whitelisted_actions(config=config, result=result) assert result.failed_rules == [ Failure( rule="MockRule", reason="MockRule is invalid for some actions", rule_mode=RuleMode.BLOCKING, risk_value=RuleRisk.HIGH, actions={"s3:GetBucket"}, granularity=RuleGranularity.ACTION, ) ]
def test_non_matching_filters_are_reported_normally( single_security_group_one_cidr_ingress): mock_config = Config( rules=["EC2SecurityGroupMissingEgressRule"], aws_account_id="123456789", stack_name="mockstack", rules_config={ "EC2SecurityGroupMissingEgressRule": RuleConfig(filters=[ Filter(rule_mode=RuleMode.WHITELISTED, eval={ "eq": [{ "ref": "config.stack_name" }, "anotherstack"] }) ], ) }, ) rules = [ DEFAULT_RULES.get(rule)(mock_config) for rule in mock_config.rules ] processor = RuleProcessor(*rules) result = processor.process_cf_template( single_security_group_one_cidr_ingress, mock_config) assert result.valid assert len(result.failed_rules) == 0 assert len(result.failed_monitored_rules) == 1 assert result.failed_monitored_rules[ 0].rule == "EC2SecurityGroupMissingEgressRule" assert ( result.failed_monitored_rules[0].reason == "Missing egress rule in sg means all traffic is allowed outbound. Make this explicit if it is desired configuration" )
def test_s3_bucket_cross_account_for_current_account_with_generic( s3_bucket_cross_account): rule = GenericCrossAccountTrustRule(Config(aws_account_id="987654321")) result = rule.invoke(s3_bucket_cross_account) assert result.valid assert compare_lists_of_failures(result.failures, [])
def test_kms_cross_account_success(principal): rule = KMSKeyCrossAccountTrustRule( Config(aws_account_id="123456789", aws_principals=["999999999"])) model = get_cfmodel_from("rules/CrossAccountTrustRule/kms_basic.yml" ).resolve(extra_params={"Principal": principal}) result = rule.invoke(model) assert result.valid
def test_filter_works_as_expected(template_two_roles_dict, expected_result_two_roles): config = Config( rules=["CrossAccountTrustRule"], aws_account_id="123456789", stack_name="mockstack", rules_config={ "CrossAccountTrustRule": RuleConfig( filters=[ Filter( rule_mode=RuleMode.WHITELISTED, eval={ "and": [ {"eq": [{"ref": "config.stack_name"}, "mockstack"]}, {"eq": [{"ref": "logical_id"}, "RootRoleOne"]}, ] }, ) ], ) }, ) rules = [DEFAULT_RULES.get(rule)(config) for rule in config.rules] processor = RuleProcessor(*rules) result = processor.process_cf_template(template_two_roles_dict, config) assert not result.valid assert result.failed_rules[0] == expected_result_two_roles[-1]
def remove_failures_of_whitelisted_actions(config: Config, result: Result): if not result.failed_rules: return clean_failures = [] for failure in result.failed_rules: if failure.granularity != RuleGranularity.ACTION: clean_failures.append(failure) continue if not failure.actions: logger.warning(f"Failure with action granularity doesn't have actions: {failure}") continue whitelisted_actions = { action for action in failure.actions if any( [ re.match(whitelisted_action_regex, action) for whitelisted_action_regex in config.get_whitelisted_actions(failure.rule) ] ) } failure.actions = failure.actions - whitelisted_actions if failure.actions: clean_failures.append(failure) result.failed_rules = clean_failures
def test_only_whitelisted_resources_are_removed(mock_rule_to_resource_whitelist): config = Config( stack_name="otherstack", rules=["S3CrossAccountTrustRule"], rule_to_resource_whitelist=mock_rule_to_resource_whitelist, ) result = Result() failed_rules = [ Failure( rule="S3CrossAccountTrustRule", reason="Forbidden cross-account policy allow with 123456789 for an S3 bucket.", rule_mode=RuleMode.BLOCKING, risk_value=RuleRisk.HIGH, resource_ids={"rolething", "thenotwhitelistedthing", "anotherone"}, actions=None, granularity=RuleGranularity.RESOURCE, ) ] result.failed_rules = failed_rules RuleProcessor.remove_failures_of_whitelisted_resources(config=config, result=result) assert result.failed_rules == [ Failure( rule="S3CrossAccountTrustRule", reason="Forbidden cross-account policy allow with 123456789 for an S3 bucket.", rule_mode=RuleMode.BLOCKING, risk_value=RuleRisk.HIGH, resource_ids={"thenotwhitelistedthing", "anotherone"}, actions=None, granularity=RuleGranularity.RESOURCE, ) ]