Skip to content

Commit 6962622

Browse files
jfagoagasCopilot
andauthored
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>
1 parent 2a4ee83 commit 6962622

File tree

3 files changed

+194
-8
lines changed

3 files changed

+194
-8
lines changed

prowler/CHANGELOG.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,6 @@ All notable changes to the **Prowler SDK** are documented in this file.
3232
- CIS 6.0 for the AWS provider [(#10127)](https://github.com/prowler-cloud/prowler/pull/10127)
3333
- OpenStack provider multiple regions support [(#10135)](https://github.com/prowler-cloud/prowler/pull/10135)
3434

35-
### 🐞 Fixed
36-
37-
- 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)
38-
3935
### 🔄 Changed
4036

4137
- 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.
6157
### 🐞 Fixed
6258

6359
- 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)
60+
- 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)
61+
- VPC endpoint service collection filtering third-party services that caused AccessDenied errors on `DescribeVpcEndpointServicePermissions` [(#10152)](https://github.com/prowler-cloud/prowler/pull/10152)
6462

6563
### 🔐 Security
6664

prowler/providers/aws/services/vpc/vpc_service.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,10 @@ def _describe_vpc_endpoint_services(self, regional_client):
264264
for page in describe_vpc_endpoint_services_paginator.paginate():
265265
for endpoint in page["ServiceDetails"]:
266266
try:
267-
if endpoint["Owner"] != "amazon":
267+
# Only collect endpoint services owned by the audited account.
268+
# The API returns ALL available services in the region,
269+
# including Amazon and third-party ones we can't inspect.
270+
if endpoint["Owner"] == self.audited_account:
268271
arn = f"arn:{self.audited_partition}:ec2:{regional_client.region}:{self.audited_account}:vpc-endpoint-service/{endpoint['ServiceId']}"
269272
if not self.audit_resources or (
270273
is_resource_filtered(arn, self.audit_resources)
@@ -303,9 +306,13 @@ def _describe_vpc_endpoint_service_permissions(self):
303306
]:
304307
service.allowed_principals.append(principal["Principal"])
305308
except ClientError as error:
306-
if (
307-
error.response["Error"]["Code"]
308-
== "InvalidVpcEndpointServiceId.NotFound"
309+
# AccessDenied/UnauthorizedOperation can occur if a
310+
# non-owned service slips through or permissions change
311+
# between collection and this call.
312+
if error.response["Error"]["Code"] in (
313+
"InvalidVpcEndpointServiceId.NotFound",
314+
"AccessDenied",
315+
"UnauthorizedOperation",
309316
):
310317
logger.warning(
311318
f"{service.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"

tests/providers/aws/services/vpc/vpc_service_test.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import botocore
44
import mock
55
from boto3 import client, resource
6+
from botocore.exceptions import ClientError
67
from moto import mock_aws
78

89
from prowler.providers.aws.services.vpc.vpc_service import VPC, Route
@@ -13,9 +14,125 @@
1314
set_mocked_aws_provider,
1415
)
1516

17+
THIRD_PARTY_ACCOUNT = "178579023202"
18+
1619
make_api_call = botocore.client.BaseClient._make_api_call
1720

1821

22+
def mock_make_api_call_endpoint_services(self, operation_name, kwarg):
23+
"""Mock that returns VPC endpoint services from mixed owners:
24+
audited account, amazon, and a third-party account."""
25+
if operation_name == "DescribeVpcEndpointServices":
26+
return {
27+
"ServiceDetails": [
28+
{
29+
"ServiceId": "vpce-svc-owned123",
30+
"ServiceName": "com.amazonaws.vpce.us-east-1.vpce-svc-owned123",
31+
"ServiceType": [{"ServiceType": "Interface"}],
32+
"Owner": AWS_ACCOUNT_NUMBER,
33+
"Tags": [{"Key": "Name", "Value": "owned-service"}],
34+
},
35+
{
36+
"ServiceId": "vpce-svc-amazon456",
37+
"ServiceName": "com.amazonaws.us-east-1.s3",
38+
"ServiceType": [{"ServiceType": "Gateway"}],
39+
"Owner": "amazon",
40+
"Tags": [],
41+
},
42+
{
43+
"ServiceId": "vpce-svc-thirdparty789",
44+
"ServiceName": "com.amazonaws.vpce.us-east-1.vpce-svc-thirdparty789",
45+
"ServiceType": [{"ServiceType": "Interface"}],
46+
"Owner": THIRD_PARTY_ACCOUNT,
47+
"Tags": [],
48+
},
49+
],
50+
"ServiceNames": [],
51+
}
52+
if operation_name == "DescribeVpcEndpointServicePermissions":
53+
return {"AllowedPrincipals": []}
54+
return make_api_call(self, operation_name, kwarg)
55+
56+
57+
def mock_make_api_call_endpoint_services_access_denied(self, operation_name, kwarg):
58+
"""Mock where DescribeVpcEndpointServicePermissions raises AccessDenied."""
59+
if operation_name == "DescribeVpcEndpointServices":
60+
return {
61+
"ServiceDetails": [
62+
{
63+
"ServiceId": "vpce-svc-owned123",
64+
"ServiceName": "com.amazonaws.vpce.us-east-1.vpce-svc-owned123",
65+
"ServiceType": [{"ServiceType": "Interface"}],
66+
"Owner": AWS_ACCOUNT_NUMBER,
67+
"Tags": [],
68+
},
69+
],
70+
"ServiceNames": [],
71+
}
72+
if operation_name == "DescribeVpcEndpointServicePermissions":
73+
raise ClientError(
74+
{"Error": {"Code": "AccessDenied", "Message": "Access denied"}},
75+
operation_name,
76+
)
77+
return make_api_call(self, operation_name, kwarg)
78+
79+
80+
def mock_make_api_call_endpoint_services_unauthorized(self, operation_name, kwarg):
81+
"""Mock where DescribeVpcEndpointServicePermissions raises UnauthorizedOperation."""
82+
if operation_name == "DescribeVpcEndpointServices":
83+
return {
84+
"ServiceDetails": [
85+
{
86+
"ServiceId": "vpce-svc-owned123",
87+
"ServiceName": "com.amazonaws.vpce.us-east-1.vpce-svc-owned123",
88+
"ServiceType": [{"ServiceType": "Interface"}],
89+
"Owner": AWS_ACCOUNT_NUMBER,
90+
"Tags": [],
91+
},
92+
],
93+
"ServiceNames": [],
94+
}
95+
if operation_name == "DescribeVpcEndpointServicePermissions":
96+
raise ClientError(
97+
{
98+
"Error": {
99+
"Code": "UnauthorizedOperation",
100+
"Message": "Unauthorized",
101+
}
102+
},
103+
operation_name,
104+
)
105+
return make_api_call(self, operation_name, kwarg)
106+
107+
108+
def mock_make_api_call_endpoint_services_not_found(self, operation_name, kwarg):
109+
"""Mock where DescribeVpcEndpointServicePermissions raises InvalidVpcEndpointServiceId.NotFound."""
110+
if operation_name == "DescribeVpcEndpointServices":
111+
return {
112+
"ServiceDetails": [
113+
{
114+
"ServiceId": "vpce-svc-owned123",
115+
"ServiceName": "com.amazonaws.vpce.us-east-1.vpce-svc-owned123",
116+
"ServiceType": [{"ServiceType": "Interface"}],
117+
"Owner": AWS_ACCOUNT_NUMBER,
118+
"Tags": [],
119+
},
120+
],
121+
"ServiceNames": [],
122+
}
123+
if operation_name == "DescribeVpcEndpointServicePermissions":
124+
raise ClientError(
125+
{
126+
"Error": {
127+
"Code": "InvalidVpcEndpointServiceId.NotFound",
128+
"Message": "Service not found",
129+
}
130+
},
131+
operation_name,
132+
)
133+
return make_api_call(self, operation_name, kwarg)
134+
135+
19136
def mock_make_api_call(self, operation_name, kwarg):
20137
if operation_name == "DescribeVpnConnections":
21138
return {
@@ -477,3 +594,67 @@ def test_describe_vpn_connections(self):
477594
assert vpn_conn.region == AWS_REGION_US_EAST_1
478595
assert vpn_conn.arn == vpn_arn
479596
assert len(vpn_conn.tunnels) == 2
597+
598+
# Test VPC Endpoint Services filters out third-party and Amazon-owned services
599+
@mock.patch(
600+
"botocore.client.BaseClient._make_api_call",
601+
new=mock_make_api_call_endpoint_services,
602+
)
603+
def test_describe_vpc_endpoint_services_filters_third_party(self):
604+
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
605+
vpc = VPC(aws_provider)
606+
607+
# Only the service owned by the audited account should be collected
608+
assert len(vpc.vpc_endpoint_services) == 1
609+
assert vpc.vpc_endpoint_services[0].id == "vpce-svc-owned123"
610+
assert vpc.vpc_endpoint_services[0].owner_id == AWS_ACCOUNT_NUMBER
611+
assert vpc.vpc_endpoint_services[0].service == (
612+
"com.amazonaws.vpce.us-east-1.vpce-svc-owned123"
613+
)
614+
assert vpc.vpc_endpoint_services[0].region == AWS_REGION_US_EAST_1
615+
# Third-party service (178579023202) must NOT be in the list
616+
for svc in vpc.vpc_endpoint_services:
617+
assert svc.owner_id != THIRD_PARTY_ACCOUNT
618+
assert svc.owner_id != "amazon"
619+
620+
# Test that AccessDenied in DescribeVpcEndpointServicePermissions is handled gracefully
621+
@mock.patch(
622+
"botocore.client.BaseClient._make_api_call",
623+
new=mock_make_api_call_endpoint_services_access_denied,
624+
)
625+
def test_describe_vpc_endpoint_service_permissions_access_denied(self):
626+
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
627+
vpc = VPC(aws_provider)
628+
629+
assert len(vpc.vpc_endpoint_services) == 1
630+
assert vpc.vpc_endpoint_services[0].id == "vpce-svc-owned123"
631+
# allowed_principals must remain empty when AccessDenied is raised
632+
assert vpc.vpc_endpoint_services[0].allowed_principals == []
633+
634+
# Test that UnauthorizedOperation in DescribeVpcEndpointServicePermissions is handled gracefully
635+
@mock.patch(
636+
"botocore.client.BaseClient._make_api_call",
637+
new=mock_make_api_call_endpoint_services_unauthorized,
638+
)
639+
def test_describe_vpc_endpoint_service_permissions_unauthorized(self):
640+
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
641+
vpc = VPC(aws_provider)
642+
643+
assert len(vpc.vpc_endpoint_services) == 1
644+
assert vpc.vpc_endpoint_services[0].id == "vpce-svc-owned123"
645+
# allowed_principals must remain empty when UnauthorizedOperation is raised
646+
assert vpc.vpc_endpoint_services[0].allowed_principals == []
647+
648+
# Test that InvalidVpcEndpointServiceId.NotFound in DescribeVpcEndpointServicePermissions is handled gracefully
649+
@mock.patch(
650+
"botocore.client.BaseClient._make_api_call",
651+
new=mock_make_api_call_endpoint_services_not_found,
652+
)
653+
def test_describe_vpc_endpoint_service_permissions_not_found(self):
654+
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
655+
vpc = VPC(aws_provider)
656+
657+
assert len(vpc.vpc_endpoint_services) == 1
658+
assert vpc.vpc_endpoint_services[0].id == "vpce-svc-owned123"
659+
# allowed_principals must remain empty when service is not found
660+
assert vpc.vpc_endpoint_services[0].allowed_principals == []

0 commit comments

Comments
 (0)