Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(aws): add new check cloudformation_stack_cdktoolkit_bootstrap_version #6323

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions prowler/config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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": ""
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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 (
Expand Down
Loading