Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
88b5eb2
feat(bedrock): add model artifact and guardrail grounding security ch…
tamg-aws Aug 13, 2026
843bffc
fix(bedrock): report unreadable inventory as MANUAL and audit only ow…
tamg-aws Aug 14, 2026
8770ec3
test(bedrock): exhaust the decision space for the two untestable reso…
tamg-aws Aug 14, 2026
eaa3f95
fix(bedrock): address review findings on the model artifact checks
tamg-aws Aug 14, 2026
8ce87a5
fix(bedrock): report unlisted guardrails and repair the IaC snippets
tamg-aws Aug 15, 2026
979dc65
fix(bedrock): keep data sources when a scan is scoped to a knowledge …
tamg-aws Aug 15, 2026
4cb1268
refactor(bedrock): filter ValidationException in one place for all fo…
tamg-aws Aug 15, 2026
f0f9315
fix(bedrock): make the knowledge base data source Terraform example d…
tamg-aws Aug 15, 2026
38f293f
fix(bedrock): require complete evidence before asserting a dedicated …
tamg-aws Aug 15, 2026
9493322
fix(bedrock): store typed contextual grounding filters instead of dicts
tamg-aws Aug 15, 2026
ef31a75
fix(bedrock): judge agent role sharing on deployed versions, not the …
tamg-aws Aug 15, 2026
3652e38
docs(bedrock): tell the agent role remediation to move the aliases
tamg-aws Aug 15, 2026
b76da50
fix(bedrock): name only the grounding filter attributes actually omitted
tamg-aws Aug 15, 2026
fb9fa14
fix(bedrock): make the incomplete-inventory message fit every entry i…
tamg-aws Aug 15, 2026
ea705c0
fix(bedrock): agree the missing-filter nouns with how many are missing
tamg-aws Aug 15, 2026
22baea7
fix(bedrock): judge role sharing on the whole account, in any partiti…
tamg-aws Aug 21, 2026
5e6d542
test(bedrock): cover scoped scans, non-commercial partitions, and ina…
tamg-aws Aug 21, 2026
53b1073
feat(compliance): map the new Bedrock checks into the AWS AI Security…
tamg-aws Aug 21, 2026
cbfe405
chore(changelog): note the Bedrock Agent ARN partition fix
tamg-aws Aug 21, 2026
d7bbcfe
chore(bedrock): trim a comment duplicating the docstring and guard th…
tamg-aws Aug 21, 2026
78fdcee
chore(bedrock): format the grounding-filter Risk like its three siblings
tamg-aws Aug 21, 2026
173c84d
fix(bedrock): handle transitional agent alias states
HugoPBrito Aug 21, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`bedrock_guardrail_contextual_grounding_filter_enabled`, `bedrock_custom_model_encrypted_with_cmk`, `bedrock_knowledge_base_encrypted_with_cmk` and `bedrock_agent_role_not_shared_across_agents` are four new AWS Bedrock checks covering guardrail contextual grounding, custom model encryption, knowledge-base data-source encryption, and non-shared agent execution roles.
22 changes: 15 additions & 7 deletions prowler/compliance/aws/aws_ai_security_framework_aws.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@
"eks_cluster_kms_cmk_encryption_in_secrets_enabled",
"dynamodb_tables_kms_cmk_encryption_enabled",
"sns_topics_kms_encryption_at_rest_enabled",
"sqs_queues_server_side_encryption_enabled"
"sqs_queues_server_side_encryption_enabled",
"bedrock_custom_model_encrypted_with_cmk",
"bedrock_knowledge_base_encrypted_with_cmk"
]
},
{
Expand Down Expand Up @@ -638,10 +640,12 @@
"Section": "AI Application Security",
"SubSection": "Output Validation",
"Service": "bedrock",
"Type": "Manual"
"Type": "Automated"
Comment thread
HugoPBrito marked this conversation as resolved.
}
],
"Checks": []
"Checks": [
"bedrock_guardrail_contextual_grounding_filter_enabled"
]
},
{
"Id": "AISF-AI-10",
Expand All @@ -652,10 +656,12 @@
"Section": "AI Application Security",
"SubSection": "RAG Security",
"Service": "bedrock",
"Type": "Manual"
"Type": "Automated"
Comment thread
HugoPBrito marked this conversation as resolved.
}
],
"Checks": []
"Checks": [
"bedrock_knowledge_base_encrypted_with_cmk"
]
},
{
"Id": "AISF-AI-11",
Expand All @@ -680,10 +686,12 @@
"Section": "AI Application Security",
"SubSection": "Agent Governance",
"Service": "bedrock",
"Type": "Manual"
"Type": "Automated"
Comment thread
HugoPBrito marked this conversation as resolved.
}
],
"Checks": []
"Checks": [
"bedrock_agent_role_not_shared_across_agents"
]
},
{
"Id": "AISF-AGENT-02",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"Provider": "aws",
"CheckID": "bedrock_agent_role_not_shared_across_agents",
"CheckTitle": "Bedrock Agent has a dedicated execution role",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
],
"ServiceName": "bedrock",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Other",
"ResourceGroup": "ai_ml",
"Description": "Every **Bedrock Agent** assumes the role in its `agentResourceRoleArn`. That role must belong to exactly one agent, so no agent inherits another's permissions.",
"Risk": "A shared execution role gives every agent using it the union of all their permissions, so a low-trust agent reaching an untrusted tool holds the rights granted for a high-trust one, turning a single prompt injection into access it was never scoped for. It also destroys attribution: CloudTrail records the role session, so an action cannot be traced to one agent.",
Comment thread
HugoPBrito marked this conversation as resolved.
Outdated
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/bedrock/latest/userguide/agents-permissions.html",
"https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent_UpdateAgent.html"
],
"Remediation": {
"Code": {
"CLI": "",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"NativeIaC": "```yaml\nResources:\n ExampleResource:\n Type: AWS::Bedrock::Agent\n Properties:\n AgentName: example_resource\n AgentResourceRoleArn: arn:aws:iam::123456789012:role/AmazonBedrockExecutionRoleForAgents_example_resource # Critical: one role per agent, never shared\n```",
"Other": "1. Open the AWS Console and go to Amazon Bedrock\n2. Select **Agents**, then an agent sharing a role\n3. Create a new IAM role trusted by bedrock.amazonaws.com holding only that agent's permissions\n4. Scope its trust policy with aws:SourceArn set to the agent ARN\n5. Edit the agent to use the new role, keeping its other settings, then prepare the agent to cut a version\n6. Open **Aliases** and point every alias at the new version, since an alias left on the old version keeps invoking the shared role\n7. Repeat until each agent has its own role, then remove the unused permissions from the old role",
"Terraform": "```hcl\nresource \"aws_bedrockagent_agent\" \"example_resource\" {\n agent_name = \"example_resource\"\n agent_resource_role_arn = aws_iam_role.example_resource_agent.arn # Critical: one role per agent, never shared\n foundation_model = \"example-model-id\"\n}\n```"
},
"Recommendation": {
"Text": "Give each Bedrock Agent its own execution role scoped to that agent's ARN with aws:SourceArn. Repoint the draft with UpdateAgent, passing every field it already has because omitted ones are not preserved, then PrepareAgent and UpdateAgentAlias to move each alias onto the new version: an alias left on the old version keeps invoking the shared role.",
"Url": "https://hub.prowler.com/check/bedrock_agent_role_not_shared_across_agents"
}
},
"Categories": [
"gen-ai",
"identity-access"
],
"DependsOn": [],
"RelatedTo": [
"bedrock_agent_role_least_privilege"
],
"Notes": "Reports one finding per AGENT, not per role, so a role shared by three agents produces three findings each naming the other two. Sharing is counted across the whole account inventory, so a partial scan can under-report. An agent whose role ARN could not be retrieved from GetAgent returns MANUAL rather than PASS, because an unknown role can be neither confirmed nor ruled out as shared, and it is also excluded from every other agent's share count."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.bedrock.bedrock_agent_client import (
bedrock_agent_client,
)

