feat(aws): add bedrock_prompt_management_exists security check (#10878)

This commit is contained in:
Daniel Barranquero
2026-05-04 12:38:15 +02:00
committed by GitHub
parent 6cb770fcc8
commit 921f49a0de
9 changed files with 535 additions and 0 deletions
+1
View File
@@ -10,6 +10,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Universal compliance pipeline integrated into the CLI: `--list-compliance` and `--list-compliance-requirements` show universal frameworks, and CSV plus OCSF outputs are generated for any framework declaring a `TableConfig` [(#10301)](https://github.com/prowler-cloud/prowler/pull/10301)
- ASD Essential Eight Maturity Model compliance framework for AWS (Maturity Level One, Nov 2023) [(#10808)](https://github.com/prowler-cloud/prowler/pull/10808)
- Update Vercel checks to return personalized finding status extended depending on billing plan and classify them with billing-plan categories [(#10663)](https://github.com/prowler-cloud/prowler/pull/10663)
- `bedrock_prompt_management_exists` check for AWS provider [(#10878)](https://github.com/prowler-cloud/prowler/pull/10878)
### 🔄 Changed
@@ -2897,6 +2897,7 @@
"bedrock_guardrails_configured",
"bedrock_model_invocation_logging_enabled",
"bedrock_model_invocation_logs_encryption_enabled",
"bedrock_prompt_management_exists",
"cloudformation_stack_outputs_find_secrets",
"cloudfront_distributions_custom_ssl_certificate",
"cloudfront_distributions_default_root_object",
@@ -2901,6 +2901,7 @@
"bedrock_guardrails_configured",
"bedrock_model_invocation_logging_enabled",
"bedrock_model_invocation_logs_encryption_enabled",
"bedrock_prompt_management_exists",
"cloudformation_stack_outputs_find_secrets",
"cloudfront_distributions_custom_ssl_certificate",
"cloudfront_distributions_default_root_object",
@@ -0,0 +1,39 @@
{
"Provider": "aws",
"CheckID": "bedrock_prompt_management_exists",
"CheckTitle": "Amazon Bedrock Prompt Management prompts exist in the region",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
],
"ServiceName": "bedrock",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "Other",
"ResourceGroup": "ai_ml",
"Description": "**Bedrock Prompt Management** enables centralized creation, versioning, and governance of prompts used with foundation models.\n\nThis region-level check verifies whether at least one managed prompt exists in each scanned region, used as an adoption signal for Prompt Management. The presence of a prompt does not by itself guarantee that every application prompt is managed.",
"Risk": "Without **Prompt Management**, prompts are scattered across applications with no central oversight, versioning, or auditability over instructions sent to foundation models, weakening governance and compliance posture.\n\nManaged prompts are a governance enabler; **prompt injection** defenses are provided by Bedrock **guardrails**, covered by separate checks.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-management.html",
"https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-management-create.html"
],
"Remediation": {
"Code": {
"CLI": "aws bedrock-agent create-prompt --name example_prompt --default-variant default --variants '[{\"name\":\"default\",\"templateType\":\"TEXT\",\"templateConfiguration\":{\"text\":{\"text\":\"Your prompt template here.\"}}}]'",
"NativeIaC": "",
"Other": "1. Open the Amazon Bedrock console\n2. Navigate to Prompt Management\n3. Click Create prompt\n4. Provide a name and configure the prompt template (a prompt can contain at most one variant; additional variants are created via CreatePromptVersion)\n5. Save the prompt",
"Terraform": ""
},
"Recommendation": {
"Text": "Adopt **Bedrock Prompt Management** to centralize prompt definitions, enforce versioning, and maintain governance over model interactions.\n\nUse managed prompts with **guardrails** and apply **least privilege** access controls to restrict who can create or modify prompts.",
"Url": "https://hub.prowler.com/check/bedrock_prompt_management_exists"
}
},
"Categories": [
"gen-ai"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Results are generated per scanned region. Regions where `ListPrompts` cannot be queried are omitted from the findings."
}
@@ -0,0 +1,54 @@
"""Check for region-level Bedrock Prompt Management adoption."""
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.bedrock.bedrock_agent_client import (
bedrock_agent_client,
)
class bedrock_prompt_management_exists(Check):
"""Check whether Amazon Bedrock Prompt Management prompts exist in the region.
A region is reported only when ListPrompts succeeded for it; regions where
the API call failed (e.g. AccessDenied, unsupported region) are skipped at
the service layer and produce no finding.
- PASS: At least one managed prompt exists in the region (one finding per prompt).
- FAIL: No managed prompts exist in the region (one finding per region).
"""
def execute(self) -> list[Check_Report_AWS]:
"""Execute the Bedrock Prompt Management exists check.
Returns:
A list of reports containing the result of the check.
"""
findings = []
for region in sorted(bedrock_agent_client.prompt_scanned_regions):
regional_prompts = sorted(
(
prompt
for prompt in bedrock_agent_client.prompts.values()
if prompt.region == region
),
key=lambda prompt: prompt.name,
)
if regional_prompts:
for prompt in regional_prompts:
report = Check_Report_AWS(metadata=self.metadata(), resource=prompt)
report.status = "PASS"
report.status_extended = f"Bedrock Prompt Management prompt {prompt.name} exists in region {region}."
findings.append(report)
else:
report = Check_Report_AWS(metadata=self.metadata(), resource={})
report.region = region
report.resource_id = "prompt-management"
report.resource_arn = f"arn:{bedrock_agent_client.audited_partition}:bedrock:{region}:{bedrock_agent_client.audited_account}:prompt-management"
report.status = "FAIL"
report.status_extended = (
f"No Bedrock Prompt Management prompts exist in region {region}."
)
findings.append(report)
return findings
@@ -140,7 +140,10 @@ class BedrockAgent(AWSService):
# Call AWSService's __init__
super().__init__("bedrock-agent", provider)
self.agents = {}
self.prompts = {}
self.prompt_scanned_regions: set = set()
self.__threading_call__(self._list_agents)
self.__threading_call__(self._list_prompts)
self.__threading_call__(self._list_tags_for_resource, self.agents.values())
def _list_agents(self, regional_client):
@@ -167,7 +170,32 @@ class BedrockAgent(AWSService):
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _list_prompts(self, regional_client):
"""List all prompts in a region.
Prompt Management is evaluated as a region-level adoption signal, so
prompt collection is intentionally not filtered by audit_resources.
"""
logger.info("Bedrock Agent - Listing Prompts...")
try:
paginator = regional_client.get_paginator("list_prompts")
for page in paginator.paginate():
for prompt in page.get("promptSummaries", []):
prompt_arn = prompt.get("arn", "")
self.prompts[prompt_arn] = Prompt(
id=prompt.get("id", ""),
name=prompt.get("name", ""),
arn=prompt_arn,
region=regional_client.region,
)
self.prompt_scanned_regions.add(regional_client.region)
except Exception as error:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _list_tags_for_resource(self, resource):
"""List tags for a Bedrock Agent resource."""
logger.info("Bedrock Agent - Listing Tags for Resource...")
try:
agent_tags = (
@@ -190,3 +218,12 @@ class Agent(BaseModel):
guardrail_id: Optional[str] = None
region: str
tags: Optional[list] = []
class Prompt(BaseModel):
"""Model representing a Bedrock Prompt Management prompt."""
id: str
name: str
arn: str
region: str
@@ -0,0 +1,280 @@
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
PROMPT_ARN = (
f"arn:aws:bedrock:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:prompt/test-prompt-id"
)
def mock_make_api_call_list_prompts_access_denied(self, operation_name, kwarg):
"""Mock API call where ListPrompts fails with AccessDeniedException."""
if operation_name == "ListPrompts":
raise ClientError(
{
"Error": {
"Code": "AccessDeniedException",
"Message": "User is not authorized to perform: bedrock:ListPrompts",
}
},
operation_name,
)
return make_api_call(self, operation_name, kwarg)
def mock_make_api_call_with_prompts(self, operation_name, kwarg):
"""Mock API call that returns prompts."""
if operation_name == "ListPrompts":
return {
"promptSummaries": [
{
"id": "test-prompt-id",
"name": "test-prompt",
"arn": PROMPT_ARN,
}
]
}
return make_api_call(self, operation_name, kwarg)
def mock_make_api_call_with_multiple_prompts(self, operation_name, kwarg):
"""Mock API call that returns multiple prompts."""
if operation_name == "ListPrompts":
return {
"promptSummaries": [
{
"id": "test-prompt-id-1",
"name": "test-prompt-1",
"arn": f"arn:aws:bedrock:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:prompt/test-prompt-id-1",
},
{
"id": "test-prompt-id-2",
"name": "test-prompt-2",
"arn": f"arn:aws:bedrock:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:prompt/test-prompt-id-2",
},
{
"id": "test-prompt-id-3",
"name": "test-prompt-3",
"arn": f"arn:aws:bedrock:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:prompt/test-prompt-id-3",
},
]
}
return make_api_call(self, operation_name, kwarg)
def mock_make_api_call_no_prompts(self, operation_name, kwarg):
"""Mock API call that returns no prompts."""
if operation_name == "ListPrompts":
return {"promptSummaries": []}
return make_api_call(self, operation_name, kwarg)
class Test_bedrock_prompt_management_exists:
@mock.patch(
"botocore.client.BaseClient._make_api_call",
new=mock_make_api_call_no_prompts,
)
@mock_aws
def test_no_prompts(self):
"""Test FAIL when no prompts exist in the region."""
from prowler.providers.aws.services.bedrock.bedrock_service import BedrockAgent
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_prompt_management_exists.bedrock_prompt_management_exists.bedrock_agent_client",
new=BedrockAgent(aws_provider),
),
):
from prowler.providers.aws.services.bedrock.bedrock_prompt_management_exists.bedrock_prompt_management_exists import (
bedrock_prompt_management_exists,
)
check = bedrock_prompt_management_exists()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"No Bedrock Prompt Management prompts exist in region {AWS_REGION_US_EAST_1}."
)
assert result[0].resource_id == "prompt-management"
assert result[0].region == AWS_REGION_US_EAST_1
assert (
result[0].resource_arn
== f"arn:aws:bedrock:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:prompt-management"
)
@mock.patch(
"botocore.client.BaseClient._make_api_call",
new=mock_make_api_call_with_prompts,
)
@mock_aws
def test_prompts_exist(self):
"""Test PASS when prompts exist in the region."""
from prowler.providers.aws.services.bedrock.bedrock_service import BedrockAgent
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_prompt_management_exists.bedrock_prompt_management_exists.bedrock_agent_client",
new=BedrockAgent(aws_provider),
),
):
from prowler.providers.aws.services.bedrock.bedrock_prompt_management_exists.bedrock_prompt_management_exists import (
bedrock_prompt_management_exists,
)
check = bedrock_prompt_management_exists()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Bedrock Prompt Management prompt test-prompt exists in region {AWS_REGION_US_EAST_1}."
)
assert result[0].resource_id == "test-prompt-id"
assert result[0].region == AWS_REGION_US_EAST_1
assert result[0].resource_arn == PROMPT_ARN
@mock.patch(
"botocore.client.BaseClient._make_api_call",
new=mock_make_api_call_with_multiple_prompts,
)
@mock_aws
def test_multiple_prompts_exist(self):
"""Test PASS with one finding per prompt when multiple prompts exist."""
from prowler.providers.aws.services.bedrock.bedrock_service import BedrockAgent
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_prompt_management_exists.bedrock_prompt_management_exists.bedrock_agent_client",
new=BedrockAgent(aws_provider),
),
):
from prowler.providers.aws.services.bedrock.bedrock_prompt_management_exists.bedrock_prompt_management_exists import (
bedrock_prompt_management_exists,
)
check = bedrock_prompt_management_exists()
result = check.execute()
assert len(result) == 3
for index, finding in enumerate(result, start=1):
expected_name = f"test-prompt-{index}"
expected_id = f"test-prompt-id-{index}"
assert finding.status == "PASS"
assert (
finding.status_extended
== f"Bedrock Prompt Management prompt {expected_name} exists in region {AWS_REGION_US_EAST_1}."
)
assert finding.resource_id == expected_id
assert finding.region == AWS_REGION_US_EAST_1
assert (
finding.resource_arn
== f"arn:aws:bedrock:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:prompt/{expected_id}"
)
@mock.patch(
"botocore.client.BaseClient._make_api_call",
new=mock_make_api_call_no_prompts,
)
@mock_aws
def test_no_prompts_multiple_regions(self):
"""Test FAIL in multiple regions when no prompts exist."""
from prowler.providers.aws.services.bedrock.bedrock_service import BedrockAgent
aws_provider = set_mocked_aws_provider(
[AWS_REGION_US_EAST_1, AWS_REGION_EU_WEST_1]
)
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
mock.patch(
"prowler.providers.aws.services.bedrock.bedrock_prompt_management_exists.bedrock_prompt_management_exists.bedrock_agent_client",
new=BedrockAgent(aws_provider),
),
):
from prowler.providers.aws.services.bedrock.bedrock_prompt_management_exists.bedrock_prompt_management_exists import (
bedrock_prompt_management_exists,
)
check = bedrock_prompt_management_exists()
result = check.execute()
assert len(result) == 2
for finding in result:
assert finding.status == "FAIL"
assert (
finding.status_extended
== f"No Bedrock Prompt Management prompts exist in region {finding.region}."
)
assert finding.resource_id == "prompt-management"
assert (
finding.resource_arn
== f"arn:aws:bedrock:{finding.region}:{AWS_ACCOUNT_NUMBER}:prompt-management"
)
regions = {finding.region for finding in result}
assert regions == {AWS_REGION_US_EAST_1, AWS_REGION_EU_WEST_1}
@mock.patch(
"botocore.client.BaseClient._make_api_call",
new=mock_make_api_call_list_prompts_access_denied,
)
@mock_aws
def test_list_prompts_client_error_skips_region(self):
"""Test that regions where ListPrompts fails produce no findings."""
from prowler.providers.aws.services.bedrock.bedrock_service import BedrockAgent
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_prompt_management_exists.bedrock_prompt_management_exists.bedrock_agent_client",
new=BedrockAgent(aws_provider),
),
):
from prowler.providers.aws.services.bedrock.bedrock_prompt_management_exists.bedrock_prompt_management_exists import (
bedrock_prompt_management_exists,
)
check = bedrock_prompt_management_exists()
result = check.execute()
assert result == []
@@ -341,3 +341,125 @@ class TestBedrockAgentPagination:
# Verify paginator was used
regional_client.get_paginator.assert_called_once_with("list_agents")
paginator.paginate.assert_called_once()
class TestBedrockPromptPagination:
"""Test suite for Bedrock Prompt pagination logic."""
def test_list_prompts_pagination(self):
"""Test that list_prompts iterates through all pages."""
# Mock the audit_info
audit_info = MagicMock()
audit_info.audited_partition = "aws"
audit_info.audited_account = "123456789012"
audit_info.audit_resources = None
# Mock the regional client
regional_client = MagicMock()
regional_client.region = "us-east-1"
# Mock paginator
paginator = MagicMock()
page1 = {
"promptSummaries": [
{
"id": "prompt-1",
"name": "prompt-name-1",
"arn": "arn:aws:bedrock:us-east-1:123456789012:prompt/prompt-1",
}
]
}
page2 = {
"promptSummaries": [
{
"id": "prompt-2",
"name": "prompt-name-2",
"arn": "arn:aws:bedrock:us-east-1:123456789012:prompt/prompt-2",
}
]
}
paginator.paginate.return_value = [page1, page2]
regional_client.get_paginator.return_value = paginator
# Initialize service and inject mock client
bedrock_agent_service = BedrockAgent(audit_info)
bedrock_agent_service.regional_clients = {"us-east-1": regional_client}
bedrock_agent_service.prompts = {} # Clear init side effects
bedrock_agent_service.prompt_scanned_regions = set()
# Run method
bedrock_agent_service._list_prompts(regional_client)
# Assertions
assert len(bedrock_agent_service.prompts) == 2
assert (
"arn:aws:bedrock:us-east-1:123456789012:prompt/prompt-1"
in bedrock_agent_service.prompts
)
assert (
"arn:aws:bedrock:us-east-1:123456789012:prompt/prompt-2"
in bedrock_agent_service.prompts
)
assert "us-east-1" in bedrock_agent_service.prompt_scanned_regions
# Verify paginator was used
regional_client.get_paginator.assert_called_once_with("list_prompts")
paginator.paginate.assert_called_once()
def test_list_prompts_ignores_audit_resources_filter(self):
"""Prompt collection is region-scoped and must ignore audit_resources."""
audit_info = MagicMock()
audit_info.audited_partition = "aws"
audit_info.audited_account = "123456789012"
audit_info.audit_resources = ["arn:aws:s3:::unrelated-resource"]
regional_client = MagicMock()
regional_client.region = "us-east-1"
paginator = MagicMock()
paginator.paginate.return_value = [
{
"promptSummaries": [
{
"id": "prompt-1",
"name": "prompt-name-1",
"arn": "arn:aws:bedrock:us-east-1:123456789012:prompt/prompt-1",
}
]
}
]
regional_client.get_paginator.return_value = paginator
bedrock_agent_service = BedrockAgent(audit_info)
bedrock_agent_service.regional_clients = {"us-east-1": regional_client}
bedrock_agent_service.prompts = {}
bedrock_agent_service.prompt_scanned_regions = set()
bedrock_agent_service._list_prompts(regional_client)
assert len(bedrock_agent_service.prompts) == 1
assert "us-east-1" in bedrock_agent_service.prompt_scanned_regions
def test_list_prompts_error_does_not_mark_region_scanned(self):
"""If ListPrompts raises, the region must not be added to prompt_scanned_regions."""
audit_info = MagicMock()
audit_info.audited_partition = "aws"
audit_info.audited_account = "123456789012"
audit_info.audit_resources = None
regional_client = MagicMock()
regional_client.region = "us-east-1"
paginator = MagicMock()
paginator.paginate.side_effect = Exception("ListPrompts failed")
regional_client.get_paginator.return_value = paginator
bedrock_agent_service = BedrockAgent(audit_info)
bedrock_agent_service.regional_clients = {"us-east-1": regional_client}
bedrock_agent_service.prompts = {}
bedrock_agent_service.prompt_scanned_regions = set()
bedrock_agent_service._list_prompts(regional_client)
assert bedrock_agent_service.prompts == {}
assert bedrock_agent_service.prompt_scanned_regions == set()