diff --git a/CHANGELOG.md b/CHANGELOG.md index 34dd0e8f..0e0fd091 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog All notable changes to this project will be documented in this file. +## [1.15.0] +### Additions +- New rules: `StackNameMatchesRegexRule` and `StorageEncryptedRule` +- New regex: `REGEX_ALPHANUMERICAL_OR_HYPHEN` to check if stack name only consists of alphanumerical characters and hyphens. + ## [1.14.0] ### Additions - `Config` includes a metrics logger, and it is called to register when a filter is used diff --git a/cfripper/rules/__init__.py b/cfripper/rules/__init__.py index 23970fad..46e4d5d2 100644 --- a/cfripper/rules/__init__.py +++ b/cfripper/rules/__init__.py @@ -39,6 +39,7 @@ SQSQueuePolicyPublicRule, ) from cfripper.rules.stack_name_matches_regex import StackNameMatchesRegexRule +from cfripper.rules.storage_encrypted_rule import StorageEncryptedRule from cfripper.rules.wildcard_policies import ( GenericResourceWildcardPolicyRule, S3BucketPolicyWildcardActionRule, @@ -88,6 +89,7 @@ S3BucketPublicReadAclRule, S3CrossAccountTrustRule, S3ObjectVersioningRule, + StorageEncryptedRule, SNSTopicDangerousPolicyActionsRule, SNSTopicPolicyNotPrincipalRule, SNSTopicPolicyWildcardActionRule, diff --git a/cfripper/rules/storage_encrypted_rule.py b/cfripper/rules/storage_encrypted_rule.py new file mode 100644 index 00000000..6864f191 --- /dev/null +++ b/cfripper/rules/storage_encrypted_rule.py @@ -0,0 +1,40 @@ +from typing import Dict, Optional + +from pycfmodel.model.cf_model import CFModel + +from cfripper.model.enums import RuleGranularity, RuleMode, RuleRisk +from cfripper.model.result import Result +from cfripper.rules.base_rules import Rule + + +class StorageEncryptedRule(Rule): + RULE_MODE = RuleMode.DEBUG # for demonstration purposes + RISK_VALUE = RuleRisk.LOW + REASON = ( + "The database {} does not seem to be encrypted. Database resources should be encrypted and have the property " + "StorageEncrypted set to True." + ) + GRANULARITY = RuleGranularity.RESOURCE + + def invoke(self, cfmodel: CFModel, extras: Optional[Dict] = None) -> Result: + result = Result() + + for resource in cfmodel.Resources.values(): + is_encrypted = getattr(resource.Properties, "StorageEncrypted", False) + db_name = getattr(resource.Properties, "DBName", "(could not get DB name)") + if ( + resource.Type == "AWS::RDS::DBInstance" + and not is_encrypted + and not getattr(resource.Properties, "Engine", "").startswith( + "aurora" + ) # not applicable for aurora since the encryption for DB instances is managed by the DB cluster + ): + + self.add_failure_to_result( + result, + self.REASON.format(db_name), + context={"config": self._config, "extras": extras}, + resource_types={resource.Type}, + ) + + return result diff --git a/tests/rules/test_StorageEncryptedRule.py b/tests/rules/test_StorageEncryptedRule.py new file mode 100644 index 00000000..2e79ede1 --- /dev/null +++ b/tests/rules/test_StorageEncryptedRule.py @@ -0,0 +1,88 @@ +import pytest + +from cfripper.model.enums import RuleGranularity, RuleMode, RuleRisk +from cfripper.model.result import Failure +from cfripper.rules.storage_encrypted_rule import StorageEncryptedRule +from tests.utils import get_cfmodel_from + + +def test_storage_encrypted_rule_valid_results(): + rule = StorageEncryptedRule(None) + model = get_cfmodel_from("rules/StorageEncryptedRule/encrypted_db_resource.yml") + resolved_model = model.resolve() + result = rule.invoke(resolved_model) + + assert result.valid + assert result.failures == [] + + +def test_rule_not_failing_for_aurora(): + rule = StorageEncryptedRule(None) + model = get_cfmodel_from("rules/StorageEncryptedRule/aurora_engine_used.yml") + resolved_model = model.resolve() + result = rule.invoke(resolved_model) + + assert result.valid + assert result.failures == [] + + +@pytest.mark.parametrize( + "template, failures", + [ + ( + "rules/StorageEncryptedRule/missing_storage_encrypted_flag.yml", + [ + Failure( + granularity=RuleGranularity.RESOURCE, + reason="The database some-name does not seem to be encrypted. Database resources should be " + "encrypted and have the property StorageEncrypted set to True.", + risk_value=RuleRisk.LOW, + rule="StorageEncryptedRule", + rule_mode=RuleMode.DEBUG, + actions=None, + resource_ids=None, + resource_types={"AWS::RDS::DBInstance"}, + ) + ], + ), + ( + "rules/StorageEncryptedRule/two_resources_not_encrypted.yml", + [ + Failure( + granularity=RuleGranularity.RESOURCE, + reason="The database some-name does not seem to be encrypted. Database resources should be " + "encrypted and have the property StorageEncrypted set to True.", + risk_value=RuleRisk.LOW, + rule="StorageEncryptedRule", + rule_mode=RuleMode.DEBUG, + actions=None, + resource_ids=None, + resource_types={"AWS::RDS::DBInstance"}, + ), + Failure( + granularity=RuleGranularity.RESOURCE, + reason="The database some-name-backup does not seem to be encrypted. Database resources should be " + "encrypted and have the property StorageEncrypted set to True.", + risk_value=RuleRisk.LOW, + rule="StorageEncryptedRule", + rule_mode=RuleMode.DEBUG, + actions=None, + resource_ids=None, + resource_types={"AWS::RDS::DBInstance"}, + ), + ], + ), + ( + "rules/StorageEncryptedRule/no_db_resource.yml", + [], + ), + ], +) +def test_add_failure_if_db_resource_not_encrypted(template, failures): + rule = StorageEncryptedRule(None) + model = get_cfmodel_from(template) + resolved_model = model.resolve() + result = rule.invoke(resolved_model) + + assert result.valid + assert result.failures == failures diff --git a/tests/test_templates/rules/StorageEncryptedRule/aurora_engine_used.yml b/tests/test_templates/rules/StorageEncryptedRule/aurora_engine_used.yml new file mode 100644 index 00000000..cab515cb --- /dev/null +++ b/tests/test_templates/rules/StorageEncryptedRule/aurora_engine_used.yml @@ -0,0 +1,15 @@ +Resources: + DBMaster: + Type: AWS::RDS::DBInstance + Properties: + AllowMajorVersionUpgrade: false + AutoMinorVersionUpgrade: true + DBInstanceIdentifier: !Sub ${AWS::StackName}-master + DBName: "some-name" + Engine: aurora-postgresql + EngineVersion: "13.2" + KmsKeyId: "some-kms-key" + MultiAZ: true + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-master \ No newline at end of file diff --git a/tests/test_templates/rules/StorageEncryptedRule/encrypted_db_resource.yml b/tests/test_templates/rules/StorageEncryptedRule/encrypted_db_resource.yml new file mode 100644 index 00000000..8efac523 --- /dev/null +++ b/tests/test_templates/rules/StorageEncryptedRule/encrypted_db_resource.yml @@ -0,0 +1,18 @@ +Resources: + DBMaster: + Type: AWS::RDS::DBInstance + Properties: + AllocatedStorage: "100" + AllowMajorVersionUpgrade: false + AutoMinorVersionUpgrade: true + BackupRetentionPeriod: 14 + DBInstanceIdentifier: !Sub ${AWS::StackName}-master + DBName: "some-name" + Engine: mysql + EngineVersion: "13.2" + KmsKeyId: "some-kms-key" + MultiAZ: true + StorageEncrypted: true + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-master \ No newline at end of file diff --git a/tests/test_templates/rules/StorageEncryptedRule/missing_storage_encrypted_flag.yml b/tests/test_templates/rules/StorageEncryptedRule/missing_storage_encrypted_flag.yml new file mode 100644 index 00000000..850e1c09 --- /dev/null +++ b/tests/test_templates/rules/StorageEncryptedRule/missing_storage_encrypted_flag.yml @@ -0,0 +1,17 @@ +Resources: + DBMaster: + Type: AWS::RDS::DBInstance + Properties: + AllocatedStorage: "100" + AllowMajorVersionUpgrade: false + AutoMinorVersionUpgrade: true + BackupRetentionPeriod: 14 + DBInstanceIdentifier: !Sub ${AWS::StackName}-master + DBName: "some-name" + Engine: mysql + EngineVersion: "13.2" + KmsKeyId: "some-kms-key" + MultiAZ: true + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-master \ No newline at end of file diff --git a/tests/test_templates/rules/StorageEncryptedRule/no_db_resource.yml b/tests/test_templates/rules/StorageEncryptedRule/no_db_resource.yml new file mode 100644 index 00000000..2055a06e --- /dev/null +++ b/tests/test_templates/rules/StorageEncryptedRule/no_db_resource.yml @@ -0,0 +1,16 @@ +Resources: + SomeResource: + Type: AWS::RDS::DBCluster + Properties: + AllocatedStorage: "100" + AutoMinorVersionUpgrade: true + BackupRetentionPeriod: 14 + DBClusterIdentifier: !Sub ${AWS::StackName}-master + DatabaseName: "some-name" + Engine: mysql + EngineVersion: "13.2" + KmsKeyId: "some-kms-key" + StorageEncrypted: false + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-master \ No newline at end of file diff --git a/tests/test_templates/rules/StorageEncryptedRule/two_resources_not_encrypted.yml b/tests/test_templates/rules/StorageEncryptedRule/two_resources_not_encrypted.yml new file mode 100644 index 00000000..a9b4eea1 --- /dev/null +++ b/tests/test_templates/rules/StorageEncryptedRule/two_resources_not_encrypted.yml @@ -0,0 +1,35 @@ +Resources: + DBMaster: + Type: AWS::RDS::DBInstance + Properties: + AllocatedStorage: "100" + AllowMajorVersionUpgrade: false + AutoMinorVersionUpgrade: true + BackupRetentionPeriod: 14 + DBInstanceIdentifier: !Sub ${AWS::StackName}-master + DBName: "some-name" + Engine: mysql + EngineVersion: "13.2" + KmsKeyId: "some-kms-key" + MultiAZ: true + StorageEncrypted: false + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-master + DBBackup: + Type: AWS::RDS::DBInstance + Properties: + AllocatedStorage: "100" + AllowMajorVersionUpgrade: true + AutoMinorVersionUpgrade: false + BackupRetentionPeriod: 7 + DBInstanceIdentifier: !Sub ${AWS::StackName}-backup + DBName: "some-name-backup" + Engine: mysql + EngineVersion: "13.2" + KmsKeyId: "some-kms-key" + MultiAZ: true + StorageEncrypted: false + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-backup \ No newline at end of file