SHARED_ROLE_AGENT_COUNT = 2


class bedrock_agent_role_not_shared_across_agents(Check):
"""Ensure each Bedrock Agent has a dedicated execution role.

A shared `agentResourceRoleArn` means every agent using it inherits the
union of all their permissions, so the least-privileged agent in the set
still holds the rights of the most privileged one. It also destroys
attribution: CloudTrail records the role session, so an action taken with
that role cannot be tied back to a single agent.

Every role an agent can run under counts, not only the working draft's.
GetAgent returns the draft, but an agent version is an immutable snapshot
that keeps the role it was cut with, and an alias routes invocations at a
specific version — so a deployed version can still hold a role the draft no
longer has, and sharing it is the same exposure. Only versions an alias
routes to are considered, since a version no alias points at cannot be
invoked. An alias routing at DRAFT resolves to the draft role already read.

Sharing is judged against the whole account inventory, so the verdict is only
as complete as that inventory. Dedication cannot be asserted while any part of
the picture is missing — an unlisted Region, an agent whose own role could
not be read, or an agent whose deployed versions could not be listed may hold
the same role. A role already seen on two agents is shared whatever else is
missing, so FAIL survives an incomplete inventory.

- PASS: No role this agent holds is used by any other agent, every Region's
agent inventory was listed, every discovered agent's role was readable, and
every agent's deployed versions were listed.
- FAIL: Two or more agents hold the same role, on the draft or on a deployed
version; the other agents holding it are named in the message, and the
version is named when the sharing is through one.
- MANUAL: This agent's own execution role could not be retrieved from
GetAgent; or its roles look dedicated but another agent's role or deployed
versions are unknown, or a Region could not be listed; or ListAgents failed
for a Region, which therefore contributed no agents at all.
"""

