diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e0fd091..f8844012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to this project will be documented in this file. ## [1.15.0] ### Additions -- New rules: `StackNameMatchesRegexRule` and `StorageEncryptedRule` +- New rules: `PublicELBCheckerRule`, `StackNameMatchesRegexRule`, and `StorageEncryptedRule` - New regex: `REGEX_ALPHANUMERICAL_OR_HYPHEN` to check if stack name only consists of alphanumerical characters and hyphens. ## [1.14.0] diff --git a/cfripper/rules/__init__.py b/cfripper/rules/__init__.py index 46e4d5d2..6e0d0894 100644 --- a/cfripper/rules/__init__.py +++ b/cfripper/rules/__init__.py @@ -23,6 +23,7 @@ from cfripper.rules.managed_policy_on_user import ManagedPolicyOnUserRule from cfripper.rules.policy_on_user import PolicyOnUserRule from cfripper.rules.privilege_escalation import PrivilegeEscalationRule +from cfripper.rules.public_elb_checker_rule import PublicELBCheckerRule from cfripper.rules.rds_security_group import RDSSecurityGroupIngressOpenToWorldRule from cfripper.rules.s3_bucket_policy import S3BucketPolicyPrincipalRule from cfripper.rules.s3_lifecycle_configuration import S3LifecycleConfigurationRule @@ -80,6 +81,7 @@ PartialWildcardPrincipalRule, PolicyOnUserRule, PrivilegeEscalationRule, + PublicELBCheckerRule, RDSSecurityGroupIngressOpenToWorldRule, S3BucketPolicyPrincipalRule, S3LifecycleConfigurationRule, diff --git a/cfripper/rules/public_elb_checker_rule.py b/cfripper/rules/public_elb_checker_rule.py new file mode 100644 index 00000000..4f8a159d --- /dev/null +++ b/cfripper/rules/public_elb_checker_rule.py @@ -0,0 +1,41 @@ +from typing import Dict, Optional + +from pycfmodel.model.resources.generic_resource import GenericResource + +from cfripper.model.enums import RuleGranularity, RuleMode, RuleRisk +from cfripper.model.result import Result +from cfripper.rules.base_rules import ResourceSpecificRule + + +class PublicELBCheckerRule(ResourceSpecificRule): + """ + Rule to check if a public facing ELB is being created. + """ + + RESOURCE_TYPES = (GenericResource,) + ELB_RESOURCE_TYPES = ["AWS::ElasticLoadBalancing::LoadBalancer", "AWS::ElasticLoadBalancingV2::LoadBalancer"] + RISK_VALUE = RuleRisk.LOW + RULE_MODE = RuleMode.BLOCKING + REASON = "Creation of public facing ELBs is restricted. LogicalId: {}" + + def resource_invoke(self, resource: GenericResource, logical_id: str, extras: Optional[Dict] = None) -> Result: + result = Result() + if resource.Type in self.ELB_RESOURCE_TYPES: + elb_scheme = getattr(resource.Properties, "Scheme", "internal") + + if elb_scheme == "internet-facing": + self.add_failure_to_result( + result=result, + reason=self.REASON.format(logical_id), + resource_ids={logical_id}, + resource_types={resource.Type}, + context={ + "config": self._config, + "extras": extras, + "logical_id": logical_id, + "resource": resource, + }, + granularity=RuleGranularity.RESOURCE, + ) + + return result diff --git a/tests/rules/test_PublicELBCheckerRule.py b/tests/rules/test_PublicELBCheckerRule.py new file mode 100644 index 00000000..5b195251 --- /dev/null +++ b/tests/rules/test_PublicELBCheckerRule.py @@ -0,0 +1,58 @@ +import pytest + +from cfripper.model.result import Failure +from cfripper.rules.public_elb_checker_rule import PublicELBCheckerRule +from tests.utils import get_cfmodel_from + + +@pytest.mark.parametrize( + "template", + [ + "rules/PublicELBCheckerRule/private_elb_instance.yml", + "rules/PublicELBCheckerRule/private_elb_v2_instance.yml", + ], +) +def test_invoke_private_elbs_passes(template): + rule = PublicELBCheckerRule(None) + rule._config.stack_name = "stackname" + result = rule.invoke(cfmodel=get_cfmodel_from(template).resolve()) + + assert result.valid + assert result.failures == [] + + +@pytest.mark.parametrize( + "template, logical_id, resource_type, reason", + [ + ( + "rules/PublicELBCheckerRule/public_facing_elb_instance.yml", + "PublicLoadBalancer", + "AWS::ElasticLoadBalancing::LoadBalancer", + "Creation of public facing ELBs is restricted. LogicalId: PublicLoadBalancer", + ), + ( + "rules/PublicELBCheckerRule/public_facing_elb_v2_instance.yml", + "PublicV2LoadBalancer", + "AWS::ElasticLoadBalancingV2::LoadBalancer", + "Creation of public facing ELBs is restricted. LogicalId: PublicV2LoadBalancer", + ), + ], +) +def test_invoke_public_elbs_fail(template, logical_id, resource_type, reason): + rule = PublicELBCheckerRule(None) + rule._config.stack_name = "stackname" + result = rule.invoke(cfmodel=get_cfmodel_from(template).resolve()) + + assert result.valid is False + assert result.failures == [ + Failure( + granularity="RESOURCE", + reason=reason, + risk_value="LOW", + rule="PublicELBCheckerRule", + rule_mode="BLOCKING", + actions=None, + resource_ids={logical_id}, + resource_types={resource_type}, + ) + ] diff --git a/tests/test_templates/rules/PublicELBCheckerRule/private_elb_instance.yml b/tests/test_templates/rules/PublicELBCheckerRule/private_elb_instance.yml new file mode 100644 index 00000000..5f8afe07 --- /dev/null +++ b/tests/test_templates/rules/PublicELBCheckerRule/private_elb_instance.yml @@ -0,0 +1,32 @@ +Resources: + PublicLoadBalancer: + Type: 'AWS::ElasticLoadBalancing::LoadBalancer' + Properties: + Name: 'AWS::StackName-extlb' + Scheme: internal + SecurityGroups: + - !GetAtt + - LoadBalancerHttpsSG + - GroupId + Subnets: !If + - ExtLoadBalancer + - - !ImportValue PublicSubnetA + - !ImportValue PublicSubnetB + - !ImportValue PublicSubnetC + - - !ImportValue PrivateSubnetA + - !ImportValue PrivateSubnetB + - !ImportValue PrivateSubnetC + ConnectionSettings: + - IdleTimeout: 3600 + Tags: + - Key: Name + Value: !Join + - '-' + - - !Ref 'AWS::StackName' + - LoadBalancerv2 + - Key: Project + Value: !Ref ProjectName + - Key: Contact + Value: !Ref ContactEmail + - Key: StackName + Value: !Ref 'AWS::StackName' \ No newline at end of file diff --git a/tests/test_templates/rules/PublicELBCheckerRule/private_elb_v2_instance.yml b/tests/test_templates/rules/PublicELBCheckerRule/private_elb_v2_instance.yml new file mode 100644 index 00000000..6c6da07c --- /dev/null +++ b/tests/test_templates/rules/PublicELBCheckerRule/private_elb_v2_instance.yml @@ -0,0 +1,34 @@ +Resources: + PublicV2LoadBalancer: + Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer' + Properties: + Name: 'AWS::StackName-extlb' + Scheme: internal + SecurityGroups: + - !GetAtt + - LoadBalancerHttpsSG + - GroupId + Subnets: !If + - ExtLoadBalancer + - - !ImportValue PublicSubnetA + - !ImportValue PublicSubnetB + - !ImportValue PublicSubnetC + - - !ImportValue PrivateSubnetA + - !ImportValue PrivateSubnetB + - !ImportValue PrivateSubnetC + Type: application + LoadBalancerAttributes: + - Key: idle_timeout.timeout_seconds + Value: '3600' + Tags: + - Key: Name + Value: !Join + - '-' + - - !Ref 'AWS::StackName' + - LoadBalancerv2 + - Key: Project + Value: !Ref ProjectName + - Key: Contact + Value: !Ref ContactEmail + - Key: StackName + Value: !Ref 'AWS::StackName' \ No newline at end of file diff --git a/tests/test_templates/rules/PublicELBCheckerRule/public_facing_elb_instance.yml b/tests/test_templates/rules/PublicELBCheckerRule/public_facing_elb_instance.yml new file mode 100644 index 00000000..5818b3ee --- /dev/null +++ b/tests/test_templates/rules/PublicELBCheckerRule/public_facing_elb_instance.yml @@ -0,0 +1,32 @@ +Resources: + PublicLoadBalancer: + Type: 'AWS::ElasticLoadBalancing::LoadBalancer' + Properties: + Name: 'AWS::StackName-extlb' + Scheme: internet-facing + SecurityGroups: + - !GetAtt + - LoadBalancerHttpsSG + - GroupId + Subnets: !If + - ExtLoadBalancer + - - !ImportValue PublicSubnetA + - !ImportValue PublicSubnetB + - !ImportValue PublicSubnetC + - - !ImportValue PrivateSubnetA + - !ImportValue PrivateSubnetB + - !ImportValue PrivateSubnetC + ConnectionSettings: + - IdleTimeout: 3600 + Tags: + - Key: Name + Value: !Join + - '-' + - - !Ref 'AWS::StackName' + - LoadBalancerv2 + - Key: Project + Value: !Ref ProjectName + - Key: Contact + Value: !Ref ContactEmail + - Key: StackName + Value: !Ref 'AWS::StackName' \ No newline at end of file diff --git a/tests/test_templates/rules/PublicELBCheckerRule/public_facing_elb_v2_instance.yml b/tests/test_templates/rules/PublicELBCheckerRule/public_facing_elb_v2_instance.yml new file mode 100644 index 00000000..6bef83ac --- /dev/null +++ b/tests/test_templates/rules/PublicELBCheckerRule/public_facing_elb_v2_instance.yml @@ -0,0 +1,34 @@ +Resources: + PublicV2LoadBalancer: + Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer' + Properties: + Name: 'AWS::StackName-extlb' + Scheme: internet-facing + SecurityGroups: + - !GetAtt + - LoadBalancerHttpsSG + - GroupId + Subnets: !If + - ExtLoadBalancer + - - !ImportValue PublicSubnetA + - !ImportValue PublicSubnetB + - !ImportValue PublicSubnetC + - - !ImportValue PrivateSubnetA + - !ImportValue PrivateSubnetB + - !ImportValue PrivateSubnetC + Type: application + LoadBalancerAttributes: + - Key: idle_timeout.timeout_seconds + Value: '3600' + Tags: + - Key: Name + Value: !Join + - '-' + - - !Ref 'AWS::StackName' + - LoadBalancerv2 + - Key: Project + Value: !Ref ProjectName + - Key: Contact + Value: !Ref ContactEmail + - Key: StackName + Value: !Ref 'AWS::StackName' \ No newline at end of file