Skip to content

Commit 963ece9

Browse files
feat(gcp): add check to detect persistent disks on suspended VM instances (#9747)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
1 parent a32eff6 commit 963ece9

File tree

6 files changed

+616
-0
lines changed

6 files changed

+616
-0
lines changed

prowler/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to the **Prowler SDK** are documented in this file.
44

55
## [5.18.0] (Prowler UNRELEASED)
66

7+
### Added
8+
9+
- `compute_instance_suspended_without_persistent_disks` check for GCP provider [(#9747)](https://github.com/prowler-cloud/prowler/pull/9747)
10+
711
### Changed
812

913
- Update Azure App Service service metadata to new format [(#9613)](https://github.com/prowler-cloud/prowler/pull/9613)

prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"Provider": "gcp",
3+
"CheckID": "compute_instance_suspended_without_persistent_disks",
4+
"CheckTitle": "Suspended VM instance does not have persistent disks attached",
5+
"CheckType": [],
6+
"ServiceName": "compute",
7+
"SubServiceName": "",
8+
"ResourceIdTemplate": "",
9+
"Severity": "medium",
10+
"ResourceType": "compute.googleapis.com/Instance",
11+
"ResourceGroup": "compute",
12+
"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.",
13+
"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",
14+
"RelatedUrl": "",
15+
"AdditionalURLs": [
16+
"https://cloud.google.com/icompute/docs/instances/suspend-resume-instance",
17+
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/persistent-disks-attached-to-suspended-vms.html"
18+
],
19+
"Remediation": {
20+
"Code": {
21+
"CLI": "gcloud compute instances delete INSTANCE_NAME --zone=ZONE",
22+
"NativeIaC": "",
23+
"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",
24+
"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```"
25+
},
26+
"Recommendation": {
27+
"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.",
28+
"Url": "https://hub.prowler.com/check/compute_instance_suspended_without_persistent_disks"
29+
}
30+
},
31+
"Categories": [],
32+
"DependsOn": [],
33+
"RelatedTo": [
34+
"compute_instance_disk_auto_delete_disabled"
35+
],
36+
"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."
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from prowler.lib.check.models import Check, Check_Report_GCP
2+
from prowler.providers.gcp.services.compute.compute_client import compute_client
3+
4+
5+
class compute_instance_suspended_without_persistent_disks(Check):
6+
"""
7+
Ensure that VM instances in SUSPENDED state do not have persistent disks attached.
8+
9+
This check identifies VM instances that are in a SUSPENDED or SUSPENDING state
10+
and have persistent disks still attached. Suspended VMs with attached disks
11+
represent unused infrastructure that continues to incur storage costs.
12+
13+
- PASS: VM instance is not in SUSPENDED/SUSPENDING state, or is suspended but has no disks attached.
14+
- FAIL: VM instance is in SUSPENDED/SUSPENDING state with persistent disks attached.
15+
"""
16+
17+
def execute(self) -> list[Check_Report_GCP]:
18+
findings = []
19+
for instance in compute_client.instances:
20+
report = Check_Report_GCP(metadata=self.metadata(), resource=instance)
21+
report.status = "PASS"
22+
report.status_extended = f"VM Instance {instance.name} is not suspended."
23+
24+
if instance.status in ("SUSPENDED", "SUSPENDING"):
25+
attached_disks = [disk.name for disk in instance.disks]
26+
27+
if attached_disks:
28+
report.status = "FAIL"
29+
report.status_extended = f"VM Instance {instance.name} is {instance.status.lower()} with {len(attached_disks)} persistent disk(s) attached: {', '.join(attached_disks)}."
30+
else:
31+
report.status_extended = f"VM Instance {instance.name} is {instance.status.lower()} but has no persistent disks attached."
32+
33+
findings.append(report)
34+
35+
return findings

prowler/providers/gcp/services/compute/compute_service.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ def _get_instances(self, zone):
198198
"deletionProtection", False
199199
),
200200
network_interfaces=network_interfaces,
201+
status=instance.get("status", "RUNNING"),
201202
on_host_maintenance=instance.get("scheduling", {}).get(
202203
"onHostMaintenance", "MIGRATE"
203204
),
@@ -700,6 +701,7 @@ class Instance(BaseModel):
700701
provisioning_model: str = "STANDARD"
701702
deletion_protection: bool = False
702703
network_interfaces: list[NetworkInterface] = []
704+
status: str = "RUNNING"
703705
on_host_maintenance: str = "MIGRATE"
704706

705707

0 commit comments

Comments
 (0)