def execute(self) -> list[Check_Report_AWS]:
"""Execute the check logic.

Returns:
A list of reports containing the result of the check.
"""
findings = []

incomplete_regions = sorted(bedrock_agent_client.agents_scan_errors)
for region, error in sorted(bedrock_agent_client.agents_scan_errors.items()):
report = Check_Report_AWS(
metadata=self.metadata(), resource={"region": region}
)
report.region = region
report.resource_id = "agent/unknown"
report.resource_arn = f"arn:{bedrock_agent_client.audited_partition}:bedrock:{region}:{bedrock_agent_client.audited_account}:agent/unknown"
report.status = "MANUAL"
report.status_extended = f"Bedrock Agents could not be listed in region {region} ({error}); verify manually that no execution role is shared between agents."
findings.append(report)

# Keyed on agent ARN, not name, so two same-named agents still count
# twice. An agent is indexed under every role it holds, because a
# deployed version keeps the role it was cut with: sharing through a
# version is the same exposure as sharing through the draft.
agents_by_role = {}
Comment thread
HugoPBrito marked this conversation as resolved.
unresolved_agents = []
for agent in bedrock_agent_client.agents.values():
entry = (agent.arn, agent.name or agent.id)
for role_arn in self._roles_held_by(agent):
if entry not in agents_by_role.setdefault(role_arn, []):
agents_by_role[role_arn].append(entry)
# Both gaps are recorded, not just the first: an agent can have an
# unreadable draft role and an unlistable version inventory at once,
# and each independently keeps another agent from being called
# dedicated.
if not agent.detail_retrieved or not agent.role_arn:
unresolved_agents.append(agent.name or agent.id)
if not agent.versions_listed:
unresolved_agents.append(
f"deployed versions of {agent.name or agent.id}"
)

