Skip to content

Commit 3dadb26

Browse files
authored
feat(gcp): add check for VM instance deletion protection (#9358)
1 parent 495aee0 commit 3dadb26

File tree

6 files changed

+215
-0
lines changed

6 files changed

+215
-0
lines changed

prowler/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
99
- `repository_immutable_releases_enabled` check for GitHub provider [(#9162)](https://github.com/prowler-cloud/prowler/pull/9162)
1010
- `compute_instance_preemptible_vm_disabled` check for GCP provider [(#9342)](https://github.com/prowler-cloud/prowler/pull/9342)
1111
- `compute_instance_automatic_restart_enabled` check for GCP provider [(#9271)](https://github.com/prowler-cloud/prowler/pull/9271)
12+
- `compute_instance_deletion_protection_enabled` check for GCP provider [(#9358)](https://github.com/prowler-cloud/prowler/pull/9358)
1213

1314
---
1415

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

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"Provider": "gcp",
3+
"CheckID": "compute_instance_deletion_protection_enabled",
4+
"CheckTitle": "VM instance has deletion protection enabled",
5+
"CheckType": [],
6+
"ServiceName": "compute",
7+
"SubServiceName": "",
8+
"ResourceIdTemplate": "",
9+
"Severity": "medium",
10+
"ResourceType": "compute.googleapis.com/Instance",
11+
"Description": "This check verifies whether GCP Compute Engine VM instances have **deletion protection** enabled to prevent accidental termination of production or critical workloads.",
12+
"Risk": "Without deletion protection enabled, VM instances are vulnerable to **accidental deletion** by users with sufficient permissions.\n\nThis could result in:\n- **Service disruption** and downtime for critical applications\n- **Data loss** if persistent disks are also deleted\n- **Recovery delays** while recreating instances and restoring configurations",
13+
"RelatedUrl": "",
14+
"AdditionalURLs": [
15+
"https://cloud.google.com/compute/docs/instances/preventing-accidental-vm-deletion",
16+
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/enable-deletion-protection.html"
17+
],
18+
"Remediation": {
19+
"Code": {
20+
"CLI": "gcloud compute instances update INSTANCE_NAME --deletion-protection --zone=ZONE",
21+
"NativeIaC": "",
22+
"Other": "1. Open the Google Cloud Console\n2. Navigate to Compute Engine > VM instances\n3. Select the target VM instance\n4. Click Edit\n5. Under Deletion protection, check the box to enable\n6. Click Save",
23+
"Terraform": "```hcl\nresource \"google_compute_instance\" \"example_resource\" {\n name = \"example-instance\"\n machine_type = \"e2-medium\"\n zone = \"us-central1-a\"\n\n # Enable deletion protection\n deletion_protection = true\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```"
24+
},
25+
"Recommendation": {
26+
"Text": "Enable deletion protection on all production and business-critical VM instances to prevent accidental termination. Regularly review instances to ensure critical workloads are protected.",
27+
"Url": "https://hub.prowler.com/check/compute_instance_deletion_protection_enabled"
28+
}
29+
},
30+
"Categories": [
31+
"resilience"
32+
],
33+
"DependsOn": [],
34+
"RelatedTo": [],
35+
"Notes": ""
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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_deletion_protection_enabled(Check):
6+
"""
7+
Ensure that VM instance has deletion protection enabled.
8+
9+
This check verifies whether GCP Compute Engine VM instances have deletion protection
10+
enabled to prevent accidental termination of production or critical workloads.
11+
12+
- PASS: VM instance has deletion protection enabled.
13+
- FAIL: VM instance does not have deletion protection enabled.
14+
"""
15+
16+
def execute(self) -> list[Check_Report_GCP]:
17+
findings = []
18+
for instance in compute_client.instances:
19+
report = Check_Report_GCP(metadata=self.metadata(), resource=instance)
20+
report.status = "PASS"
21+
report.status_extended = (
22+
f"VM Instance {instance.name} has deletion protection enabled."
23+
)
24+
if not instance.deletion_protection:
25+
report.status = "FAIL"
26+
report.status_extended = f"VM Instance {instance.name} does not have deletion protection enabled."
27+
findings.append(report)
28+
29+
return findings

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ def _get_instances(self, zone):
143143
preemptible=instance.get("scheduling", {}).get(
144144
"preemptible", False
145145
),
146+
deletion_protection=instance.get(
147+
"deletionProtection", False
148+
),
146149
)
147150
)
148151

@@ -377,6 +380,7 @@ class Instance(BaseModel):
377380
automatic_restart: bool = False
378381
preemptible: bool = False
379382
provisioning_model: str = "STANDARD"
383+
deletion_protection: bool = False
380384

381385

382386
class Network(BaseModel):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
from unittest import mock
2+
3+
from tests.providers.gcp.gcp_fixtures import (
4+
GCP_PROJECT_ID,
5+
GCP_US_CENTER1_LOCATION,
6+
set_mocked_gcp_provider,
7+
)
8+
9+
10+
class TestComputeInstanceDeletionProtectionEnabled:
11+
def test_compute_no_instances(self):
12+
compute_client = mock.MagicMock()
13+
compute_client.instances = []
14+
15+
with (
16+
mock.patch(
17+
"prowler.providers.common.provider.Provider.get_global_provider",
18+
return_value=set_mocked_gcp_provider(),
19+
),
20+
mock.patch(
21+
"prowler.providers.gcp.services.compute.compute_instance_deletion_protection_enabled.compute_instance_deletion_protection_enabled.compute_client",
22+
new=compute_client,
23+
),
24+
):
25+
from prowler.providers.gcp.services.compute.compute_instance_deletion_protection_enabled.compute_instance_deletion_protection_enabled import (
26+
compute_instance_deletion_protection_enabled,
27+
)
28+
29+
check = compute_instance_deletion_protection_enabled()
30+
result = check.execute()
31+
assert len(result) == 0
32+
33+
def test_instance_deletion_protection_enabled(self):
34+
compute_client = mock.MagicMock()
35+
36+
with (
37+
mock.patch(
38+
"prowler.providers.common.provider.Provider.get_global_provider",
39+
return_value=set_mocked_gcp_provider(),
40+
),
41+
mock.patch(
42+
"prowler.providers.gcp.services.compute.compute_instance_deletion_protection_enabled.compute_instance_deletion_protection_enabled.compute_client",
43+
new=compute_client,
44+
),
45+
):
46+
from prowler.providers.gcp.services.compute.compute_instance_deletion_protection_enabled.compute_instance_deletion_protection_enabled import (
47+
compute_instance_deletion_protection_enabled,
48+
)
49+
from prowler.providers.gcp.services.compute.compute_service import Instance
50+
51+
compute_client.project_ids = [GCP_PROJECT_ID]
52+
compute_client.region = GCP_US_CENTER1_LOCATION
53+
54+
compute_client.instances = [
55+
Instance(
56+
name="test-instance",
57+
id="1234567890",
58+
zone=f"{GCP_US_CENTER1_LOCATION}-a",
59+
region=GCP_US_CENTER1_LOCATION,
60+
public_ip=False,
61+
metadata={},
62+
shielded_enabled_vtpm=True,
63+
shielded_enabled_integrity_monitoring=True,
64+
confidential_computing=False,
65+
service_accounts=[
66+
{"email": "123-compute@developer.gserviceaccount.com"}
67+
],
68+
ip_forward=False,
69+
disks_encryption=[],
70+
project_id=GCP_PROJECT_ID,
71+
deletion_protection=True,
72+
)
73+
]
74+
75+
check = compute_instance_deletion_protection_enabled()
76+
result = check.execute()
77+
78+
assert len(result) == 1
79+
assert result[0].status == "PASS"
80+
assert (
81+
result[0].status_extended
82+
== f"VM Instance {compute_client.instances[0].name} has deletion protection enabled."
83+
)
84+
assert result[0].resource_id == "1234567890"
85+
assert result[0].resource_name == "test-instance"
86+
assert result[0].location == GCP_US_CENTER1_LOCATION
87+
assert result[0].project_id == GCP_PROJECT_ID
88+
89+
def test_instance_deletion_protection_disabled(self):
90+
compute_client = mock.MagicMock()
91+
92+
with (
93+
mock.patch(
94+
"prowler.providers.common.provider.Provider.get_global_provider",
95+
return_value=set_mocked_gcp_provider(),
96+
),
97+
mock.patch(
98+
"prowler.providers.gcp.services.compute.compute_instance_deletion_protection_enabled.compute_instance_deletion_protection_enabled.compute_client",
99+
new=compute_client,
100+
),
101+
):
102+
from prowler.providers.gcp.services.compute.compute_instance_deletion_protection_enabled.compute_instance_deletion_protection_enabled import (
103+
compute_instance_deletion_protection_enabled,
104+
)
105+
from prowler.providers.gcp.services.compute.compute_service import Instance
106+
107+
compute_client.project_ids = [GCP_PROJECT_ID]
108+
compute_client.region = GCP_US_CENTER1_LOCATION
109+
110+
compute_client.instances = [
111+
Instance(
112+
name="test-instance",
113+
id="1234567890",
114+
zone=f"{GCP_US_CENTER1_LOCATION}-a",
115+
region=GCP_US_CENTER1_LOCATION,
116+
public_ip=False,
117+
metadata={},
118+
shielded_enabled_vtpm=True,
119+
shielded_enabled_integrity_monitoring=True,
120+
confidential_computing=False,
121+
service_accounts=[
122+
{
123+
"email": f"{GCP_PROJECT_ID}-compute@developer.gserviceaccount.com"
124+
}
125+
],
126+
ip_forward=False,
127+
disks_encryption=[],
128+
project_id=GCP_PROJECT_ID,
129+
deletion_protection=False,
130+
)
131+
]
132+
133+
check = compute_instance_deletion_protection_enabled()
134+
result = check.execute()
135+
136+
assert len(result) == 1
137+
assert result[0].status == "FAIL"
138+
assert (
139+
result[0].status_extended
140+
== f"VM Instance {compute_client.instances[0].name} does not have deletion protection enabled."
141+
)
142+
assert result[0].resource_id == "1234567890"
143+
assert result[0].resource_name == "test-instance"
144+
assert result[0].location == GCP_US_CENTER1_LOCATION
145+
assert result[0].project_id == GCP_PROJECT_ID

0 commit comments

Comments
 (0)