diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index d5dbeeaba3..cb87c8c9f4 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to the **Prowler SDK** are documented in this file. +## [5.26.0] (Prowler UNRELEASED) + +### 🚀 Added + +- `bedrock_guardrails_configured` check for AWS provider [(#10844)](https://github.com/prowler-cloud/prowler/pull/10844) + +--- + ## [5.25.0] (Prowler v5.25.0) ### 🚀 Added diff --git a/prowler/compliance/aws/ccc_aws.json b/prowler/compliance/aws/ccc_aws.json index a625de389c..ea28875c5c 100644 --- a/prowler/compliance/aws/ccc_aws.json +++ b/prowler/compliance/aws/ccc_aws.json @@ -6426,9 +6426,9 @@ } ], "Checks": [ + "bedrock_agent_guardrail_enabled", "bedrock_guardrail_prompt_attack_filter_enabled", - "bedrock_guardrail_sensitive_information_filter_enabled", - "bedrock_agent_guardrail_enabled" + "bedrock_guardrail_sensitive_information_filter_enabled" ] }, { @@ -6485,9 +6485,9 @@ } ], "Checks": [ + "bedrock_agent_guardrail_enabled", "bedrock_guardrail_prompt_attack_filter_enabled", - "bedrock_guardrail_sensitive_information_filter_enabled", - "bedrock_agent_guardrail_enabled" + "bedrock_guardrail_sensitive_information_filter_enabled" ] }, { @@ -6546,8 +6546,8 @@ } ], "Checks": [ - "bedrock_guardrail_sensitive_information_filter_enabled", - "bedrock_agent_guardrail_enabled" + "bedrock_agent_guardrail_enabled", + "bedrock_guardrail_sensitive_information_filter_enabled" ] }, { @@ -6606,8 +6606,8 @@ } ], "Checks": [ - "bedrock_guardrail_sensitive_information_filter_enabled", - "bedrock_agent_guardrail_enabled" + "bedrock_agent_guardrail_enabled", + "bedrock_guardrail_sensitive_information_filter_enabled" ] }, { diff --git a/prowler/compliance/aws/kisa_isms_p_2023_aws.json b/prowler/compliance/aws/kisa_isms_p_2023_aws.json index cb37e8bf67..f7c2a0bc69 100644 --- a/prowler/compliance/aws/kisa_isms_p_2023_aws.json +++ b/prowler/compliance/aws/kisa_isms_p_2023_aws.json @@ -2894,6 +2894,7 @@ "bedrock_agent_guardrail_enabled", "bedrock_guardrail_prompt_attack_filter_enabled", "bedrock_guardrail_sensitive_information_filter_enabled", + "bedrock_guardrails_configured", "bedrock_model_invocation_logging_enabled", "bedrock_model_invocation_logs_encryption_enabled", "cloudformation_stack_outputs_find_secrets", diff --git a/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json b/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json index d1dbcb64b2..877b46250c 100644 --- a/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json +++ b/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json @@ -2898,6 +2898,7 @@ "bedrock_agent_guardrail_enabled", "bedrock_guardrail_prompt_attack_filter_enabled", "bedrock_guardrail_sensitive_information_filter_enabled", + "bedrock_guardrails_configured", "bedrock_model_invocation_logging_enabled", "bedrock_model_invocation_logs_encryption_enabled", "cloudformation_stack_outputs_find_secrets", diff --git a/prowler/providers/aws/services/bedrock/bedrock_guardrails_configured/__init__.py b/prowler/providers/aws/services/bedrock/bedrock_guardrails_configured/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/bedrock/bedrock_guardrails_configured/bedrock_guardrails_configured.metadata.json b/prowler/providers/aws/services/bedrock/bedrock_guardrails_configured/bedrock_guardrails_configured.metadata.json new file mode 100644 index 0000000000..c70d59fdf8 --- /dev/null +++ b/prowler/providers/aws/services/bedrock/bedrock_guardrails_configured/bedrock_guardrails_configured.metadata.json @@ -0,0 +1,44 @@ +{ + "Provider": "aws", + "CheckID": "bedrock_guardrails_configured", + "CheckTitle": "Bedrock has at least one guardrail configured in the audited region", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/AWS Security Best Practices/Runtime Behavior Analysis" + ], + "ServiceName": "bedrock", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Other", + "ResourceGroup": "ai_ml", + "Description": "**Amazon Bedrock guardrails** provide reusable safety policies for filtering harmful or unwanted content in model inputs and outputs.\n\nThis evaluation checks whether at least one guardrail exists in each successfully scanned region. It does **not** verify that guardrails are attached to agents or passed on individual model invocation API calls.", + "Risk": "Without any configured **Bedrock guardrails** in a region, teams lack a native reusable policy object for **content filtering** and **safety controls**. Applications may invoke models without standardized protections against **harmful content**, **prompt injection**, or **sensitive-data exposure** unless equivalent controls are enforced elsewhere.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html", + "https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-create.html" + ], + "Remediation": { + "Code": { + "CLI": "aws bedrock create-guardrail --name example_resource --blocked-input-messaging 'Blocked' --blocked-outputs-messaging 'Blocked' --content-policy-config 'filtersConfig=[{type=HATE,inputStrength=HIGH,outputStrength=HIGH}]'", + "NativeIaC": "```yaml\nResources:\n example_resource:\n Type: AWS::Bedrock::Guardrail\n Properties:\n Name: example_resource\n BlockedInputMessaging: \"Blocked\"\n BlockedOutputsMessaging: \"Blocked\"\n ContentPolicyConfig:\n FiltersConfig:\n - Type: HATE\n InputStrength: HIGH # Critical: configures content filtering\n OutputStrength: HIGH\n```", + "Other": "1. Open the AWS Console and go to Amazon Bedrock\n2. Select **Guardrails** from the navigation pane\n3. Click **Create guardrail**\n4. Configure content filters for harmful categories\n5. Set input and output messaging for blocked content\n6. Click **Create guardrail**", + "Terraform": "```hcl\nresource \"aws_bedrock_guardrail\" \"example_resource\" {\n name = \"example_resource\"\n blocked_input_messaging = \"Blocked\"\n blocked_outputs_messaging = \"Blocked\"\n\n content_policy_config {\n filters_config {\n type = \"HATE\" # Critical: configures content filtering\n input_strength = \"HIGH\"\n output_strength = \"HIGH\"\n }\n }\n}\n```" + }, + "Recommendation": { + "Text": "Create at least one **Bedrock guardrail** in each region where Bedrock is used, then separately ensure those guardrails are attached to relevant agents and invocation paths.\n- Configure **content filters** for harmful categories (hate, violence, sexual, misconduct)\n- Add **sensitive information filters** and **denied topic policies**\n- Apply guardrails at the API call level using `guardrailIdentifier` where supported", + "Url": "https://hub.prowler.com/check/bedrock_guardrails_configured" + } + }, + "Categories": [ + "gen-ai" + ], + "DependsOn": [], + "RelatedTo": [ + "bedrock_guardrail_prompt_attack_filter_enabled", + "bedrock_guardrail_sensitive_information_filter_enabled", + "bedrock_agent_guardrail_enabled" + ], + "Notes": "This check validates guardrail existence per successfully scanned region. It does not verify attachment to agents or the use of guardrails on model invocations. Regions where Bedrock guardrails cannot be enumerated are skipped to avoid false failures." +} diff --git a/prowler/providers/aws/services/bedrock/bedrock_guardrails_configured/bedrock_guardrails_configured.py b/prowler/providers/aws/services/bedrock/bedrock_guardrails_configured/bedrock_guardrails_configured.py new file mode 100644 index 0000000000..f0f29fdf8b --- /dev/null +++ b/prowler/providers/aws/services/bedrock/bedrock_guardrails_configured/bedrock_guardrails_configured.py @@ -0,0 +1,50 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.bedrock.bedrock_client import bedrock_client + + +class bedrock_guardrails_configured(Check): + """Ensure Bedrock guardrails are configured in successfully scanned regions. + + This check verifies that at least one Amazon Bedrock guardrail is configured + in each successfully scanned region. + - PASS: At least one Bedrock guardrail is configured in the region. + - FAIL: No Bedrock guardrails are configured in the region. + """ + + def execute(self) -> list[Check_Report_AWS]: + """Execute the check logic. + + Returns: + A list of reports containing the result of the check. + """ + findings = [] + for region in sorted(bedrock_client.guardrails_scanned_regions): + regional_guardrails = sorted( + ( + guardrail + for guardrail in bedrock_client.guardrails.values() + if guardrail.region == region + ), + key=lambda guardrail: guardrail.name, + ) + + if regional_guardrails: + for guardrail in regional_guardrails: + report = Check_Report_AWS( + metadata=self.metadata(), resource=guardrail + ) + report.status = "PASS" + report.status_extended = f"Bedrock guardrail {guardrail.name} is available in region {region}. This does not confirm that the guardrail is attached to agents or used on model invocations." + findings.append(report) + else: + report = Check_Report_AWS(metadata=self.metadata(), resource={}) + report.region = region + report.resource_id = "bedrock-guardrails" + report.resource_arn = f"arn:{bedrock_client.audited_partition}:bedrock:{region}:{bedrock_client.audited_account}:guardrails" + report.status = "FAIL" + report.status_extended = ( + f"Bedrock has no guardrails configured in region {region}." + ) + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/bedrock/bedrock_service.py b/prowler/providers/aws/services/bedrock/bedrock_service.py index c0e3c6717a..c19bcb9166 100644 --- a/prowler/providers/aws/services/bedrock/bedrock_service.py +++ b/prowler/providers/aws/services/bedrock/bedrock_service.py @@ -1,5 +1,6 @@ from typing import Optional +from botocore.exceptions import ClientError from pydantic.v1 import BaseModel from prowler.lib.logger import logger @@ -13,6 +14,8 @@ class Bedrock(AWSService): super().__init__(__class__.__name__, provider) self.logging_configurations = {} self.guardrails = {} + self.guardrails_scanned_regions = set() + self.guardrails_scan_errors = {} self.__threading_call__(self._get_model_invocation_logging_configuration) self.__threading_call__(self._list_guardrails) self.__threading_call__(self._get_guardrail, self.guardrails.values()) @@ -67,7 +70,18 @@ class Bedrock(AWSService): arn=guardrail["arn"], region=regional_client.region, ) + self.guardrails_scanned_regions.add(regional_client.region) + except ClientError as error: + self.guardrails_scan_errors[regional_client.region] = error.response[ + "Error" + ].get("Code", error.__class__.__name__) + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) except Exception as error: + self.guardrails_scan_errors[regional_client.region] = ( + error.__class__.__name__ + ) logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) diff --git a/tests/providers/aws/services/bedrock/bedrock_guardrails_configured/bedrock_guardrails_configured_test.py b/tests/providers/aws/services/bedrock/bedrock_guardrails_configured/bedrock_guardrails_configured_test.py new file mode 100644 index 0000000000..63ecb39832 --- /dev/null +++ b/tests/providers/aws/services/bedrock/bedrock_guardrails_configured/bedrock_guardrails_configured_test.py @@ -0,0 +1,302 @@ +from unittest import mock + +import botocore +from botocore.exceptions import ClientError +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + +make_api_call = botocore.client.BaseClient._make_api_call + +GUARDRAIL_ARN = ( + f"arn:aws:bedrock:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:guardrail/test-id" +) + + +def mock_make_api_call_with_guardrail(self, operation_name, kwarg): + """Mock API call returning one guardrail in us-east-1.""" + if operation_name == "ListGuardrails": + return { + "guardrails": [ + { + "id": "test-id", + "arn": GUARDRAIL_ARN, + "status": "READY", + "name": "test-guardrail", + } + ] + } + elif operation_name == "GetGuardrail": + return { + "name": "test-guardrail", + "guardrailId": "test-id", + "guardrailArn": GUARDRAIL_ARN, + "status": "READY", + "blockedInputMessaging": "Blocked", + "blockedOutputsMessaging": "Blocked", + "contentPolicy": {"filters": []}, + } + return make_api_call(self, operation_name, kwarg) + + +def mock_make_api_call_without_guardrails(self, operation_name, kwarg): + """Mock API call returning no guardrails.""" + if operation_name == "ListGuardrails": + return {"guardrails": []} + return make_api_call(self, operation_name, kwarg) + + +def mock_make_api_call_guardrails_only_in_us_east_1(self, operation_name, kwarg): + """Mock API call returning a guardrail only in us-east-1 and none elsewhere.""" + if operation_name == "ListGuardrails": + if self.meta.region_name == AWS_REGION_US_EAST_1: + return { + "guardrails": [ + { + "id": "test-id", + "arn": GUARDRAIL_ARN, + "status": "READY", + "name": "test-guardrail", + } + ] + } + return {"guardrails": []} + elif operation_name == "GetGuardrail": + return { + "name": "test-guardrail", + "guardrailId": "test-id", + "guardrailArn": GUARDRAIL_ARN, + "status": "READY", + "blockedInputMessaging": "Blocked", + "blockedOutputsMessaging": "Blocked", + "contentPolicy": {"filters": []}, + } + return make_api_call(self, operation_name, kwarg) + + +def mock_make_api_call_guardrail_validation_exception(self, operation_name, kwarg): + """Mock API call raising ValidationException for ListGuardrails.""" + if operation_name == "ListGuardrails": + raise ClientError( + { + "Error": { + "Code": "ValidationException", + "Message": "Guardrails are not supported in this region.", + } + }, + operation_name, + ) + return make_api_call(self, operation_name, kwarg) + + +class Test_bedrock_guardrails_configured: + @mock.patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_without_guardrails, + ) + @mock_aws + def test_no_guardrails_single_region(self): + """Test FAIL when no guardrails are configured in a single region.""" + from prowler.providers.aws.services.bedrock.bedrock_service import Bedrock + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.bedrock.bedrock_guardrails_configured.bedrock_guardrails_configured.bedrock_client", + new=Bedrock(aws_provider), + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_guardrails_configured.bedrock_guardrails_configured import ( + bedrock_guardrails_configured, + ) + + check = bedrock_guardrails_configured() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Bedrock has no guardrails configured in region {AWS_REGION_US_EAST_1}." + ) + assert result[0].resource_id == "bedrock-guardrails" + assert ( + result[0].resource_arn + == f"arn:aws:bedrock:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:guardrails" + ) + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags == [] + + @mock_aws + @mock.patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_without_guardrails, + ) + def test_no_guardrails_multi_region(self): + """Test FAIL in both regions when no guardrails are configured.""" + from prowler.providers.aws.services.bedrock.bedrock_service import Bedrock + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.bedrock.bedrock_guardrails_configured.bedrock_guardrails_configured.bedrock_client", + new=Bedrock(aws_provider), + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_guardrails_configured.bedrock_guardrails_configured import ( + bedrock_guardrails_configured, + ) + + check = bedrock_guardrails_configured() + result = check.execute() + + assert len(result) == 2 + assert result[0].status == "FAIL" + assert result[0].resource_id == "bedrock-guardrails" + assert result[0].resource_tags == [] + assert result[1].status == "FAIL" + assert result[1].resource_id == "bedrock-guardrails" + assert result[1].resource_tags == [] + + @mock.patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_with_guardrail, + ) + @mock_aws + def test_guardrail_configured(self): + """Test PASS when at least one guardrail is configured in the region.""" + from prowler.providers.aws.services.bedrock.bedrock_service import Bedrock + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.bedrock.bedrock_guardrails_configured.bedrock_guardrails_configured.bedrock_client", + new=Bedrock(aws_provider), + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_guardrails_configured.bedrock_guardrails_configured import ( + bedrock_guardrails_configured, + ) + + check = bedrock_guardrails_configured() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Bedrock guardrail test-guardrail is available in region {AWS_REGION_US_EAST_1}. This does not confirm that the guardrail is attached to agents or used on model invocations." + ) + assert result[0].resource_id == "test-id" + assert result[0].resource_arn == GUARDRAIL_ARN + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags == [] + + @mock.patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_guardrails_only_in_us_east_1, + ) + @mock_aws + def test_guardrails_in_one_region_only(self): + """Test PASS in the region with a guardrail and FAIL in the region without one.""" + from prowler.providers.aws.services.bedrock.bedrock_service import Bedrock + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.bedrock.bedrock_guardrails_configured.bedrock_guardrails_configured.bedrock_client", + new=Bedrock(aws_provider), + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_guardrails_configured.bedrock_guardrails_configured import ( + bedrock_guardrails_configured, + ) + + check = bedrock_guardrails_configured() + result = check.execute() + + assert len(result) == 2 + + results_by_region = {r.region: r for r in result} + + eu_result = results_by_region[AWS_REGION_EU_WEST_1] + assert eu_result.status == "FAIL" + assert ( + eu_result.status_extended + == f"Bedrock has no guardrails configured in region {AWS_REGION_EU_WEST_1}." + ) + assert eu_result.resource_id == "bedrock-guardrails" + assert ( + eu_result.resource_arn + == f"arn:aws:bedrock:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:guardrails" + ) + assert eu_result.resource_tags == [] + + us_result = results_by_region[AWS_REGION_US_EAST_1] + assert us_result.status == "PASS" + assert ( + us_result.status_extended + == f"Bedrock guardrail test-guardrail is available in region {AWS_REGION_US_EAST_1}. This does not confirm that the guardrail is attached to agents or used on model invocations." + ) + assert us_result.resource_id == "test-id" + assert us_result.resource_arn == GUARDRAIL_ARN + assert us_result.resource_tags == [] + + @mock.patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_guardrail_validation_exception, + ) + @mock_aws + def test_guardrails_unsupported_region_is_skipped(self): + """Test unsupported regions are skipped instead of failing.""" + from prowler.providers.aws.services.bedrock.bedrock_service import Bedrock + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.bedrock.bedrock_guardrails_configured.bedrock_guardrails_configured.bedrock_client", + new=Bedrock(aws_provider), + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_guardrails_configured.bedrock_guardrails_configured import ( + bedrock_guardrails_configured, + ) + + check = bedrock_guardrails_configured() + result = check.execute() + + assert len(result) == 0