mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-01-25 02:08:11 +00:00
feat(gcp): add check to ensure Compute Engine disk images are not publicly shared (#9718)
This commit is contained in:
@@ -13,6 +13,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- Bedrock service pagination [(#9606)](https://github.com/prowler-cloud/prowler/pull/9606)
|
||||
- `ResourceGroup` field to all check metadata for resource classification [(#9656)](https://github.com/prowler-cloud/prowler/pull/9656)
|
||||
- `compute_instance_group_load_balancer_attached` check for GCP provider [(#9695)](https://github.com/prowler-cloud/prowler/pull/9695)
|
||||
- `compute_image_not_publicly_shared` check for GCP provider [(#9718)](https://github.com/prowler-cloud/prowler/pull/9718)
|
||||
|
||||
### Changed
|
||||
- Update AWS Step Functions service metadata to new format [(#9432)](https://github.com/prowler-cloud/prowler/pull/9432)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"Provider": "gcp",
|
||||
"CheckID": "compute_image_not_publicly_shared",
|
||||
"CheckTitle": "Compute Engine disk image is not publicly shared",
|
||||
"CheckType": [],
|
||||
"ServiceName": "compute",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "compute.googleapis.com/Image",
|
||||
"ResourceGroup": "compute",
|
||||
"Description": "Custom disk images should not be shared publicly with **allAuthenticatedUsers**.\n\nNote: Per Google Cloud API restrictions, **allUsers** cannot be assigned to Compute Engine images. The security concern is **allAuthenticatedUsers**, which grants access to anyone with a Google account.\n\nPublicly shared disk images can expose application snapshots and sensitive data to anyone with a Google Cloud account, potentially leading to unauthorized access and data breaches.",
|
||||
"Risk": "Publicly shared disk images can expose **sensitive data** and application configurations to unauthorized users.\n\n- Any authenticated GCP user can access the image content\n- Could lead to **data breaches** if images contain secrets or proprietary code\n- Attackers may use exposed images to understand application architecture",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://cloud.google.com/compute/docs/images/managing-access-custom-images",
|
||||
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/publicly-shared-disk-images.html"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "gcloud compute images remove-iam-policy-binding IMAGE_NAME --member='allAuthenticatedUsers' --role='ROLE_NAME'",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. Go to the GCP Console\n2. Navigate to Compute Engine > Images\n3. Select the disk image\n4. Click on the INFO PANEL to view permissions\n5. Remove **allAuthenticatedUsers** bindings\n6. Click Save",
|
||||
"Terraform": "```hcl\nresource \"google_compute_image_iam_binding\" \"example_resource\" {\n project = \"your-project-id\"\n image = \"your-image-name\"\n role = \"roles/compute.imageUser\"\n # Remove allAuthenticatedUsers and grant access only to specific members\n members = [\n \"user:specific-user@example.com\",\n ]\n}\n```"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Restrict access to custom disk images by removing the **allAuthenticatedUsers** IAM binding. Apply the principle of least privilege by granting access only to specific users, groups, or service accounts that require it.",
|
||||
"Url": "https://hub.prowler.com/check/compute_image_not_publicly_shared"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"internet-exposed"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_GCP
|
||||
from prowler.providers.gcp.services.compute.compute_client import compute_client
|
||||
|
||||
|
||||
class compute_image_not_publicly_shared(Check):
|
||||
"""Ensure Compute Engine disk images are not publicly shared.
|
||||
|
||||
This check evaluates whether custom disk images in GCP Compute Engine
|
||||
have IAM bindings that grant access to allAuthenticatedUsers, which allows
|
||||
anyone with a Google account to access the image.
|
||||
|
||||
Note: allUsers cannot be assigned to Compute Engine images (API restriction).
|
||||
Only allAuthenticatedUsers can be set, which is the security risk.
|
||||
Reference: https://cloud.google.com/compute/docs/images/managing-access-custom-images
|
||||
|
||||
- PASS: The disk image is not publicly shared.
|
||||
- FAIL: The disk image is publicly shared with allAuthenticatedUsers.
|
||||
"""
|
||||
|
||||
def execute(self) -> list[Check_Report_GCP]:
|
||||
findings = []
|
||||
for image in compute_client.images:
|
||||
report = Check_Report_GCP(
|
||||
metadata=self.metadata(),
|
||||
resource=image,
|
||||
location="global",
|
||||
)
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Compute Engine disk image {image.name} is not publicly shared."
|
||||
)
|
||||
|
||||
if image.publicly_shared:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Compute Engine disk image {image.name} is publicly shared with allAuthenticatedUsers."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -21,6 +21,7 @@ class Compute(GCPService):
|
||||
self.compute_projects = []
|
||||
self.load_balancers = []
|
||||
self.instance_groups = []
|
||||
self.images = []
|
||||
self._get_regions()
|
||||
self._get_projects()
|
||||
self._get_url_maps()
|
||||
@@ -34,6 +35,7 @@ class Compute(GCPService):
|
||||
self.__threading_call__(self._get_regional_instance_groups, self.regions)
|
||||
self.__threading_call__(self._get_zonal_instance_groups, self.zones)
|
||||
self._associate_migs_with_load_balancers()
|
||||
self._get_images()
|
||||
|
||||
def _get_regions(self):
|
||||
for project_id in self.project_ids:
|
||||
@@ -533,6 +535,52 @@ class Compute(GCPService):
|
||||
if (mig.project_id, mig.name) in load_balanced_groups:
|
||||
mig.load_balanced = True
|
||||
|
||||
def _get_images(self) -> None:
|
||||
for project_id in self.project_ids:
|
||||
try:
|
||||
request = self.client.images().list(project=project_id)
|
||||
while request is not None:
|
||||
response = request.execute(num_retries=DEFAULT_RETRY_ATTEMPTS)
|
||||
for image in response.get("items", []):
|
||||
publicly_shared = False
|
||||
try:
|
||||
iam_policy = (
|
||||
self.client.images()
|
||||
.getIamPolicy(
|
||||
project=project_id, resource=image["name"]
|
||||
)
|
||||
.execute(num_retries=DEFAULT_RETRY_ATTEMPTS)
|
||||
)
|
||||
for binding in iam_policy.get("bindings", []):
|
||||
# allUsers cannot be assigned to Compute Engine images (API restriction).
|
||||
# Only allAuthenticatedUsers can be set, which is the security risk.
|
||||
if "allAuthenticatedUsers" in binding.get(
|
||||
"members", []
|
||||
):
|
||||
publicly_shared = True
|
||||
break
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{project_id}/{image['name']} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
self.images.append(
|
||||
Image(
|
||||
name=image["name"],
|
||||
id=image["id"],
|
||||
project_id=project_id,
|
||||
publicly_shared=publicly_shared,
|
||||
)
|
||||
)
|
||||
|
||||
request = self.client.images().list_next(
|
||||
previous_request=request, previous_response=response
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{project_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
class Disk(BaseModel):
|
||||
name: str
|
||||
@@ -625,3 +673,10 @@ class ManagedInstanceGroup(BaseModel):
|
||||
project_id: str
|
||||
auto_healing_policies: list[AutoHealingPolicy] = []
|
||||
load_balanced: bool = False
|
||||
|
||||
|
||||
class Image(BaseModel):
|
||||
name: str
|
||||
id: str
|
||||
project_id: str
|
||||
publicly_shared: bool = False
|
||||
|
||||
@@ -58,6 +58,7 @@ def mock_api_client(GCPService, service, api_version, _):
|
||||
mock_api_services_calls(client)
|
||||
mock_api_access_policies_calls(client)
|
||||
mock_api_instance_group_managers_calls(client)
|
||||
mock_api_images_calls(client)
|
||||
|
||||
return client
|
||||
|
||||
@@ -1260,3 +1261,53 @@ def mock_api_instance_group_managers_calls(client: MagicMock):
|
||||
]
|
||||
}
|
||||
client.instanceGroupManagers().list_next.return_value = None
|
||||
|
||||
|
||||
def mock_api_images_calls(client: MagicMock):
|
||||
image1_id = str(uuid4())
|
||||
image2_id = str(uuid4())
|
||||
image3_id = str(uuid4())
|
||||
|
||||
client.images().list().execute.return_value = {
|
||||
"items": [
|
||||
{
|
||||
"name": "test-image-1",
|
||||
"id": image1_id,
|
||||
},
|
||||
{
|
||||
"name": "test-image-2",
|
||||
"id": image2_id,
|
||||
},
|
||||
{
|
||||
"name": "test-image-3",
|
||||
"id": image3_id,
|
||||
},
|
||||
]
|
||||
}
|
||||
client.images().list_next.return_value = None
|
||||
|
||||
def mock_get_image_iam_policy(project, resource):
|
||||
return_value = MagicMock()
|
||||
if resource == "test-image-1":
|
||||
return_value.execute.return_value = {
|
||||
"bindings": [
|
||||
{
|
||||
"role": "roles/compute.imageUser",
|
||||
"members": ["user:test@example.com"],
|
||||
}
|
||||
]
|
||||
}
|
||||
elif resource == "test-image-2":
|
||||
return_value.execute.return_value = {
|
||||
"bindings": [
|
||||
{
|
||||
"role": "roles/compute.imageUser",
|
||||
"members": ["allAuthenticatedUsers"],
|
||||
}
|
||||
]
|
||||
}
|
||||
elif resource == "test-image-3":
|
||||
return_value.execute.side_effect = Exception("Permission denied")
|
||||
return return_value
|
||||
|
||||
client.images().getIamPolicy = mock_get_image_iam_policy
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
from unittest import mock
|
||||
|
||||
from tests.providers.gcp.gcp_fixtures import GCP_PROJECT_ID, set_mocked_gcp_provider
|
||||
|
||||
|
||||
class Test_compute_image_not_publicly_shared:
|
||||
def test_compute_no_images(self):
|
||||
compute_client = mock.MagicMock()
|
||||
compute_client.images = []
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.gcp.services.compute.compute_image_not_publicly_shared.compute_image_not_publicly_shared.compute_client",
|
||||
new=compute_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.compute.compute_image_not_publicly_shared.compute_image_not_publicly_shared import (
|
||||
compute_image_not_publicly_shared,
|
||||
)
|
||||
|
||||
check = compute_image_not_publicly_shared()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_image_not_publicly_shared(self):
|
||||
compute_client = mock.MagicMock()
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.gcp.services.compute.compute_image_not_publicly_shared.compute_image_not_publicly_shared.compute_client",
|
||||
new=compute_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.compute.compute_image_not_publicly_shared.compute_image_not_publicly_shared import (
|
||||
compute_image_not_publicly_shared,
|
||||
)
|
||||
from prowler.providers.gcp.services.compute.compute_service import Image
|
||||
|
||||
image = Image(
|
||||
name="private-image",
|
||||
id="1234567890",
|
||||
project_id=GCP_PROJECT_ID,
|
||||
publicly_shared=False,
|
||||
)
|
||||
|
||||
compute_client.project_ids = [GCP_PROJECT_ID]
|
||||
compute_client.images = [image]
|
||||
|
||||
check = compute_image_not_publicly_shared()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Compute Engine disk image private-image is not publicly shared."
|
||||
)
|
||||
assert result[0].resource_id == "1234567890"
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
assert result[0].resource_name == "private-image"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_image_publicly_shared_with_all_authenticated_users(self):
|
||||
from prowler.providers.gcp.services.compute.compute_service import Image
|
||||
|
||||
image = Image(
|
||||
name="public-image",
|
||||
id="1234567890",
|
||||
project_id=GCP_PROJECT_ID,
|
||||
publicly_shared=True,
|
||||
)
|
||||
|
||||
compute_client = mock.MagicMock()
|
||||
compute_client.project_ids = [GCP_PROJECT_ID]
|
||||
compute_client.images = [image]
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.gcp.services.compute.compute_image_not_publicly_shared.compute_image_not_publicly_shared.compute_client",
|
||||
new=compute_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.compute.compute_image_not_publicly_shared.compute_image_not_publicly_shared import (
|
||||
compute_image_not_publicly_shared,
|
||||
)
|
||||
|
||||
check = compute_image_not_publicly_shared()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Compute Engine disk image public-image is publicly shared with allAuthenticatedUsers."
|
||||
)
|
||||
assert result[0].resource_id == "1234567890"
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
assert result[0].resource_name == "public-image"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_multiple_images_mixed_sharing(self):
|
||||
from prowler.providers.gcp.services.compute.compute_service import Image
|
||||
|
||||
private_image = Image(
|
||||
name="private-image",
|
||||
id="1111111111",
|
||||
project_id=GCP_PROJECT_ID,
|
||||
publicly_shared=False,
|
||||
)
|
||||
|
||||
public_image = Image(
|
||||
name="public-image",
|
||||
id="2222222222",
|
||||
project_id=GCP_PROJECT_ID,
|
||||
publicly_shared=True,
|
||||
)
|
||||
|
||||
compute_client = mock.MagicMock()
|
||||
compute_client.project_ids = [GCP_PROJECT_ID]
|
||||
compute_client.images = [private_image, public_image]
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.gcp.services.compute.compute_image_not_publicly_shared.compute_image_not_publicly_shared.compute_client",
|
||||
new=compute_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.compute.compute_image_not_publicly_shared.compute_image_not_publicly_shared import (
|
||||
compute_image_not_publicly_shared,
|
||||
)
|
||||
|
||||
check = compute_image_not_publicly_shared()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 2
|
||||
|
||||
private_result = next(
|
||||
r for r in result if r.resource_name == "private-image"
|
||||
)
|
||||
public_result = next(r for r in result if r.resource_name == "public-image")
|
||||
|
||||
assert private_result.status == "PASS"
|
||||
assert (
|
||||
private_result.status_extended
|
||||
== "Compute Engine disk image private-image is not publicly shared."
|
||||
)
|
||||
|
||||
assert public_result.status == "FAIL"
|
||||
assert (
|
||||
public_result.status_extended
|
||||
== "Compute Engine disk image public-image is publicly shared with allAuthenticatedUsers."
|
||||
)
|
||||
@@ -258,3 +258,20 @@ class TestComputeService:
|
||||
assert len(zonal_mig.auto_healing_policies) == 1
|
||||
assert zonal_mig.auto_healing_policies[0].health_check == "tcp-health-check"
|
||||
assert zonal_mig.auto_healing_policies[0].initial_delay_sec == 120
|
||||
|
||||
# Test images
|
||||
assert len(compute_client.images) == 3
|
||||
assert compute_client.images[0].name == "test-image-1"
|
||||
assert compute_client.images[0].id.__class__.__name__ == "str"
|
||||
assert compute_client.images[0].project_id == GCP_PROJECT_ID
|
||||
assert not compute_client.images[0].publicly_shared
|
||||
|
||||
assert compute_client.images[1].name == "test-image-2"
|
||||
assert compute_client.images[1].id.__class__.__name__ == "str"
|
||||
assert compute_client.images[1].project_id == GCP_PROJECT_ID
|
||||
assert compute_client.images[1].publicly_shared
|
||||
|
||||
assert compute_client.images[2].name == "test-image-3"
|
||||
assert compute_client.images[2].id.__class__.__name__ == "str"
|
||||
assert compute_client.images[2].project_id == GCP_PROJECT_ID
|
||||
assert not compute_client.images[2].publicly_shared
|
||||
|
||||
Reference in New Issue
Block a user