feat(aws): add bedrock_access_private_connectivity_enforced security check

Add new security check bedrock_access_private_connectivity_enforced for aws provider.
Includes check implementation, metadata, and unit tests.
This commit is contained in:
Daniel Barranquero
2026-04-08 13:53:43 +02:00
parent bc38104903
commit 64e45d85ce
16 changed files with 313 additions and 0 deletions

View File

@@ -18,6 +18,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- CISA SCuBA Google Workspace Baselines compliance [(#10466)](https://github.com/prowler-cloud/prowler/pull/10466)
- CIS Google Workspace Foundations Benchmark v1.3.0 compliance [(#10462)](https://github.com/prowler-cloud/prowler/pull/10462)
- `calendar_external_sharing_primary_calendar`, `calendar_external_sharing_secondary_calendar`, and `calendar_external_invitations_warning` checks for Google Workspace provider using the Cloud Identity Policy API [(#10597)](https://github.com/prowler-cloud/prowler/pull/10597)
- `bedrock_access_private_connectivity_enforced` check for aws provider [(#10611)](https://github.com/prowler-cloud/prowler/pull/10611)
- `entra_conditional_access_policy_device_registration_mfa_required` check and `entra_intune_enrollment_sign_in_frequency_every_time` enhancement for M365 provider [(#10222)](https://github.com/prowler-cloud/prowler/pull/10222)
- `entra_conditional_access_policy_block_elevated_insider_risk` check for M365 provider [(#10234)](https://github.com/prowler-cloud/prowler/pull/10234)
- `Vercel` provider support with 30 checks [(#10189)](https://github.com/prowler-cloud/prowler/pull/10189)

View File

@@ -396,6 +396,7 @@
}
],
"Checks": [
"bedrock_access_private_connectivity_enforced",
"vpc_subnet_no_public_ip_by_default",
"vpc_subnet_separate_private_public",
"vpc_endpoint_connections_trust_boundaries",

View File

@@ -664,6 +664,7 @@
"opensearch_service_domains_not_publicly_accessible",
"awslambda_function_not_publicly_accessible",
"apigateway_restapi_waf_acl_attached",
"bedrock_access_private_connectivity_enforced",
"cloudfront_distributions_using_waf",
"eks_cluster_not_publicly_accessible",
"sagemaker_models_network_isolation_enabled",

View File

@@ -7208,6 +7208,7 @@
}
],
"Checks": [
"bedrock_access_private_connectivity_enforced",
"workspaces_vpc_2private_1public_subnets_nat",
"vpc_endpoint_connections_trust_boundaries",
"networkfirewall_multi_az"

View File

@@ -130,6 +130,7 @@
"sagemaker_notebook_instance_without_direct_internet_access_configured",
"sagemaker_notebook_instance_encryption_enabled",
"secretsmanager_automatic_rotation_enabled",
"bedrock_access_private_connectivity_enforced",
"securityhub_enabled",
"sns_topics_kms_encryption_at_rest_enabled",
"vpc_endpoint_connections_trust_boundaries",

View File

@@ -4981,6 +4981,7 @@
}
],
"Checks": [
"bedrock_access_private_connectivity_enforced",
"ec2_securitygroup_default_restrict_traffic",
"vpc_subnet_separate_private_public",
"vpc_peering_routing_tables_with_least_privilege",

View File

@@ -1454,6 +1454,7 @@
"Checks": [
"awslambda_function_inside_vpc",
"awslambda_function_vpc_multi_az",
"bedrock_access_private_connectivity_enforced",
"cloudwatch_changes_to_vpcs_alarm_configured",
"ec2_transitgateway_auto_accept_vpc_attachments",
"networkfirewall_in_all_vpc",
@@ -1548,6 +1549,7 @@
"Checks": [
"awslambda_function_inside_vpc",
"awslambda_function_vpc_multi_az",
"bedrock_access_private_connectivity_enforced",
"cloudwatch_changes_to_vpcs_alarm_configured",
"ec2_transitgateway_auto_accept_vpc_attachments",
"networkfirewall_in_all_vpc",
@@ -1642,6 +1644,7 @@
"Checks": [
"awslambda_function_inside_vpc",
"awslambda_function_vpc_multi_az",
"bedrock_access_private_connectivity_enforced",
"cloudwatch_changes_to_vpcs_alarm_configured",
"ec2_transitgateway_auto_accept_vpc_attachments",
"networkfirewall_in_all_vpc",

View File

@@ -1614,6 +1614,7 @@
"sagemaker_notebook_instance_without_direct_internet_access_configured",
"sagemaker_training_jobs_network_isolation_enabled",
"sagemaker_training_jobs_vpc_settings_configured",
"bedrock_access_private_connectivity_enforced",
"vpc_endpoint_connections_trust_boundaries",
"vpc_endpoint_for_ec2_enabled",
"vpc_peering_routing_tables_with_least_privilege",
@@ -3284,6 +3285,7 @@
"storagegateway_fileshare_encryption_enabled",
"transfer_server_in_transit_encryption_enabled",
"trustedadvisor_errors_and_warnings",
"bedrock_access_private_connectivity_enforced",
"vpc_endpoint_connections_trust_boundaries",
"vpc_endpoint_for_ec2_enabled",
"vpc_endpoint_services_allowed_principals_trust_boundaries",

View File

@@ -1613,6 +1613,7 @@
"sagemaker_notebook_instance_without_direct_internet_access_configured",
"sagemaker_training_jobs_network_isolation_enabled",
"sagemaker_training_jobs_vpc_settings_configured",
"bedrock_access_private_connectivity_enforced",
"vpc_endpoint_connections_trust_boundaries",
"vpc_endpoint_for_ec2_enabled",
"vpc_peering_routing_tables_with_least_privilege",
@@ -3288,6 +3289,7 @@
"storagegateway_fileshare_encryption_enabled",
"transfer_server_in_transit_encryption_enabled",
"trustedadvisor_errors_and_warnings",
"bedrock_access_private_connectivity_enforced",
"vpc_endpoint_connections_trust_boundaries",
"vpc_endpoint_for_ec2_enabled",
"vpc_endpoint_services_allowed_principals_trust_boundaries",

View File

@@ -1368,6 +1368,7 @@
"Id": "6.8.2.a",
"Description": "consider the functional, logical and physical relationship, including location, between trustworthy systems and services;",
"Checks": [
"bedrock_access_private_connectivity_enforced",
"iam_role_cross_service_confused_deputy_prevention",
"vpc_endpoint_services_allowed_principals_trust_boundaries",
"vpc_endpoint_connections_trust_boundaries"

View File

@@ -803,6 +803,7 @@
}
],
"Checks": [
"bedrock_access_private_connectivity_enforced",
"vpc_subnet_different_az",
"vpc_subnet_separate_private_public",
"vpc_endpoint_connections_trust_boundaries",

View File

@@ -1025,6 +1025,7 @@
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
"ec2_networkacl_allow_ingress_any_port",
"bedrock_access_private_connectivity_enforced",
"vpc_subnet_separate_private_public",
"vpc_peering_routing_tables_with_least_privilege",
"vpc_endpoint_connections_trust_boundaries",

View File

@@ -0,0 +1,41 @@
{
"Provider": "aws",
"CheckID": "bedrock_access_private_connectivity_enforced",
"CheckTitle": "VPC has an Amazon Bedrock Runtime VPC endpoint to enforce private connectivity",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices/Network Reachability"
],
"ServiceName": "bedrock",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "AwsEc2VpcEndpointService",
"ResourceGroup": "network",
"Description": "**Amazon VPCs** are evaluated for an **interface VPC endpoint** to the **Amazon Bedrock Runtime API** (`bedrock-runtime`). Its presence indicates private Bedrock API connectivity over **AWS PrivateLink** within the VPC, keeping model invocation traffic off the public Internet.",
"Risk": "Without a private Bedrock Runtime endpoint, model invocation traffic exits via IGW/NAT. This expands exposure to network path threats (e.g., DNS hijack, MITM) and weakens egress isolation. It also creates an Internet egress dependency for API access, reducing **availability** if NAT/edge paths fail and weakening **confidentiality** of inference data in transit.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/bedrock/latest/userguide/usingVPC.html",
"https://docs.aws.amazon.com/bedrock/latest/userguide/vpc-interface-endpoints.html"
],
"Remediation": {
"Code": {
"CLI": "aws ec2 create-vpc-endpoint --vpc-id <VPC_ID> --service-name com.amazonaws.<REGION>.bedrock-runtime --vpc-endpoint-type Interface --subnet-ids <SUBNET_ID>",
"NativeIaC": "```yaml\n# CloudFormation: create a Bedrock Runtime interface VPC endpoint\nResources:\n BedrockRuntimeEndpoint:\n Type: AWS::EC2::VPCEndpoint\n Properties:\n VpcId: \"<example_resource_id>\" # CRITICAL: target VPC\n ServiceName: !Sub \"com.amazonaws.${AWS::Region}.bedrock-runtime\" # CRITICAL: Bedrock Runtime endpoint service\n VpcEndpointType: Interface # CRITICAL: interface endpoint\n SubnetIds: # CRITICAL: subnets for endpoint ENIs\n - \"<example_resource_id>\"\n```",
"Other": "1. In the AWS console, go to VPC > Endpoints\n2. Click Create endpoint\n3. For Service category, choose AWS services and select Service name com.amazonaws.<region>.bedrock-runtime\n4. Select your VPC and at least one subnet\n5. Click Create endpoint",
"Terraform": "```hcl\n# Create a Bedrock Runtime interface VPC endpoint\nresource \"aws_vpc_endpoint\" \"<example_resource_name>\" {\n vpc_id = \"<example_resource_id>\" # CRITICAL: target VPC\n service_name = \"com.amazonaws.<region>.bedrock-runtime\" # CRITICAL: Bedrock Runtime endpoint service\n vpc_endpoint_type = \"Interface\" # CRITICAL: interface endpoint\n subnet_ids = [\"<example_resource_id>\"] # CRITICAL: subnet(s) for endpoint ENIs\n}\n```"
},
"Recommendation": {
"Text": "Use an **interface VPC endpoint** for the Bedrock Runtime service in each VPC that requires Bedrock API access.\n- Enable **private DNS** to keep calls on the AWS network\n- Apply restrictive endpoint policies (**least privilege**)\n- Reduce reliance on public egress and layer controls for **defense in depth**",
"Url": "https://hub.prowler.com/check/bedrock_access_private_connectivity_enforced"
}
},
"Categories": [
"internet-exposed",
"trust-boundaries",
"gen-ai"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}

View File

@@ -0,0 +1,39 @@
"""Check for VPC endpoints enforcing private connectivity to Amazon Bedrock."""
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.vpc.vpc_client import vpc_client
class bedrock_access_private_connectivity_enforced(Check):
"""Ensure VPC endpoints enforce private connectivity for Amazon Bedrock.
This check evaluates whether each VPC has a VPC endpoint for the
Bedrock Runtime service to enforce private connectivity over AWS PrivateLink.
- PASS: The VPC has a Bedrock Runtime VPC endpoint configured.
- FAIL: The VPC does not have a Bedrock Runtime VPC endpoint configured.
"""
def execute(self) -> list[Check_Report_AWS]:
"""Execute the check logic.
Returns:
A list of reports containing the result of the check.
"""
findings = []
for vpc_id, vpc in vpc_client.vpcs.items():
if vpc_client.provider.scan_unused_services or vpc.in_use:
report = Check_Report_AWS(metadata=self.metadata(), resource=vpc)
report.status = "FAIL"
report.status_extended = f"VPC {vpc.id} does not have a Bedrock Runtime VPC endpoint configured to enforce private connectivity."
for endpoint in vpc_client.vpc_endpoints:
if (
endpoint.vpc_id == vpc_id
and "bedrock-runtime" in endpoint.service_name
):
report.status = "PASS"
report.status_extended = f"VPC {vpc.id} has a Bedrock Runtime {endpoint.type} VPC endpoint configured to enforce private connectivity."
break
findings.append(report)
return findings

View File

@@ -0,0 +1,217 @@
from unittest import mock
from boto3 import client
from moto import mock_aws
from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider
class Test_bedrock_access_private_connectivity_enforced:
@mock_aws
def test_no_resources(self):
"""Test when no VPCs exist (scan_unused_services=False and no in_use VPCs)."""
from prowler.providers.aws.services.vpc.vpc_service import VPC
aws_provider = set_mocked_aws_provider(
[AWS_REGION_US_EAST_1], scan_unused_services=False
)
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
mock.patch(
"prowler.providers.aws.services.bedrock.bedrock_access_private_connectivity_enforced.bedrock_access_private_connectivity_enforced.vpc_client",
new=VPC(aws_provider),
),
):
from prowler.providers.aws.services.bedrock.bedrock_access_private_connectivity_enforced.bedrock_access_private_connectivity_enforced import (
bedrock_access_private_connectivity_enforced,
)
check = bedrock_access_private_connectivity_enforced()
result = check.execute()
assert len(result) == 0
@mock_aws
def test_vpc_no_bedrock_endpoint(self):
"""Test FAIL: VPC exists but has no Bedrock Runtime VPC endpoint."""
ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1)
vpc = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]
from prowler.providers.aws.services.vpc.vpc_service import VPC
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_access_private_connectivity_enforced.bedrock_access_private_connectivity_enforced.vpc_client",
new=VPC(aws_provider),
),
):
from prowler.providers.aws.services.bedrock.bedrock_access_private_connectivity_enforced.bedrock_access_private_connectivity_enforced import (
bedrock_access_private_connectivity_enforced,
)
check = bedrock_access_private_connectivity_enforced()
result = check.execute()
# Default VPC + created VPC, both should FAIL
vpc_ids = [r.resource_id for r in result]
assert vpc["VpcId"] in vpc_ids
for finding in result:
if finding.resource_id == vpc["VpcId"]:
assert finding.status == "FAIL"
assert finding.region == AWS_REGION_US_EAST_1
assert (
finding.status_extended
== f"VPC {vpc['VpcId']} does not have a Bedrock Runtime VPC endpoint configured to enforce private connectivity."
)
assert (
finding.resource_arn
== f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:123456789012:vpc/{vpc['VpcId']}"
)
@mock_aws
def test_vpc_with_bedrock_runtime_endpoint(self):
"""Test PASS: VPC has a Bedrock Runtime VPC endpoint configured."""
ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1)
vpc = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]
route_table = ec2_client.create_route_table(VpcId=vpc["VpcId"])[
"RouteTable"
]
ec2_client.create_vpc_endpoint(
VpcId=vpc["VpcId"],
ServiceName="com.amazonaws.us-east-1.bedrock-runtime",
RouteTableIds=[route_table["RouteTableId"]],
VpcEndpointType="Interface",
)
from prowler.providers.aws.services.vpc.vpc_service import VPC
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_access_private_connectivity_enforced.bedrock_access_private_connectivity_enforced.vpc_client",
new=VPC(aws_provider),
),
):
from prowler.providers.aws.services.bedrock.bedrock_access_private_connectivity_enforced.bedrock_access_private_connectivity_enforced import (
bedrock_access_private_connectivity_enforced,
)
check = bedrock_access_private_connectivity_enforced()
result = check.execute()
# Find the result for our VPC
for finding in result:
if finding.resource_id == vpc["VpcId"]:
assert finding.status == "PASS"
assert finding.region == AWS_REGION_US_EAST_1
assert (
finding.status_extended
== f"VPC {vpc['VpcId']} has a Bedrock Runtime Interface VPC endpoint configured to enforce private connectivity."
)
assert (
finding.resource_arn
== f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:123456789012:vpc/{vpc['VpcId']}"
)
break
else:
raise AssertionError(
f"No finding found for VPC {vpc['VpcId']}"
)
@mock_aws
def test_vpc_with_non_bedrock_endpoint(self):
"""Test FAIL: VPC has a VPC endpoint but not for Bedrock Runtime."""
ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1)
vpc = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]
route_table = ec2_client.create_route_table(VpcId=vpc["VpcId"])[
"RouteTable"
]
ec2_client.create_vpc_endpoint(
VpcId=vpc["VpcId"],
ServiceName="com.amazonaws.us-east-1.s3",
RouteTableIds=[route_table["RouteTableId"]],
VpcEndpointType="Gateway",
)
from prowler.providers.aws.services.vpc.vpc_service import VPC
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_access_private_connectivity_enforced.bedrock_access_private_connectivity_enforced.vpc_client",
new=VPC(aws_provider),
),
):
from prowler.providers.aws.services.bedrock.bedrock_access_private_connectivity_enforced.bedrock_access_private_connectivity_enforced import (
bedrock_access_private_connectivity_enforced,
)
check = bedrock_access_private_connectivity_enforced()
result = check.execute()
for finding in result:
if finding.resource_id == vpc["VpcId"]:
assert finding.status == "FAIL"
assert finding.region == AWS_REGION_US_EAST_1
assert (
finding.status_extended
== f"VPC {vpc['VpcId']} does not have a Bedrock Runtime VPC endpoint configured to enforce private connectivity."
)
break
else:
raise AssertionError(
f"No finding found for VPC {vpc['VpcId']}"
)
@mock_aws
def test_vpc_not_in_use_scan_unused_false(self):
"""Test that unused VPCs are skipped when scan_unused_services is False."""
# The default VPC created by moto is not in_use
from prowler.providers.aws.services.vpc.vpc_service import VPC
aws_provider = set_mocked_aws_provider(
[AWS_REGION_US_EAST_1], scan_unused_services=False
)
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
mock.patch(
"prowler.providers.aws.services.bedrock.bedrock_access_private_connectivity_enforced.bedrock_access_private_connectivity_enforced.vpc_client",
new=VPC(aws_provider),
),
):
from prowler.providers.aws.services.bedrock.bedrock_access_private_connectivity_enforced.bedrock_access_private_connectivity_enforced import (
bedrock_access_private_connectivity_enforced,
)
check = bedrock_access_private_connectivity_enforced()
result = check.execute()
# Default VPC is not in_use and scan_unused_services is False
assert len(result) == 0