fix(aws): filter VPC endpoint services by audited account to prevent AccessDenied errors (#10152)

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jfagoagas <16007882+jfagoagas@users.noreply.github.com>
This commit is contained in:
Pepe Fagoaga
2026-02-24 17:30:31 +00:00
committed by GitHub
parent 2a4ee830cc
commit 6962622fd2
3 changed files with 194 additions and 8 deletions

View File

@@ -32,10 +32,6 @@ All notable changes to the **Prowler SDK** are documented in this file.
- CIS 6.0 for the AWS provider [(#10127)](https://github.com/prowler-cloud/prowler/pull/10127) - CIS 6.0 for the AWS provider [(#10127)](https://github.com/prowler-cloud/prowler/pull/10127)
- OpenStack provider multiple regions support [(#10135)](https://github.com/prowler-cloud/prowler/pull/10135) - OpenStack provider multiple regions support [(#10135)](https://github.com/prowler-cloud/prowler/pull/10135)
### 🐞 Fixed
- Standardize resource_id values across Azure checks to use actual Azure resource IDs and prevent duplicate resource entries [(#9994)](https://github.com/prowler-cloud/prowler/pull/9994)
### 🔄 Changed ### 🔄 Changed
- Update Azure Monitor service metadata to new format [(#9622)](https://github.com/prowler-cloud/prowler/pull/9622) - Update Azure Monitor service metadata to new format [(#9622)](https://github.com/prowler-cloud/prowler/pull/9622)
@@ -61,6 +57,8 @@ All notable changes to the **Prowler SDK** are documented in this file.
### 🐞 Fixed ### 🐞 Fixed
- Update AWS checks metadata URLs to replace deprecated Trend Micro CloudOne Conformity (EOL July 2026) with Vision One and remove docs.prowler.com references [(#10068)](https://github.com/prowler-cloud/prowler/pull/10068) - Update AWS checks metadata URLs to replace deprecated Trend Micro CloudOne Conformity (EOL July 2026) with Vision One and remove docs.prowler.com references [(#10068)](https://github.com/prowler-cloud/prowler/pull/10068)
- Standardize resource_id values across Azure checks to use actual Azure resource IDs and prevent duplicate resource entries [(#9994)](https://github.com/prowler-cloud/prowler/pull/9994)
- VPC endpoint service collection filtering third-party services that caused AccessDenied errors on `DescribeVpcEndpointServicePermissions` [(#10152)](https://github.com/prowler-cloud/prowler/pull/10152)
### 🔐 Security ### 🔐 Security

View File

@@ -264,7 +264,10 @@ class VPC(AWSService):
for page in describe_vpc_endpoint_services_paginator.paginate(): for page in describe_vpc_endpoint_services_paginator.paginate():
for endpoint in page["ServiceDetails"]: for endpoint in page["ServiceDetails"]:
try: try:
if endpoint["Owner"] != "amazon": # Only collect endpoint services owned by the audited account.
# The API returns ALL available services in the region,
# including Amazon and third-party ones we can't inspect.
if endpoint["Owner"] == self.audited_account:
arn = f"arn:{self.audited_partition}:ec2:{regional_client.region}:{self.audited_account}:vpc-endpoint-service/{endpoint['ServiceId']}" arn = f"arn:{self.audited_partition}:ec2:{regional_client.region}:{self.audited_account}:vpc-endpoint-service/{endpoint['ServiceId']}"
if not self.audit_resources or ( if not self.audit_resources or (
is_resource_filtered(arn, self.audit_resources) is_resource_filtered(arn, self.audit_resources)
@@ -303,9 +306,13 @@ class VPC(AWSService):
]: ]:
service.allowed_principals.append(principal["Principal"]) service.allowed_principals.append(principal["Principal"])
except ClientError as error: except ClientError as error:
if ( # AccessDenied/UnauthorizedOperation can occur if a
error.response["Error"]["Code"] # non-owned service slips through or permissions change
== "InvalidVpcEndpointServiceId.NotFound" # between collection and this call.
if error.response["Error"]["Code"] in (
"InvalidVpcEndpointServiceId.NotFound",
"AccessDenied",
"UnauthorizedOperation",
): ):
logger.warning( logger.warning(
f"{service.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" f"{service.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"

View File

@@ -3,6 +3,7 @@ import json
import botocore import botocore
import mock import mock
from boto3 import client, resource from boto3 import client, resource
from botocore.exceptions import ClientError
from moto import mock_aws from moto import mock_aws
from prowler.providers.aws.services.vpc.vpc_service import VPC, Route from prowler.providers.aws.services.vpc.vpc_service import VPC, Route
@@ -13,9 +14,125 @@ from tests.providers.aws.utils import (
set_mocked_aws_provider, set_mocked_aws_provider,
) )
THIRD_PARTY_ACCOUNT = "178579023202"
make_api_call = botocore.client.BaseClient._make_api_call make_api_call = botocore.client.BaseClient._make_api_call
def mock_make_api_call_endpoint_services(self, operation_name, kwarg):
"""Mock that returns VPC endpoint services from mixed owners:
audited account, amazon, and a third-party account."""
if operation_name == "DescribeVpcEndpointServices":
return {
"ServiceDetails": [
{
"ServiceId": "vpce-svc-owned123",
"ServiceName": "com.amazonaws.vpce.us-east-1.vpce-svc-owned123",
"ServiceType": [{"ServiceType": "Interface"}],
"Owner": AWS_ACCOUNT_NUMBER,
"Tags": [{"Key": "Name", "Value": "owned-service"}],
},
{
"ServiceId": "vpce-svc-amazon456",
"ServiceName": "com.amazonaws.us-east-1.s3",
"ServiceType": [{"ServiceType": "Gateway"}],
"Owner": "amazon",
"Tags": [],
},
{
"ServiceId": "vpce-svc-thirdparty789",
"ServiceName": "com.amazonaws.vpce.us-east-1.vpce-svc-thirdparty789",
"ServiceType": [{"ServiceType": "Interface"}],
"Owner": THIRD_PARTY_ACCOUNT,
"Tags": [],
},
],
"ServiceNames": [],
}
if operation_name == "DescribeVpcEndpointServicePermissions":
return {"AllowedPrincipals": []}
return make_api_call(self, operation_name, kwarg)
def mock_make_api_call_endpoint_services_access_denied(self, operation_name, kwarg):
"""Mock where DescribeVpcEndpointServicePermissions raises AccessDenied."""
if operation_name == "DescribeVpcEndpointServices":
return {
"ServiceDetails": [
{
"ServiceId": "vpce-svc-owned123",
"ServiceName": "com.amazonaws.vpce.us-east-1.vpce-svc-owned123",
"ServiceType": [{"ServiceType": "Interface"}],
"Owner": AWS_ACCOUNT_NUMBER,
"Tags": [],
},
],
"ServiceNames": [],
}
if operation_name == "DescribeVpcEndpointServicePermissions":
raise ClientError(
{"Error": {"Code": "AccessDenied", "Message": "Access denied"}},
operation_name,
)
return make_api_call(self, operation_name, kwarg)
def mock_make_api_call_endpoint_services_unauthorized(self, operation_name, kwarg):
"""Mock where DescribeVpcEndpointServicePermissions raises UnauthorizedOperation."""
if operation_name == "DescribeVpcEndpointServices":
return {
"ServiceDetails": [
{
"ServiceId": "vpce-svc-owned123",
"ServiceName": "com.amazonaws.vpce.us-east-1.vpce-svc-owned123",
"ServiceType": [{"ServiceType": "Interface"}],
"Owner": AWS_ACCOUNT_NUMBER,
"Tags": [],
},
],
"ServiceNames": [],
}
if operation_name == "DescribeVpcEndpointServicePermissions":
raise ClientError(
{
"Error": {
"Code": "UnauthorizedOperation",
"Message": "Unauthorized",
}
},
operation_name,
)
return make_api_call(self, operation_name, kwarg)
def mock_make_api_call_endpoint_services_not_found(self, operation_name, kwarg):
"""Mock where DescribeVpcEndpointServicePermissions raises InvalidVpcEndpointServiceId.NotFound."""
if operation_name == "DescribeVpcEndpointServices":
return {
"ServiceDetails": [
{
"ServiceId": "vpce-svc-owned123",
"ServiceName": "com.amazonaws.vpce.us-east-1.vpce-svc-owned123",
"ServiceType": [{"ServiceType": "Interface"}],
"Owner": AWS_ACCOUNT_NUMBER,
"Tags": [],
},
],
"ServiceNames": [],
}
if operation_name == "DescribeVpcEndpointServicePermissions":
raise ClientError(
{
"Error": {
"Code": "InvalidVpcEndpointServiceId.NotFound",
"Message": "Service not found",
}
},
operation_name,
)
return make_api_call(self, operation_name, kwarg)
def mock_make_api_call(self, operation_name, kwarg): def mock_make_api_call(self, operation_name, kwarg):
if operation_name == "DescribeVpnConnections": if operation_name == "DescribeVpnConnections":
return { return {
@@ -477,3 +594,67 @@ class Test_VPC_Service:
assert vpn_conn.region == AWS_REGION_US_EAST_1 assert vpn_conn.region == AWS_REGION_US_EAST_1
assert vpn_conn.arn == vpn_arn assert vpn_conn.arn == vpn_arn
assert len(vpn_conn.tunnels) == 2 assert len(vpn_conn.tunnels) == 2
# Test VPC Endpoint Services filters out third-party and Amazon-owned services
@mock.patch(
"botocore.client.BaseClient._make_api_call",
new=mock_make_api_call_endpoint_services,
)
def test_describe_vpc_endpoint_services_filters_third_party(self):
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
vpc = VPC(aws_provider)
# Only the service owned by the audited account should be collected
assert len(vpc.vpc_endpoint_services) == 1
assert vpc.vpc_endpoint_services[0].id == "vpce-svc-owned123"
assert vpc.vpc_endpoint_services[0].owner_id == AWS_ACCOUNT_NUMBER
assert vpc.vpc_endpoint_services[0].service == (
"com.amazonaws.vpce.us-east-1.vpce-svc-owned123"
)
assert vpc.vpc_endpoint_services[0].region == AWS_REGION_US_EAST_1
# Third-party service (178579023202) must NOT be in the list
for svc in vpc.vpc_endpoint_services:
assert svc.owner_id != THIRD_PARTY_ACCOUNT
assert svc.owner_id != "amazon"
# Test that AccessDenied in DescribeVpcEndpointServicePermissions is handled gracefully
@mock.patch(
"botocore.client.BaseClient._make_api_call",
new=mock_make_api_call_endpoint_services_access_denied,
)
def test_describe_vpc_endpoint_service_permissions_access_denied(self):
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
vpc = VPC(aws_provider)
assert len(vpc.vpc_endpoint_services) == 1
assert vpc.vpc_endpoint_services[0].id == "vpce-svc-owned123"
# allowed_principals must remain empty when AccessDenied is raised
assert vpc.vpc_endpoint_services[0].allowed_principals == []
# Test that UnauthorizedOperation in DescribeVpcEndpointServicePermissions is handled gracefully
@mock.patch(
"botocore.client.BaseClient._make_api_call",
new=mock_make_api_call_endpoint_services_unauthorized,
)
def test_describe_vpc_endpoint_service_permissions_unauthorized(self):
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
vpc = VPC(aws_provider)
assert len(vpc.vpc_endpoint_services) == 1
assert vpc.vpc_endpoint_services[0].id == "vpce-svc-owned123"
# allowed_principals must remain empty when UnauthorizedOperation is raised
assert vpc.vpc_endpoint_services[0].allowed_principals == []
# Test that InvalidVpcEndpointServiceId.NotFound in DescribeVpcEndpointServicePermissions is handled gracefully
@mock.patch(
"botocore.client.BaseClient._make_api_call",
new=mock_make_api_call_endpoint_services_not_found,
)
def test_describe_vpc_endpoint_service_permissions_not_found(self):
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
vpc = VPC(aws_provider)
assert len(vpc.vpc_endpoint_services) == 1
assert vpc.vpc_endpoint_services[0].id == "vpce-svc-owned123"
# allowed_principals must remain empty when service is not found
assert vpc.vpc_endpoint_services[0].allowed_principals == []