# Dedication can only be asserted from a complete picture: an unlisted
# Region, an agent whose role could not be read, or an agent whose
# deployed versions could not be listed may hold the same role.
incomplete = sorted(unresolved_agents) + [
f"agents in region {region}" for region in incomplete_regions
]

for agent in bedrock_agent_client.agents.values():
report = Check_Report_AWS(metadata=self.metadata(), resource=agent)
Comment thread
HugoPBrito marked this conversation as resolved.
name = agent.name or agent.id

if not agent.detail_retrieved or not agent.role_arn:
report.status = "MANUAL"
report.status_extended = f"Bedrock Agent {name} execution role could not be retrieved in region {agent.region}; verify manually that no other agent shares it."
findings.append(report)
continue

# Any role this agent holds, on the draft or on a deployed version,
# is a finding when another agent holds it too. Reported on the
# lowest-sorted shared role for determinism across scans.
shared_roles = sorted(
role_arn
for role_arn in self._roles_held_by(agent)
if len(agents_by_role.get(role_arn, [])) >= SHARED_ROLE_AGENT_COUNT
)
if shared_roles:
role_arn = shared_roles[0]
others = sorted(
other_name
for other_arn, other_name in agents_by_role[role_arn]
if other_arn != agent.arn
)
through = (
""
if role_arn == agent.role_arn
else f" through deployed version {sorted(version for version, version_role in agent.version_role_arns.items() if version_role == role_arn)[0]}"
)
report.status = "FAIL"
report.status_extended = f"Bedrock Agent {name} shares execution role {role_arn}{through} with {', '.join(others)} in region {agent.region}, so each agent inherits the union of their permissions and CloudTrail cannot attribute an action to one of them."
elif incomplete:
report.status = "MANUAL"
report.status_extended = f"Bedrock Agent {name} execution role is used by no other agent whose role could be read in region {agent.region}, but the role of {', '.join(incomplete)} is unknown; verify manually that none of them shares it."
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
else:
report.status = "PASS"
report.status_extended = f"Bedrock Agent {name} has a dedicated execution role in region {agent.region}."
findings.append(report)

return findings

def _roles_held_by(self, agent) -> set:
"""Collect every execution role an agent can run under.

Args:
agent: The Bedrock Agent to inspect.

Returns:
The working draft's role plus the role of each deployed version an
alias routes to, skipping any that could not be read.
"""
roles = set()
if agent.detail_retrieved and agent.role_arn:
roles.add(agent.role_arn)
roles.update(
role_arn for role_arn in agent.version_role_arns.values() if role_arn
)
return roles
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"Provider": "aws",
"CheckID": "bedrock_custom_model_encrypted_with_cmk",
"CheckTitle": "Bedrock custom model is encrypted with a customer-managed KMS key",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/AWS Security Best Practices/Data Encryption"
],
"ServiceName": "bedrock",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "Other",
"ResourceGroup": "ai_ml",
"Description": "**Bedrock custom models** produced by model customization can be encrypted at rest with a customer-managed KMS key rather than resting under an AWS-owned key the organization cannot audit, rotate, or revoke.",
"Risk": "A fine-tuned model is derived from the training data used to build it and can leak that data through inference. Under an AWS-owned key the organization has no key policy to restrict who may decrypt the artifacts, no rotation it controls, no CloudTrail record of key usage, and no way to revoke access to the model by disabling a key.",
Comment thread
HugoPBrito marked this conversation as resolved.
Outdated
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/bedrock/latest/userguide/encryption-custom-job.html",
"https://docs.aws.amazon.com/bedrock/latest/APIReference/API_GetCustomModel.html"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Open the AWS Console and go to Amazon Bedrock\n2. Select **Custom models**, then create a new customization job\n3. Under encryption, choose a customer-managed KMS key instead of the default\n4. Grant the customization service role kms:Decrypt, kms:GenerateDataKey and kms:DescribeKey on that key\n5. Re-run customization, because the key of an existing custom model cannot be changed in place",
"Terraform": "```hcl\nresource \"aws_bedrock_custom_model\" \"example_resource\" {\n custom_model_name = \"example-custom-model\"\n job_name = \"example-customization-job\"\n base_model_identifier = \"arn:aws:bedrock:us-east-1::foundation-model/amazon.titan-text-express-v1\"\n role_arn = \"arn:aws:iam::123456789012:role/example-bedrock-customization-role\"\n custom_model_kms_key_id = \"arn:aws:kms:us-east-1:123456789012:key/example-key-id\" # Critical: without this the model is encrypted with an AWS owned key\n\n hyperparameters = {\n epochCount = \"1\"\n }\n\n output_data_config {\n s3_uri = \"s3://example-bucket/output/\"\n }\n\n training_data_config {\n s3_uri = \"s3://example-bucket/training/\"\n }\n}\n```"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
"Recommendation": {
"Text": "Choose a customer-managed KMS key when creating each model customization job so the resulting custom model rests under a key with an auditable policy and rotation. The key cannot be changed after the model is created, so re-run customization for existing models.",
"Url": "https://hub.prowler.com/check/bedrock_custom_model_encrypted_with_cmk"
}
},
"Categories": [
"gen-ai",
"encryption"
],
"DependsOn": [],
"RelatedTo": [
"bedrock_prompt_encrypted_with_cmk"
],
"Notes": "Reports one finding per custom model, reading modelKmsKeyArn from GetCustomModel because ListCustomModels summaries do not carry it. A model whose detail could not be retrieved returns MANUAL rather than PASS, because an absent key ARN means the answer is unknown, not that no key is set."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.bedrock.bedrock_client import bedrock_client


