Skip to content

Commit 9d7b9c3

Browse files
feat(gcp): Add VPC Service Controls check for Cloud Storage (#9256)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
1 parent 127b8d8 commit 9d7b9c3

File tree

13 files changed

+806
-2
lines changed

13 files changed

+806
-2
lines changed

prowler/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
All notable changes to the **Prowler SDK** are documented in this file.
44

5+
## [v5.15.0] (Prowler UNRELEASED)
6+
7+
### Added
8+
- `cloudstorage_uses_vpc_service_controls` check for GCP provider [(#9256)](https://github.com/prowler-cloud/prowler/pull/9256)
9+
10+
---
11+
512
## [v5.14.1] (Prowler UNRELEASED)
613

714
### Fixed

prowler/providers/gcp/services/accesscontextmanager/__init__.py

Whitespace-only changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from prowler.providers.common.provider import Provider
2+
from prowler.providers.gcp.services.accesscontextmanager.accesscontextmanager_service import (
3+
AccessContextManager,
4+
)
5+
6+
accesscontextmanager_client = AccessContextManager(Provider.get_global_provider())
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
from pydantic.v1 import BaseModel
2+
3+
import prowler.providers.gcp.config as config
4+
from prowler.lib.logger import logger
5+
from prowler.providers.gcp.gcp_provider import GcpProvider
6+
from prowler.providers.gcp.lib.service.service import GCPService
7+
from prowler.providers.gcp.services.cloudresourcemanager.cloudresourcemanager_client import (
8+
cloudresourcemanager_client,
9+
)
10+
11+
12+
class AccessContextManager(GCPService):
13+
def __init__(self, provider: GcpProvider):
14+
super().__init__("accesscontextmanager", provider, api_version="v1")
15+
self.service_perimeters = []
16+
self._get_service_perimeters()
17+
18+
def _get_service_perimeters(self):
19+
for org in cloudresourcemanager_client.organizations:
20+
try:
21+
access_policies = []
22+
try:
23+
request = self.client.accessPolicies().list(
24+
parent=f"organizations/{org.id}"
25+
)
26+
while request is not None:
27+
response = request.execute(
28+
num_retries=config.DEFAULT_RETRY_ATTEMPTS
29+
)
30+
access_policies.extend(response.get("accessPolicies", []))
31+
32+
request = self.client.accessPolicies().list_next(
33+
previous_request=request, previous_response=response
34+
)
35+
except Exception as error:
36+
logger.error(
37+
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
38+
)
39+
continue
40+
41+
for policy in access_policies:
42+
try:
43+
request = (
44+
self.client.accessPolicies()
45+
.servicePerimeters()
46+
.list(parent=policy["name"])
47+
)
48+
while request is not None:
49+
response = request.execute(
50+
num_retries=config.DEFAULT_RETRY_ATTEMPTS
51+
)
52+
53+
for perimeter in response.get("servicePerimeters", []):
54+
status = perimeter.get("status", {})
55+
spec = perimeter.get("spec", {})
56+
57+
perimeter_config = status if status else spec
58+
59+
resources = perimeter_config.get("resources", [])
60+
restricted_services = perimeter_config.get(
61+
"restrictedServices", []
62+
)
63+
64+
self.service_perimeters.append(
65+
ServicePerimeter(
66+
name=perimeter["name"],
67+
title=perimeter.get("title", ""),
68+
perimeter_type=perimeter.get(
69+
"perimeterType", ""
70+
),
71+
resources=resources,
72+
restricted_services=restricted_services,
73+
policy_name=policy["name"],
74+
)
75+
)
76+
77+
request = (
78+
self.client.accessPolicies()
79+
.servicePerimeters()
80+
.list_next(
81+
previous_request=request, previous_response=response
82+
)
83+
)
84+
except Exception as error:
85+
logger.error(
86+
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
87+
)
88+
89+
except Exception as error:
90+
logger.error(
91+
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
92+
)
93+
94+
95+
class ServicePerimeter(BaseModel):
96+
name: str
97+
title: str
98+
perimeter_type: str
99+
resources: list[str]
100+
restricted_services: list[str]
101+
policy_name: str

prowler/providers/gcp/services/cloudresourcemanager/cloudresourcemanager_service.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ def __init__(self, provider: GcpProvider):
1919
def _get_iam_policy(self):
2020
for project_id in self.project_ids:
2121
try:
22+
# Get project details to obtain project number
23+
project_details = (
24+
self.client.projects()
25+
.get(projectId=project_id)
26+
.execute(num_retries=DEFAULT_RETRY_ATTEMPTS)
27+
)
28+
project_number = project_details.get("projectNumber", "")
29+
2230
policy = (
2331
self.client.projects()
2432
.getIamPolicy(resource=project_id)
@@ -41,6 +49,7 @@ def _get_iam_policy(self):
4149
self.cloud_resource_manager_projects.append(
4250
Project(
4351
id=project_id,
52+
number=project_number,
4453
audit_logging=audit_logging,
4554
audit_configs=audit_configs,
4655
)
@@ -96,6 +105,7 @@ class Binding(BaseModel):
96105

97106
class Project(BaseModel):
98107
id: str
108+
number: str = ""
99109
audit_logging: bool
100110
audit_configs: list[AuditConfig] = []
101111

prowler/providers/gcp/services/cloudstorage/cloudstorage_uses_vpc_service_controls/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"Provider": "gcp",
3+
"CheckID": "cloudstorage_uses_vpc_service_controls",
4+
"CheckTitle": "Cloud Storage services are protected by VPC Service Controls",
5+
"CheckType": [],
6+
"ServiceName": "cloudstorage",
7+
"SubServiceName": "",
8+
"ResourceIdTemplate": "",
9+
"Severity": "medium",
10+
"ResourceType": "cloudresourcemanager.googleapis.com/Project",
11+
"Description": "**GCP Projects** are evaluated to ensure they have **VPC Service Controls** enabled for Cloud Storage. VPC Service Controls establish security boundaries by restricting access to Cloud Storage resources to specific networks and trusted clients, preventing unauthorized data access and exfiltration.",
12+
"Risk": "Projects without VPC Service Controls protection for Cloud Storage may be vulnerable to unauthorized data access and exfiltration, even with proper IAM policies in place. VPC Service Controls provide an additional layer of network-level security that restricts API access based on the context of the request.",
13+
"RelatedUrl": "",
14+
"AdditionalURLs": [
15+
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudStorage/use-vpc-service-controls.html",
16+
"https://cloud.google.com/vpc-service-controls/docs/create-service-perimeters"
17+
],
18+
"Remediation": {
19+
"Code": {
20+
"CLI": "",
21+
"NativeIaC": "",
22+
"Other": "1) Open Google Cloud Console → Security → VPC Service Controls\n2) Create a new service perimeter or select an existing one\n3) Add the relevant GCP projects to the perimeter's protected resources\n4) Add 'storage.googleapis.com' to the list of restricted services\n5) Configure appropriate ingress and egress rules\n6) Save the perimeter configuration",
23+
"Terraform": ""
24+
},
25+
"Recommendation": {
26+
"Text": "Enable VPC Service Controls for all Cloud Storage buckets by adding their projects to a service perimeter with storage.googleapis.com as a restricted service. This prevents data exfiltration and ensures API calls are only allowed from authorized networks.",
27+
"Url": "https://hub.prowler.com/check/cloudstorage_uses_vpc_service_controls"
28+
}
29+
},
30+
"Categories": [
31+
"internet-exposed"
32+
],
33+
"DependsOn": [],
34+
"RelatedTo": [],
35+
"Notes": ""
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from prowler.lib.check.models import Check, Check_Report_GCP
2+
from prowler.providers.gcp.services.accesscontextmanager.accesscontextmanager_client import (
3+
accesscontextmanager_client,
4+
)
5+
from prowler.providers.gcp.services.cloudresourcemanager.cloudresourcemanager_client import (
6+
cloudresourcemanager_client,
7+
)
8+
9+
10+
class cloudstorage_uses_vpc_service_controls(Check):
11+
"""
12+
Ensure Cloud Storage is protected by VPC Service Controls at project level.
13+
14+
Reports PASS if a project is in a VPC Service Controls perimeter
15+
with storage.googleapis.com as a restricted service, otherwise FAIL.
16+
"""
17+
18+
def execute(self) -> list[Check_Report_GCP]:
19+
findings = []
20+
21+
protected_projects = {}
22+
for perimeter in accesscontextmanager_client.service_perimeters:
23+
if any(
24+
service == "storage.googleapis.com"
25+
for service in perimeter.restricted_services
26+
):
27+
for resource in perimeter.resources:
28+
protected_projects[resource] = perimeter.title
29+
30+
for project in cloudresourcemanager_client.cloud_resource_manager_projects:
31+
report = Check_Report_GCP(
32+
metadata=self.metadata(),
33+
resource=cloudresourcemanager_client.projects[project.id],
34+
project_id=project.id,
35+
location=cloudresourcemanager_client.region,
36+
resource_name=(
37+
cloudresourcemanager_client.projects[project.id].name
38+
if cloudresourcemanager_client.projects[project.id].name
39+
else "GCP Project"
40+
),
41+
)
42+
report.status = "FAIL"
43+
report.status_extended = f"Project {project.id} does not have VPC Service Controls enabled for Cloud Storage."
44+
# GCP stores resources by project number, not project ID
45+
project_resource_id = f"projects/{project.number}"
46+
47+
if project_resource_id in protected_projects:
48+
report.status = "PASS"
49+
report.status_extended = f"Project {project.id} has VPC Service Controls enabled for Cloud Storage in perimeter {protected_projects[project_resource_id]}."
50+
51+
findings.append(report)
52+
53+
return findings

tests/providers/gcp/gcp_fixtures.py

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def mock_api_client(GCPService, service, api_version, _):
5656
mock_api_policies_calls(client)
5757
mock_api_sink_calls(client)
5858
mock_api_services_calls(client)
59+
mock_api_access_policies_calls(client)
5960

6061
return client
6162

@@ -117,8 +118,9 @@ def mock_api_projects_calls(client: MagicMock):
117118
"etag": "BwWWja0YfJA=",
118119
"version": 3,
119120
}
120-
# Used by compute client
121+
# Used by compute client and cloudresourcemanager
121122
client.projects().get().execute.return_value = {
123+
"projectNumber": "123456789012",
122124
"commonInstanceMetadata": {
123125
"items": [
124126
{
@@ -134,7 +136,7 @@ def mock_api_projects_calls(client: MagicMock):
134136
"value": "TRUE",
135137
},
136138
]
137-
}
139+
},
138140
}
139141
client.projects().list_next.return_value = None
140142
# Used by dataproc client
@@ -1100,3 +1102,75 @@ def mock_api_services_calls(client: MagicMock):
11001102
]
11011103
}
11021104
client.services().list_next.return_value = None
1105+
1106+
1107+
def mock_api_access_policies_calls(client: MagicMock):
1108+
# Mock access policies list based on parent organization
1109+
def mock_list_access_policies(parent):
1110+
return_value = MagicMock()
1111+
# Only return policies for the first organization (123456789)
1112+
if parent == "organizations/123456789":
1113+
return_value.execute.return_value = {
1114+
"accessPolicies": [
1115+
{
1116+
"name": "accessPolicies/123456",
1117+
"title": "Test Access Policy 1",
1118+
},
1119+
{
1120+
"name": "accessPolicies/789012",
1121+
"title": "Test Access Policy 2",
1122+
},
1123+
]
1124+
}
1125+
elif parent == "organizations/987654321":
1126+
# No policies for the second organization
1127+
return_value.execute.return_value = {"accessPolicies": []}
1128+
else:
1129+
return_value.execute.return_value = {"accessPolicies": []}
1130+
return return_value
1131+
1132+
client.accessPolicies().list = mock_list_access_policies
1133+
client.accessPolicies().list_next.return_value = None
1134+
1135+
# Mock service perimeters list based on parent access policy
1136+
def mock_list_service_perimeters(parent):
1137+
return_value = MagicMock()
1138+
if parent == "accessPolicies/123456":
1139+
return_value.execute.return_value = {
1140+
"servicePerimeters": [
1141+
{
1142+
"name": "accessPolicies/123456/servicePerimeters/perimeter1",
1143+
"title": "Test Perimeter 1",
1144+
"perimeterType": "PERIMETER_TYPE_REGULAR",
1145+
"status": {
1146+
"resources": [
1147+
f"projects/{GCP_PROJECT_ID}",
1148+
],
1149+
"restrictedServices": [
1150+
"storage.googleapis.com",
1151+
"bigquery.googleapis.com",
1152+
],
1153+
},
1154+
},
1155+
{
1156+
"name": "accessPolicies/123456/servicePerimeters/perimeter2",
1157+
"title": "Test Perimeter 2",
1158+
"perimeterType": "PERIMETER_TYPE_BRIDGE",
1159+
"spec": {
1160+
"resources": [],
1161+
"restrictedServices": [
1162+
"compute.googleapis.com",
1163+
],
1164+
},
1165+
},
1166+
]
1167+
}
1168+
elif parent == "accessPolicies/789012":
1169+
# No perimeters for the second policy
1170+
return_value.execute.return_value = {"servicePerimeters": []}
1171+
else:
1172+
return_value.execute.return_value = {"servicePerimeters": []}
1173+
return return_value
1174+
1175+
client.accessPolicies().servicePerimeters().list = mock_list_service_perimeters
1176+
client.accessPolicies().servicePerimeters().list_next.return_value = None

0 commit comments

Comments
 (0)