diff --git a/prowler/config/config.yaml b/prowler/config/config.yaml index 775e44cbac5..cceb5499484 100644 --- a/prowler/config/config.yaml +++ b/prowler/config/config.yaml @@ -73,6 +73,10 @@ aws: # aws.cloudwatch_log_group_retention_policy_specific_days_enabled --> by default is 365 days log_group_retention_days: 365 + # AWS CloudFormation Configuration + # cloudformation_stack_cdktoolkit_bootstrap_version --> by default is 21 + recommended_cdk_bootstrap_version: 21 + # AWS AppStream Session Configuration # aws.appstream_fleet_session_idle_disconnect_timeout max_idle_disconnect_timeout_in_seconds: 600 # 10 Minutes diff --git a/prowler/providers/aws/services/cloudformation/cloudformation_stack_cdktoolkit_bootstrap_version/__init__.py b/prowler/providers/aws/services/cloudformation/cloudformation_stack_cdktoolkit_bootstrap_version/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/aws/services/cloudformation/cloudformation_stack_cdktoolkit_bootstrap_version/cloudformation_stack_cdktoolkit_bootstrap_version.metadata.json b/prowler/providers/aws/services/cloudformation/cloudformation_stack_cdktoolkit_bootstrap_version/cloudformation_stack_cdktoolkit_bootstrap_version.metadata.json new file mode 100644 index 00000000000..494daa33372 --- /dev/null +++ b/prowler/providers/aws/services/cloudformation/cloudformation_stack_cdktoolkit_bootstrap_version/cloudformation_stack_cdktoolkit_bootstrap_version.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "aws", + "CheckID": "cloudformation_stack_cdktoolkit_bootstrap_version", + "CheckTitle": "Ensure that CDKToolkit stacks have a Bootstrap version of 21 or higher to mitigate security risks.", + "CheckType": [], + "ServiceName": "cloudformation", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:cloudformation:region:account-id:stack/resource-id", + "Severity": "high", + "ResourceType": "AwsCloudFormationStack", + "Description": "Ensure that CDKToolkit stacks have a Bootstrap version of 21 or higher to mitigate security risks.", + "Risk": "Using outdated CDKToolkit Bootstrap versions can expose accounts to risks such as bucket takeover or privilege escalation.", + "RelatedUrl": "https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Update the CDKToolkit stack Bootstrap version to 21 or later by running the cdk bootstrap command with the latest CDK version.", + "Url": "https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/cloudformation/cloudformation_stack_cdktoolkit_bootstrap_version/cloudformation_stack_cdktoolkit_bootstrap_version.py b/prowler/providers/aws/services/cloudformation/cloudformation_stack_cdktoolkit_bootstrap_version/cloudformation_stack_cdktoolkit_bootstrap_version.py new file mode 100644 index 00000000000..4306e3e593b --- /dev/null +++ b/prowler/providers/aws/services/cloudformation/cloudformation_stack_cdktoolkit_bootstrap_version/cloudformation_stack_cdktoolkit_bootstrap_version.py @@ -0,0 +1,39 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.cloudformation.cloudformation_client import ( + cloudformation_client, +) + + +class cloudformation_stack_cdktoolkit_bootstrap_version(Check): + """Check if a CDKToolkit CloudFormation Stack has a Bootstrap version less than recommended""" + + def execute(self): + """Execute the cloudformation_stack_cdktoolkit_bootstrap_version check""" + findings = [] + recommended_cdk_bootstrap_version = cloudformation_client.audit_config.get( + "recommended_cdk_bootstrap_version", 21 + ) + for stack in cloudformation_client.stacks: + # Only check stacks named CDKToolkit + if stack.name == "CDKToolkit": + bootstrap_version = None + if stack.outputs: + for output in stack.outputs: + if output.startswith("BootstrapVersion:"): + bootstrap_version = int(output.split(":")[1]) + break + if bootstrap_version: + report = Check_Report_AWS(self.metadata()) + report.region = stack.region + report.resource_id = stack.name + report.resource_arn = stack.arn + report.resource_tags = stack.tags + report.status = "PASS" + report.status_extended = f"CloudFormation Stack CDKToolkit has a Bootstrap version {bootstrap_version}, which meets the recommended version." + if bootstrap_version < recommended_cdk_bootstrap_version: + report.status = "FAIL" + report.status_extended = f"CloudFormation Stack CDKToolkit has a Bootstrap version {bootstrap_version}, which is less than the recommended version {recommended_cdk_bootstrap_version}." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/cloudformation/cloudformation_stack_outputs_find_secrets/cloudformation_stack_outputs_find_secrets.py b/prowler/providers/aws/services/cloudformation/cloudformation_stack_outputs_find_secrets/cloudformation_stack_outputs_find_secrets.py index 7c40f568a4a..2bcaa2ac30c 100644 --- a/prowler/providers/aws/services/cloudformation/cloudformation_stack_outputs_find_secrets/cloudformation_stack_outputs_find_secrets.py +++ b/prowler/providers/aws/services/cloudformation/cloudformation_stack_outputs_find_secrets/cloudformation_stack_outputs_find_secrets.py @@ -21,7 +21,9 @@ def execute(self): report.resource_arn = stack.arn report.resource_tags = stack.tags report.status = "PASS" - report.status_extended = f"No secrets found in Stack {stack.name} Outputs." + report.status_extended = ( + f"No secrets found in CloudFormation Stack {stack.name} Outputs." + ) if stack.outputs: data = "" # Store the CloudFormation Stack Outputs into a file @@ -40,11 +42,13 @@ def execute(self): ] ) report.status = "FAIL" - report.status_extended = f"Potential secret found in Stack {stack.name} Outputs -> {secrets_string}." + report.status_extended = f"Potential secret found in CloudFormation Stack {stack.name} Outputs -> {secrets_string}." else: report.status = "PASS" - report.status_extended = f"CloudFormation {stack.name} has no Outputs." + report.status_extended = ( + f"CloudFormation Stack {stack.name} has no Outputs." + ) findings.append(report) diff --git a/prowler/providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/cloudformation_stacks_termination_protection_enabled.py b/prowler/providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/cloudformation_stacks_termination_protection_enabled.py index 84244f561ac..af44d330386 100644 --- a/prowler/providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/cloudformation_stacks_termination_protection_enabled.py +++ b/prowler/providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/cloudformation_stacks_termination_protection_enabled.py @@ -20,10 +20,10 @@ def execute(self): if stack.enable_termination_protection: report.status = "PASS" - report.status_extended = f"CloudFormation {stack.name} has termination protection enabled." + report.status_extended = f"CloudFormation Stack {stack.name} has termination protection enabled." else: report.status = "FAIL" - report.status_extended = f"CloudFormation {stack.name} has termination protection disabled." + report.status_extended = f"CloudFormation Stack {stack.name} has termination protection disabled." findings.append(report) return findings diff --git a/tests/providers/aws/services/cloudformation/cloudformation_stack_cdktoolkit_bootstrap_version/cloudformation_stack_cdktoolkit_bootstrap_version_test.py b/tests/providers/aws/services/cloudformation/cloudformation_stack_cdktoolkit_bootstrap_version/cloudformation_stack_cdktoolkit_bootstrap_version_test.py new file mode 100644 index 00000000000..b9d7c57cbe9 --- /dev/null +++ b/tests/providers/aws/services/cloudformation/cloudformation_stack_cdktoolkit_bootstrap_version/cloudformation_stack_cdktoolkit_bootstrap_version_test.py @@ -0,0 +1,97 @@ +from unittest import mock + +from prowler.providers.aws.services.cloudformation.cloudformation_service import Stack + +# Mock Test Region +AWS_REGION = "eu-west-1" + + +class Test_cloudformation_stack_cdktoolkit_bootstrap_version: + def test_no_stacks(self): + cloudformation_client = mock.MagicMock + cloudformation_client.stacks = [] + cloudformation_client.audit_config = {"recommended_cdk_bootstrap_version": 21} + with mock.patch( + "prowler.providers.aws.services.cloudformation.cloudformation_client.cloudformation_client", + new=cloudformation_client, + ): + from prowler.providers.aws.services.cloudformation.cloudformation_stack_cdktoolkit_bootstrap_version.cloudformation_stack_cdktoolkit_bootstrap_version import ( + cloudformation_stack_cdktoolkit_bootstrap_version, + ) + + check = cloudformation_stack_cdktoolkit_bootstrap_version() + result = check.execute() + + assert len(result) == 0 + + def test_stack_with_valid_bootstrap_version(self): + cloudformation_client = mock.MagicMock + cloudformation_client.stacks = [ + Stack( + arn="arn:aws:cloudformation:eu-west-1:123456789012:stack/CDKToolkit/1234abcd", + name="CDKToolkit", + outputs=["BootstrapVersion:21"], + region=AWS_REGION, + ) + ] + cloudformation_client.audit_config = {"recommended_cdk_bootstrap_version": 21} + + with mock.patch( + "prowler.providers.aws.services.cloudformation.cloudformation_client.cloudformation_client", + new=cloudformation_client, + ): + from prowler.providers.aws.services.cloudformation.cloudformation_stack_cdktoolkit_bootstrap_version.cloudformation_stack_cdktoolkit_bootstrap_version import ( + cloudformation_stack_cdktoolkit_bootstrap_version, + ) + + check = cloudformation_stack_cdktoolkit_bootstrap_version() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "CloudFormation Stack CDKToolkit has a Bootstrap version 21, which meets the recommended version." + ) + assert result[0].resource_id == "CDKToolkit" + assert ( + result[0].resource_arn + == "arn:aws:cloudformation:eu-west-1:123456789012:stack/CDKToolkit/1234abcd" + ) + assert result[0].region == AWS_REGION + + def test_stack_with_invalid_bootstrap_version(self): + cloudformation_client = mock.MagicMock + cloudformation_client.stacks = [ + Stack( + arn="arn:aws:cloudformation:eu-west-1:123456789012:stack/CDKToolkit/1234abcd", + name="CDKToolkit", + outputs=["BootstrapVersion:20"], + region=AWS_REGION, + ) + ] + cloudformation_client.audit_config = {"recommended_cdk_bootstrap_version": 21} + + with mock.patch( + "prowler.providers.aws.services.cloudformation.cloudformation_client.cloudformation_client", + new=cloudformation_client, + ): + from prowler.providers.aws.services.cloudformation.cloudformation_stack_cdktoolkit_bootstrap_version.cloudformation_stack_cdktoolkit_bootstrap_version import ( + cloudformation_stack_cdktoolkit_bootstrap_version, + ) + + check = cloudformation_stack_cdktoolkit_bootstrap_version() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "CloudFormation Stack CDKToolkit has a Bootstrap version 20, which is less than the recommended version 21." + ) + assert result[0].resource_id == "CDKToolkit" + assert ( + result[0].resource_arn + == "arn:aws:cloudformation:eu-west-1:123456789012:stack/CDKToolkit/1234abcd" + ) + assert result[0].region == AWS_REGION diff --git a/tests/providers/aws/services/cloudformation/cloudformation_stack_outputs_find_secrets/cloudformation_stack_outputs_find_secrets_test.py b/tests/providers/aws/services/cloudformation/cloudformation_stack_outputs_find_secrets/cloudformation_stack_outputs_find_secrets_test.py index 6d73a78e22e..429d8651b4d 100644 --- a/tests/providers/aws/services/cloudformation/cloudformation_stack_outputs_find_secrets/cloudformation_stack_outputs_find_secrets_test.py +++ b/tests/providers/aws/services/cloudformation/cloudformation_stack_outputs_find_secrets/cloudformation_stack_outputs_find_secrets_test.py @@ -51,7 +51,7 @@ def test_stack_secret_in_outputs(self): assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Potential secret found in Stack {stack_name} Outputs -> Secret Keyword in Output 1." + == f"Potential secret found in CloudFormation Stack {stack_name} Outputs -> Secret Keyword in Output 1." ) assert result[0].resource_id == "Test-Stack" assert ( @@ -90,7 +90,7 @@ def test_stack_secret_in_outputs_false_case(self): assert result[0].status == "PASS" assert ( result[0].status_extended - == f"No secrets found in Stack {stack_name} Outputs." + == f"No secrets found in CloudFormation Stack {stack_name} Outputs." ) assert result[0].resource_id == "Test-Stack" assert ( @@ -127,7 +127,7 @@ def test_stack_no_secret_in_outputs(self): assert result[0].status == "PASS" assert ( result[0].status_extended - == f"No secrets found in Stack {stack_name} Outputs." + == f"No secrets found in CloudFormation Stack {stack_name} Outputs." ) assert result[0].resource_id == "Test-Stack" assert ( @@ -164,7 +164,7 @@ def test_stack_no_outputs(self): assert result[0].status == "PASS" assert ( result[0].status_extended - == f"CloudFormation {stack_name} has no Outputs." + == f"CloudFormation Stack {stack_name} has no Outputs." ) assert result[0].resource_id == "Test-Stack" assert ( diff --git a/tests/providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/cloudformation_stacks_termination_protection_enabled_test.py b/tests/providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/cloudformation_stacks_termination_protection_enabled_test.py index 9bde9f5173b..7fa73d68ec8 100644 --- a/tests/providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/cloudformation_stacks_termination_protection_enabled_test.py +++ b/tests/providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/cloudformation_stacks_termination_protection_enabled_test.py @@ -52,7 +52,7 @@ def test_stack_termination_protection_enabled(self): assert result[0].status == "PASS" assert ( result[0].status_extended - == f"CloudFormation {stack_name} has termination protection enabled." + == f"CloudFormation Stack {stack_name} has termination protection enabled." ) assert result[0].resource_id == "Test-Stack" assert ( @@ -90,7 +90,7 @@ def test_stack_termination_protection_disabled(self): assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"CloudFormation {stack_name} has termination protection disabled." + == f"CloudFormation Stack {stack_name} has termination protection disabled." ) assert result[0].resource_id == "Test-Stack" assert (