class bedrock_custom_model_encrypted_with_cmk(Check):
"""Ensure Bedrock custom models are encrypted with a customer-managed KMS key.

- PASS: GetCustomModel returns a `modelKmsKeyArn`, so the model artifacts
are encrypted with a key the account controls.
- FAIL: No `modelKmsKeyArn` is set, so the model is encrypted with an
AWS-owned key that the organization cannot audit, rotate, or revoke.
- MANUAL: GetCustomModel failed, so the key could not be retrieved and an
absent value cannot be read as "no key"; or ListCustomModels failed for a
region, so that region's custom models are unknown rather than absent.

Only models this account owns are audited (`ListCustomModels` is called with
`isOwned=True`): the KMS key of a model shared in through Resource Access
Manager belongs to the owning account and cannot be changed here.
"""

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, error in sorted(bedrock_client.custom_models_scan_errors.items()):
report = Check_Report_AWS(
metadata=self.metadata(), resource={"region": region}
)
report.region = region
report.resource_id = "custom-model/unknown"
report.resource_arn = f"arn:{bedrock_client.audited_partition}:bedrock:{region}:{bedrock_client.audited_account}:custom-model/unknown"
report.status = "MANUAL"
report.status_extended = f"Bedrock custom models could not be listed in region {region} ({error}); verify manually that every custom model uses a customer-managed KMS key."
findings.append(report)

for model in bedrock_client.custom_models.values():
report = Check_Report_AWS(metadata=self.metadata(), resource=model)

if not model.detail_retrieved:
report.status = "MANUAL"
report.status_extended = f"Bedrock custom model {model.name} encryption configuration could not be retrieved in region {model.region}; verify manually that it uses a customer-managed KMS key."
elif model.kms_key_arn:
report.status = "PASS"
report.status_extended = f"Bedrock custom model {model.name} is encrypted with a customer-managed KMS key in region {model.region}."
else:
report.status = "FAIL"
report.status_extended = f"Bedrock custom model {model.name} is not encrypted with a customer-managed KMS key in region {model.region}, so the fine-tuned weights rest under an AWS-owned key the organization cannot audit or revoke."
findings.append(report)

return findings
Loading