From 681b5983a33d2d7a08927f5a01c3a14428594255 Mon Sep 17 00:00:00 2001 From: Lydia Vilchez Date: Fri, 9 Jan 2026 13:14:05 +0100 Subject: [PATCH 1/3] feat(gcp): add check to detect persistent disks on suspended VM instances --- .../__init__.py | 0 ...ded_without_persistent_disks.metadata.json | 39 ++ ...ance_suspended_without_persistent_disks.py | 35 ++ .../gcp/services/compute/compute_service.py | 2 + ...suspended_without_persistent_disks_test.py | 538 ++++++++++++++++++ 5 files changed, 614 insertions(+) create mode 100644 prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/__init__.py create mode 100644 prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks.metadata.json create mode 100644 prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks.py create mode 100644 tests/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks_test.py diff --git a/prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/__init__.py b/prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks.metadata.json new file mode 100644 index 0000000000..8f05647989 --- /dev/null +++ b/prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks.metadata.json @@ -0,0 +1,39 @@ +{ + "Provider": "gcp", + "CheckID": "compute_instance_suspended_without_persistent_disks", + "CheckTitle": "VM instance in suspended state does not have persistent disks attached", + "CheckType": [], + "ServiceName": "compute", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low", + "ResourceType": "compute.googleapis.com/Instance", + "ResourceGroup": "compute", + "Description": "This check identifies VM instances that are in a **SUSPENDED** or **SUSPENDING** state with persistent disks still attached.\n\nSuspended VMs with attached disks represent unused infrastructure that continues to incur storage costs without providing any compute value.", + "Risk": "Persistent disks attached to suspended VM instances continue to accrue **storage charges** regardless of whether the VM is running.\n\nThis could result in:\n- **Unnecessary cloud costs** from forgotten or abandoned infrastructure\n- **Resource waste** from provisioned storage not being utilized\n- **Budget overruns** if suspended instances accumulate over time", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/icompute/docs/instances/suspend-resume-instance", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/persistent-disks-attached-to-suspended-vms.html" + ], + "Remediation": { + "Code": { + "CLI": "gcloud compute instances delete INSTANCE_NAME --zone=ZONE", + "NativeIaC": "", + "Other": "1. Open the Google Cloud Console\n2. Navigate to Compute Engine > VM instances\n3. Identify suspended instances with attached disks\n4. If the instance is no longer needed, select it and click DELETE\n5. If the instance will be resumed, take no action or resume it with: gcloud compute instances resume INSTANCE_NAME --zone=ZONE", + "Terraform": "```hcl\n# To remediate, either delete the suspended instance or resume it\n# Delete by removing the resource from your Terraform configuration\n# Or resume by changing the desired_status\nresource \"google_compute_instance\" \"example_resource\" {\n name = \"example-instance\"\n machine_type = \"e2-medium\"\n zone = \"us-central1-a\"\n\n # Set desired_status to RUNNING to resume the instance\n desired_status = \"RUNNING\"\n\n boot_disk {\n initialize_params {\n image = \"debian-cloud/debian-11\"\n }\n }\n\n network_interface {\n network = \"default\"\n }\n}\n```" + }, + "Recommendation": { + "Text": "Regularly review suspended VM instances and either **resume** them if needed or **delete** them along with their attached disks to eliminate unnecessary storage costs. Implement automated policies to detect and alert on long-suspended instances.", + "Url": "https://hub.prowler.com/check/compute_instance_suspended_without_persistent_disks" + } + }, + "Categories": [ + "resilience" + ], + "DependsOn": [], + "RelatedTo": [ + "compute_instance_disk_auto_delete_disabled" + ], + "Notes": "" +} diff --git a/prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks.py b/prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks.py new file mode 100644 index 0000000000..a9307ec803 --- /dev/null +++ b/prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks.py @@ -0,0 +1,35 @@ +from prowler.lib.check.models import Check, Check_Report_GCP +from prowler.providers.gcp.services.compute.compute_client import compute_client + + +class compute_instance_suspended_without_persistent_disks(Check): + """ + Ensure that VM instances in SUSPENDED state do not have persistent disks attached. + + This check identifies VM instances that are in a SUSPENDED or SUSPENDING state + and have persistent disks still attached. Suspended VMs with attached disks + represent unused infrastructure that continues to incur storage costs. + + - PASS: VM instance is not in SUSPENDED/SUSPENDING state, or is suspended but has no disks attached. + - FAIL: VM instance is in SUSPENDED/SUSPENDING state with persistent disks attached. + """ + + def execute(self) -> list[Check_Report_GCP]: + findings = [] + for instance in compute_client.instances: + report = Check_Report_GCP(metadata=self.metadata(), resource=instance) + report.status = "PASS" + report.status_extended = f"VM Instance {instance.name} is not suspended." + + if instance.status in ("SUSPENDED", "SUSPENDING"): + attached_disks = [disk.name for disk in instance.disks] + + if attached_disks: + report.status = "FAIL" + report.status_extended = f"VM Instance {instance.name} is {instance.status.lower()} with {len(attached_disks)} persistent disk(s) attached: {', '.join(attached_disks)}." + else: + report.status_extended = f"VM Instance {instance.name} is {instance.status.lower()} but has no persistent disks attached." + + findings.append(report) + + return findings diff --git a/prowler/providers/gcp/services/compute/compute_service.py b/prowler/providers/gcp/services/compute/compute_service.py index efe40dd765..17bc4824a0 100644 --- a/prowler/providers/gcp/services/compute/compute_service.py +++ b/prowler/providers/gcp/services/compute/compute_service.py @@ -188,6 +188,7 @@ def _get_instances(self, zone): "deletionProtection", False ), network_interfaces=network_interfaces, + status=instance.get("status", "RUNNING"), ) ) @@ -636,6 +637,7 @@ class Instance(BaseModel): provisioning_model: str = "STANDARD" deletion_protection: bool = False network_interfaces: list[NetworkInterface] = [] + status: str = "RUNNING" class Network(BaseModel): diff --git a/tests/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks_test.py b/tests/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks_test.py new file mode 100644 index 0000000000..96e9d2558a --- /dev/null +++ b/tests/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks_test.py @@ -0,0 +1,538 @@ +from unittest import mock + +from tests.providers.gcp.gcp_fixtures import ( + GCP_PROJECT_ID, + GCP_US_CENTER1_LOCATION, + set_mocked_gcp_provider, +) + + +class TestComputeInstanceSuspendedWithoutPersistentDisks: + + def test_compute_no_instances(self): + compute_client = mock.MagicMock() + compute_client.instances = [] + + 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_instance_suspended_without_persistent_disks.compute_instance_suspended_without_persistent_disks.compute_client", + new=compute_client, + ), + ): + from prowler.providers.gcp.services.compute.compute_instance_suspended_without_persistent_disks.compute_instance_suspended_without_persistent_disks import ( + compute_instance_suspended_without_persistent_disks, + ) + + check = compute_instance_suspended_without_persistent_disks() + result = check.execute() + assert len(result) == 0 + + def test_instance_running_with_disks(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_instance_suspended_without_persistent_disks.compute_instance_suspended_without_persistent_disks.compute_client", + new=compute_client, + ), + ): + from prowler.providers.gcp.services.compute.compute_instance_suspended_without_persistent_disks.compute_instance_suspended_without_persistent_disks import ( + compute_instance_suspended_without_persistent_disks, + ) + from prowler.providers.gcp.services.compute.compute_service import ( + Disk, + Instance, + ) + + compute_client.project_ids = [GCP_PROJECT_ID] + compute_client.region = GCP_US_CENTER1_LOCATION + + compute_client.instances = [ + Instance( + name="running-instance", + id="1234567890", + zone=f"{GCP_US_CENTER1_LOCATION}-a", + region=GCP_US_CENTER1_LOCATION, + public_ip=False, + metadata={}, + shielded_enabled_vtpm=True, + shielded_enabled_integrity_monitoring=True, + confidential_computing=False, + service_accounts=[ + {"email": "123-compute@developer.gserviceaccount.com"} + ], + ip_forward=False, + disks_encryption=[], + disks=[ + Disk( + name="boot-disk", + auto_delete=False, + boot=True, + encryption=False, + ), + Disk( + name="data-disk", + auto_delete=False, + boot=False, + encryption=False, + ), + ], + project_id=GCP_PROJECT_ID, + status="RUNNING", + ) + ] + + check = compute_instance_suspended_without_persistent_disks() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "VM Instance running-instance is not suspended." + ) + assert result[0].resource_id == "1234567890" + assert result[0].resource_name == "running-instance" + assert result[0].location == GCP_US_CENTER1_LOCATION + assert result[0].project_id == GCP_PROJECT_ID + + def test_instance_suspended_with_disks(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_instance_suspended_without_persistent_disks.compute_instance_suspended_without_persistent_disks.compute_client", + new=compute_client, + ), + ): + from prowler.providers.gcp.services.compute.compute_instance_suspended_without_persistent_disks.compute_instance_suspended_without_persistent_disks import ( + compute_instance_suspended_without_persistent_disks, + ) + from prowler.providers.gcp.services.compute.compute_service import ( + Disk, + Instance, + ) + + compute_client.project_ids = [GCP_PROJECT_ID] + compute_client.region = GCP_US_CENTER1_LOCATION + + compute_client.instances = [ + Instance( + name="suspended-instance", + id="1234567890", + zone=f"{GCP_US_CENTER1_LOCATION}-a", + region=GCP_US_CENTER1_LOCATION, + public_ip=False, + metadata={}, + shielded_enabled_vtpm=True, + shielded_enabled_integrity_monitoring=True, + confidential_computing=False, + service_accounts=[ + {"email": "123-compute@developer.gserviceaccount.com"} + ], + ip_forward=False, + disks_encryption=[], + disks=[ + Disk( + name="boot-disk", + auto_delete=False, + boot=True, + encryption=False, + ), + Disk( + name="data-disk", + auto_delete=False, + boot=False, + encryption=False, + ), + ], + project_id=GCP_PROJECT_ID, + status="SUSPENDED", + ) + ] + + check = compute_instance_suspended_without_persistent_disks() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "VM Instance suspended-instance is suspended with 2 persistent disk(s) attached: boot-disk, data-disk." + ) + assert result[0].resource_id == "1234567890" + assert result[0].resource_name == "suspended-instance" + assert result[0].location == GCP_US_CENTER1_LOCATION + assert result[0].project_id == GCP_PROJECT_ID + + def test_instance_suspending_with_disks(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_instance_suspended_without_persistent_disks.compute_instance_suspended_without_persistent_disks.compute_client", + new=compute_client, + ), + ): + from prowler.providers.gcp.services.compute.compute_instance_suspended_without_persistent_disks.compute_instance_suspended_without_persistent_disks import ( + compute_instance_suspended_without_persistent_disks, + ) + from prowler.providers.gcp.services.compute.compute_service import ( + Disk, + Instance, + ) + + compute_client.project_ids = [GCP_PROJECT_ID] + compute_client.region = GCP_US_CENTER1_LOCATION + + compute_client.instances = [ + Instance( + name="suspending-instance", + id="9876543210", + zone=f"{GCP_US_CENTER1_LOCATION}-b", + region=GCP_US_CENTER1_LOCATION, + public_ip=False, + metadata={}, + shielded_enabled_vtpm=True, + shielded_enabled_integrity_monitoring=True, + confidential_computing=False, + service_accounts=[], + ip_forward=False, + disks_encryption=[], + disks=[ + Disk( + name="boot-disk", + auto_delete=True, + boot=True, + encryption=False, + ), + ], + project_id=GCP_PROJECT_ID, + status="SUSPENDING", + ) + ] + + check = compute_instance_suspended_without_persistent_disks() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "VM Instance suspending-instance is suspending with 1 persistent disk(s) attached: boot-disk." + ) + assert result[0].resource_id == "9876543210" + assert result[0].resource_name == "suspending-instance" + + def test_instance_suspended_no_disks(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_instance_suspended_without_persistent_disks.compute_instance_suspended_without_persistent_disks.compute_client", + new=compute_client, + ), + ): + from prowler.providers.gcp.services.compute.compute_instance_suspended_without_persistent_disks.compute_instance_suspended_without_persistent_disks import ( + compute_instance_suspended_without_persistent_disks, + ) + from prowler.providers.gcp.services.compute.compute_service import Instance + + compute_client.project_ids = [GCP_PROJECT_ID] + compute_client.region = GCP_US_CENTER1_LOCATION + + compute_client.instances = [ + Instance( + name="suspended-no-disks", + id="1111111111", + zone=f"{GCP_US_CENTER1_LOCATION}-a", + region=GCP_US_CENTER1_LOCATION, + public_ip=False, + metadata={}, + shielded_enabled_vtpm=True, + shielded_enabled_integrity_monitoring=True, + confidential_computing=False, + service_accounts=[], + ip_forward=False, + disks_encryption=[], + disks=[], + project_id=GCP_PROJECT_ID, + status="SUSPENDED", + ) + ] + + check = compute_instance_suspended_without_persistent_disks() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "VM Instance suspended-no-disks is suspended but has no persistent disks attached." + ) + assert result[0].resource_id == "1111111111" + assert result[0].resource_name == "suspended-no-disks" + + def test_instance_terminated_with_disks(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_instance_suspended_without_persistent_disks.compute_instance_suspended_without_persistent_disks.compute_client", + new=compute_client, + ), + ): + from prowler.providers.gcp.services.compute.compute_instance_suspended_without_persistent_disks.compute_instance_suspended_without_persistent_disks import ( + compute_instance_suspended_without_persistent_disks, + ) + from prowler.providers.gcp.services.compute.compute_service import ( + Disk, + Instance, + ) + + compute_client.project_ids = [GCP_PROJECT_ID] + compute_client.region = GCP_US_CENTER1_LOCATION + + compute_client.instances = [ + Instance( + name="terminated-instance", + id="2222222222", + zone=f"{GCP_US_CENTER1_LOCATION}-a", + region=GCP_US_CENTER1_LOCATION, + public_ip=False, + metadata={}, + shielded_enabled_vtpm=True, + shielded_enabled_integrity_monitoring=True, + confidential_computing=False, + service_accounts=[], + ip_forward=False, + disks_encryption=[], + disks=[ + Disk( + name="boot-disk", + auto_delete=False, + boot=True, + encryption=False, + ), + ], + project_id=GCP_PROJECT_ID, + status="TERMINATED", + ) + ] + + check = compute_instance_suspended_without_persistent_disks() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "VM Instance terminated-instance is not suspended." + ) + assert result[0].resource_id == "2222222222" + assert result[0].resource_name == "terminated-instance" + + def test_multiple_instances_mixed_results(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_instance_suspended_without_persistent_disks.compute_instance_suspended_without_persistent_disks.compute_client", + new=compute_client, + ), + ): + from prowler.providers.gcp.services.compute.compute_instance_suspended_without_persistent_disks.compute_instance_suspended_without_persistent_disks import ( + compute_instance_suspended_without_persistent_disks, + ) + from prowler.providers.gcp.services.compute.compute_service import ( + Disk, + Instance, + ) + + compute_client.project_ids = [GCP_PROJECT_ID] + compute_client.region = GCP_US_CENTER1_LOCATION + + compute_client.instances = [ + Instance( + name="running-instance", + id="1111111111", + zone=f"{GCP_US_CENTER1_LOCATION}-a", + region=GCP_US_CENTER1_LOCATION, + public_ip=False, + metadata={}, + shielded_enabled_vtpm=True, + shielded_enabled_integrity_monitoring=True, + confidential_computing=False, + service_accounts=[], + ip_forward=False, + disks_encryption=[], + disks=[ + Disk( + name="boot-disk", + auto_delete=False, + boot=True, + encryption=False, + ), + ], + project_id=GCP_PROJECT_ID, + status="RUNNING", + ), + Instance( + name="suspended-with-disks", + id="2222222222", + zone=f"{GCP_US_CENTER1_LOCATION}-b", + region=GCP_US_CENTER1_LOCATION, + public_ip=False, + metadata={}, + shielded_enabled_vtpm=True, + shielded_enabled_integrity_monitoring=True, + confidential_computing=False, + service_accounts=[], + ip_forward=False, + disks_encryption=[], + disks=[ + Disk( + name="persistent-disk", + auto_delete=True, + boot=True, + encryption=False, + ), + ], + project_id=GCP_PROJECT_ID, + status="SUSPENDED", + ), + Instance( + name="suspended-no-disks", + id="3333333333", + zone=f"{GCP_US_CENTER1_LOCATION}-c", + region=GCP_US_CENTER1_LOCATION, + public_ip=False, + metadata={}, + shielded_enabled_vtpm=True, + shielded_enabled_integrity_monitoring=True, + confidential_computing=False, + service_accounts=[], + ip_forward=False, + disks_encryption=[], + disks=[], + project_id=GCP_PROJECT_ID, + status="SUSPENDED", + ), + ] + + check = compute_instance_suspended_without_persistent_disks() + result = check.execute() + + assert len(result) == 3 + + # First instance - RUNNING with disks (PASS) + assert result[0].status == "PASS" + assert result[0].resource_name == "running-instance" + assert "is not suspended" in result[0].status_extended + + # Second instance - SUSPENDED with disks (FAIL) + assert result[1].status == "FAIL" + assert result[1].resource_name == "suspended-with-disks" + assert ( + "is suspended with 1 persistent disk(s) attached" + in result[1].status_extended + ) + + # Third instance - SUSPENDED without disks (PASS) + assert result[2].status == "PASS" + assert result[2].resource_name == "suspended-no-disks" + assert ( + "is suspended but has no persistent disks attached" + in result[2].status_extended + ) + + def test_instance_stopping_with_disks(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_instance_suspended_without_persistent_disks.compute_instance_suspended_without_persistent_disks.compute_client", + new=compute_client, + ), + ): + from prowler.providers.gcp.services.compute.compute_instance_suspended_without_persistent_disks.compute_instance_suspended_without_persistent_disks import ( + compute_instance_suspended_without_persistent_disks, + ) + from prowler.providers.gcp.services.compute.compute_service import ( + Disk, + Instance, + ) + + compute_client.project_ids = [GCP_PROJECT_ID] + compute_client.region = GCP_US_CENTER1_LOCATION + + compute_client.instances = [ + Instance( + name="stopping-instance", + id="4444444444", + zone=f"{GCP_US_CENTER1_LOCATION}-a", + region=GCP_US_CENTER1_LOCATION, + public_ip=False, + metadata={}, + shielded_enabled_vtpm=True, + shielded_enabled_integrity_monitoring=True, + confidential_computing=False, + service_accounts=[], + ip_forward=False, + disks_encryption=[], + disks=[ + Disk( + name="boot-disk", + auto_delete=False, + boot=True, + encryption=False, + ), + ], + project_id=GCP_PROJECT_ID, + status="STOPPING", + ) + ] + + check = compute_instance_suspended_without_persistent_disks() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "VM Instance stopping-instance is not suspended." + ) + assert result[0].resource_id == "4444444444" + assert result[0].resource_name == "stopping-instance" From fde118015a3cfda0cc4d456fd747a91d1d9761ba Mon Sep 17 00:00:00 2001 From: Lydia Vilchez Date: Fri, 9 Jan 2026 13:31:54 +0100 Subject: [PATCH 2/3] docs: update CHANGELOG --- prowler/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index c42eb0728e..b46bef58e3 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `compute_instance_group_load_balancer_attached` check for GCP provider [(#9695)](https://github.com/prowler-cloud/prowler/pull/9695) - `compute_instance_single_network_interface` check for GCP provider [(#9702)](https://github.com/prowler-cloud/prowler/pull/9702) - `compute_image_not_publicly_shared` check for GCP provider [(#9718)](https://github.com/prowler-cloud/prowler/pull/9718) +- `compute_instance_suspended_without_persistent_disks` check for GCP provider [(#9747)](https://github.com/prowler-cloud/prowler/pull/9747) ### Changed - Update AWS Step Functions service metadata to new format [(#9432)](https://github.com/prowler-cloud/prowler/pull/9432) From bff3599477bbeb417b18d1697433633842d28546 Mon Sep 17 00:00:00 2001 From: Lydia Vilchez Date: Mon, 19 Jan 2026 17:49:51 +0100 Subject: [PATCH 3/3] refactor(gcp): refocus suspended VM persistent disks check on security risks --- ...pended_without_persistent_disks.metadata.json | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks.metadata.json index 8f05647989..d90b06b906 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks.metadata.json @@ -1,16 +1,16 @@ { "Provider": "gcp", "CheckID": "compute_instance_suspended_without_persistent_disks", - "CheckTitle": "VM instance in suspended state does not have persistent disks attached", + "CheckTitle": "Suspended VM instance does not have persistent disks attached", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "low", + "Severity": "medium", "ResourceType": "compute.googleapis.com/Instance", "ResourceGroup": "compute", - "Description": "This check identifies VM instances that are in a **SUSPENDED** or **SUSPENDING** state with persistent disks still attached.\n\nSuspended VMs with attached disks represent unused infrastructure that continues to incur storage costs without providing any compute value.", - "Risk": "Persistent disks attached to suspended VM instances continue to accrue **storage charges** regardless of whether the VM is running.\n\nThis could result in:\n- **Unnecessary cloud costs** from forgotten or abandoned infrastructure\n- **Resource waste** from provisioned storage not being utilized\n- **Budget overruns** if suspended instances accumulate over time", + "Description": "This check identifies VM instances in a **SUSPENDED** or **SUSPENDING** state with persistent disks still attached.\n\nPersistent disks on suspended VMs remain accessible through the GCP API and could contain **sensitive data** while the instance is inactive, potentially creating security blind spots in long-forgotten infrastructure.", + "Risk": "Persistent disks on suspended VM instances remain accessible through the GCP API and may contain **sensitive data**, creating potential security risks:\n\n- **Unauthorized data access** if credentials are compromised or permissions are misconfigured\n- **Data exposure** from forgotten infrastructure that is no longer actively monitored\n- **Security blind spots** where suspended resources are overlooked during security reviews and audits", "RelatedUrl": "", "AdditionalURLs": [ "https://cloud.google.com/icompute/docs/instances/suspend-resume-instance", @@ -24,16 +24,14 @@ "Terraform": "```hcl\n# To remediate, either delete the suspended instance or resume it\n# Delete by removing the resource from your Terraform configuration\n# Or resume by changing the desired_status\nresource \"google_compute_instance\" \"example_resource\" {\n name = \"example-instance\"\n machine_type = \"e2-medium\"\n zone = \"us-central1-a\"\n\n # Set desired_status to RUNNING to resume the instance\n desired_status = \"RUNNING\"\n\n boot_disk {\n initialize_params {\n image = \"debian-cloud/debian-11\"\n }\n }\n\n network_interface {\n network = \"default\"\n }\n}\n```" }, "Recommendation": { - "Text": "Regularly review suspended VM instances and either **resume** them if needed or **delete** them along with their attached disks to eliminate unnecessary storage costs. Implement automated policies to detect and alert on long-suspended instances.", + "Text": "Regularly review suspended VM instances to reduce your attack surface. Either **resume** instances if still needed, or **delete** them along with their attached disks to eliminate potential data exposure. Implement automated policies to detect and alert on long-suspended instances as part of your security monitoring.", "Url": "https://hub.prowler.com/check/compute_instance_suspended_without_persistent_disks" } }, - "Categories": [ - "resilience" - ], + "Categories": [], "DependsOn": [], "RelatedTo": [ "compute_instance_disk_auto_delete_disabled" ], - "Notes": "" + "Notes": "This check is focused on security risks rather than cost optimization. Persistent disks on suspended VMs remain accessible and may contain sensitive data, creating potential unauthorized access risks." }