mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-01-25 02:08:11 +00:00
feat(gcp): add compute check to ensure VM disks have auto-delete disabled (#9604)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
@@ -7,6 +7,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
### Added
|
||||
- Add Prowler ThreatScore for the Alibaba Cloud provider [(#9511)](https://github.com/prowler-cloud/prowler/pull/9511)
|
||||
- `compute_instance_group_multiple_zones` check for GCP provider [(#9566)](https://github.com/prowler-cloud/prowler/pull/9566)
|
||||
- `compute_instance_disk_auto_delete_disabled` check for GCP provider [(#9604)](https://github.com/prowler-cloud/prowler/pull/9604)
|
||||
- Bedrock service pagination [(#9606)](https://github.com/prowler-cloud/prowler/pull/9606)
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"Provider": "gcp",
|
||||
"CheckID": "compute_instance_disk_auto_delete_disabled",
|
||||
"CheckTitle": "VM instance attached disks have auto-delete disabled",
|
||||
"CheckType": [],
|
||||
"ServiceName": "compute",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "compute.googleapis.com/Instance",
|
||||
"Description": "This check verifies whether GCP Compute Engine VM instances have **auto-delete** disabled for their attached persistent disks.\n\nWhen auto-delete is enabled, persistent disks are automatically removed when the associated VM instance is deleted, which can lead to unintended data loss.",
|
||||
"Risk": "With auto-delete enabled, persistent disks are automatically deleted when the associated VM instance is terminated.\n\nThis could result in:\n- **Permanent data loss** if the instance is accidentally or intentionally deleted\n- **Recovery challenges** for mission-critical workloads\n- **Compliance violations** where data retention is required",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://cloud.google.com/compute/docs/disks/add-persistent-disk",
|
||||
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/disable-auto-delete.html"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "gcloud compute instances set-disk-auto-delete INSTANCE_NAME --zone=ZONE --no-auto-delete --disk=DISK_NAME",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. Open the Google Cloud Console\n2. Navigate to Compute Engine > VM instances\n3. Click the target VM instance name\n4. Click Edit\n5. In the Boot disk section, select 'Keep disk' from the 'When deleting instance' dropdown\n6. For Additional disks, click each disk and select 'Keep disk' under 'Deletion rule'\n7. Click Save",
|
||||
"Terraform": "```hcl\nresource \"google_compute_instance\" \"example_resource\" {\n name = \"example-instance\"\n machine_type = \"e2-medium\"\n zone = \"us-central1-a\"\n\n boot_disk {\n # Disable auto-delete for the boot disk\n auto_delete = false\n\n initialize_params {\n image = \"debian-cloud/debian-11\"\n }\n }\n\n attached_disk {\n source = google_compute_disk.example_disk.id\n # Disable auto-delete for attached disks\n auto_delete = false\n }\n\n network_interface {\n network = \"default\"\n }\n}\n```"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Disable `auto-delete` for all persistent disks attached to **production** and **business-critical** VM instances to prevent **accidental data loss**. Regularly review disk configurations to ensure data retention requirements are met.",
|
||||
"Url": "https://hub.prowler.com/check/compute_instance_disk_auto_delete_disabled"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"resilience"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_GCP
|
||||
from prowler.providers.gcp.services.compute.compute_client import compute_client
|
||||
|
||||
|
||||
class compute_instance_disk_auto_delete_disabled(Check):
|
||||
"""
|
||||
Ensure that VM instance attached disks have auto-delete disabled.
|
||||
|
||||
This check verifies whether GCP Compute Engine VM instances have auto-delete
|
||||
disabled for their attached persistent disks to prevent accidental data loss
|
||||
when the instance is terminated.
|
||||
|
||||
- PASS: All attached disks have auto-delete disabled.
|
||||
- FAIL: One or more attached disks have auto-delete enabled.
|
||||
"""
|
||||
|
||||
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} has auto-delete disabled for all attached disks."
|
||||
|
||||
auto_delete_disks = [
|
||||
disk.name for disk in instance.disks if disk.auto_delete
|
||||
]
|
||||
|
||||
if auto_delete_disks:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"VM Instance {instance.name} has auto-delete enabled for the following disks: {', '.join(auto_delete_disks)}."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -138,6 +138,19 @@ class Compute(GCPService):
|
||||
)
|
||||
for disk in instance.get("disks", [])
|
||||
],
|
||||
disks=[
|
||||
Disk(
|
||||
name=disk["deviceName"],
|
||||
auto_delete=disk.get("autoDelete", False),
|
||||
boot=disk.get("boot", False),
|
||||
encryption=bool(
|
||||
disk.get("diskEncryptionKey", {}).get(
|
||||
"sha256"
|
||||
)
|
||||
),
|
||||
)
|
||||
for disk in instance.get("disks", [])
|
||||
],
|
||||
automatic_restart=instance.get("scheduling", {}).get(
|
||||
"automaticRestart", False
|
||||
),
|
||||
@@ -449,6 +462,13 @@ class Compute(GCPService):
|
||||
)
|
||||
|
||||
|
||||
class Disk(BaseModel):
|
||||
name: str
|
||||
auto_delete: bool = False
|
||||
boot: bool
|
||||
encryption: bool = False
|
||||
|
||||
|
||||
class Instance(BaseModel):
|
||||
name: str
|
||||
id: str
|
||||
@@ -463,6 +483,7 @@ class Instance(BaseModel):
|
||||
service_accounts: list
|
||||
ip_forward: bool
|
||||
disks_encryption: list
|
||||
disks: list[Disk] = []
|
||||
automatic_restart: bool = False
|
||||
preemptible: bool = False
|
||||
provisioning_model: str = "STANDARD"
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
from unittest import mock
|
||||
|
||||
from tests.providers.gcp.gcp_fixtures import (
|
||||
GCP_PROJECT_ID,
|
||||
GCP_US_CENTER1_LOCATION,
|
||||
set_mocked_gcp_provider,
|
||||
)
|
||||
|
||||
|
||||
class TestComputeInstanceDiskAutoDeleteDisabled:
|
||||
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_disk_auto_delete_disabled.compute_instance_disk_auto_delete_disabled.compute_client",
|
||||
new=compute_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.compute.compute_instance_disk_auto_delete_disabled.compute_instance_disk_auto_delete_disabled import (
|
||||
compute_instance_disk_auto_delete_disabled,
|
||||
)
|
||||
|
||||
check = compute_instance_disk_auto_delete_disabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_instance_disk_auto_delete_disabled(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_disk_auto_delete_disabled.compute_instance_disk_auto_delete_disabled.compute_client",
|
||||
new=compute_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.compute.compute_instance_disk_auto_delete_disabled.compute_instance_disk_auto_delete_disabled import (
|
||||
compute_instance_disk_auto_delete_disabled,
|
||||
)
|
||||
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="test-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,
|
||||
)
|
||||
]
|
||||
|
||||
check = compute_instance_disk_auto_delete_disabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "VM Instance test-instance has auto-delete disabled for all attached disks."
|
||||
)
|
||||
assert result[0].resource_id == "1234567890"
|
||||
assert result[0].resource_name == "test-instance"
|
||||
assert result[0].location == GCP_US_CENTER1_LOCATION
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
|
||||
def test_instance_disk_auto_delete_enabled_single_disk(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_disk_auto_delete_disabled.compute_instance_disk_auto_delete_disabled.compute_client",
|
||||
new=compute_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.compute.compute_instance_disk_auto_delete_disabled.compute_instance_disk_auto_delete_disabled import (
|
||||
compute_instance_disk_auto_delete_disabled,
|
||||
)
|
||||
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="test-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=True,
|
||||
boot=True,
|
||||
encryption=False,
|
||||
),
|
||||
Disk(
|
||||
name="data-disk",
|
||||
auto_delete=False,
|
||||
boot=False,
|
||||
encryption=False,
|
||||
),
|
||||
],
|
||||
project_id=GCP_PROJECT_ID,
|
||||
)
|
||||
]
|
||||
|
||||
check = compute_instance_disk_auto_delete_disabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "VM Instance test-instance has auto-delete enabled for the following disks: boot-disk."
|
||||
)
|
||||
assert result[0].resource_id == "1234567890"
|
||||
assert result[0].resource_name == "test-instance"
|
||||
assert result[0].location == GCP_US_CENTER1_LOCATION
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
|
||||
def test_instance_disk_auto_delete_enabled_multiple_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_disk_auto_delete_disabled.compute_instance_disk_auto_delete_disabled.compute_client",
|
||||
new=compute_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.compute.compute_instance_disk_auto_delete_disabled.compute_instance_disk_auto_delete_disabled import (
|
||||
compute_instance_disk_auto_delete_disabled,
|
||||
)
|
||||
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="test-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=True,
|
||||
boot=True,
|
||||
encryption=False,
|
||||
),
|
||||
Disk(
|
||||
name="data-disk",
|
||||
auto_delete=True,
|
||||
boot=False,
|
||||
encryption=False,
|
||||
),
|
||||
],
|
||||
project_id=GCP_PROJECT_ID,
|
||||
)
|
||||
]
|
||||
|
||||
check = compute_instance_disk_auto_delete_disabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "VM Instance test-instance has auto-delete enabled for the following disks: boot-disk, data-disk."
|
||||
)
|
||||
assert result[0].resource_id == "1234567890"
|
||||
assert result[0].resource_name == "test-instance"
|
||||
assert result[0].location == GCP_US_CENTER1_LOCATION
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
|
||||
def test_instance_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_disk_auto_delete_disabled.compute_instance_disk_auto_delete_disabled.compute_client",
|
||||
new=compute_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.compute.compute_instance_disk_auto_delete_disabled.compute_instance_disk_auto_delete_disabled import (
|
||||
compute_instance_disk_auto_delete_disabled,
|
||||
)
|
||||
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="test-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=[],
|
||||
project_id=GCP_PROJECT_ID,
|
||||
)
|
||||
]
|
||||
|
||||
check = compute_instance_disk_auto_delete_disabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "VM Instance test-instance has auto-delete disabled for all attached disks."
|
||||
)
|
||||
assert result[0].resource_id == "1234567890"
|
||||
assert result[0].resource_name == "test-instance"
|
||||
assert result[0].location == GCP_US_CENTER1_LOCATION
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
|
||||
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_disk_auto_delete_disabled.compute_instance_disk_auto_delete_disabled.compute_client",
|
||||
new=compute_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.compute.compute_instance_disk_auto_delete_disabled.compute_instance_disk_auto_delete_disabled import (
|
||||
compute_instance_disk_auto_delete_disabled,
|
||||
)
|
||||
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="compliant-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,
|
||||
),
|
||||
Instance(
|
||||
name="non-compliant-instance",
|
||||
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="auto-delete-disk",
|
||||
auto_delete=True,
|
||||
boot=True,
|
||||
encryption=False,
|
||||
),
|
||||
],
|
||||
project_id=GCP_PROJECT_ID,
|
||||
),
|
||||
]
|
||||
|
||||
check = compute_instance_disk_auto_delete_disabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 2
|
||||
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].resource_name == "compliant-instance"
|
||||
|
||||
assert result[1].status == "FAIL"
|
||||
assert result[1].resource_name == "non-compliant-instance"
|
||||
assert "auto-delete-disk" in result[1].status_extended
|
||||
Reference in New Issue
Block a user