feat(gcp-compute): add check to ensure VMs are not preemptible or spot (#9342)

Co-authored-by: HugoPBrito <hugopbrit@gmail.com>
This commit is contained in:
lydiavilchez
2025-11-28 12:49:19 +01:00
committed by GitHub
parent dc852b4595
commit b2abdbeb60
6 changed files with 467 additions and 3 deletions
+1
View File
@@ -7,6 +7,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### Added
- `cloudstorage_uses_vpc_service_controls` check for GCP provider [(#9256)](https://github.com/prowler-cloud/prowler/pull/9256)
- `repository_immutable_releases_enabled` check for GitHub provider [(#9162)](https://github.com/prowler-cloud/prowler/pull/9162)
- `compute_instance_preemptible_vm_disabled` check for GCP provider [(#9342)](https://github.com/prowler-cloud/prowler/pull/9342)
- `compute_instance_automatic_restart_enabled` check for GCP provider [(#9271)](https://github.com/prowler-cloud/prowler/pull/9271)
---
@@ -0,0 +1,37 @@
{
"Provider": "gcp",
"CheckID": "compute_instance_preemptible_vm_disabled",
"CheckTitle": "VM instance is not configured as preemptible or Spot VM",
"CheckType": [],
"ServiceName": "compute",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "compute.googleapis.com/Instance",
"Description": "This check verifies that VM instances are not configured as **preemptible** or **Spot VMs**.\n\nBoth preemptible and Spot VMs can be terminated by Google at any time when resources are needed elsewhere, making them unsuitable for production and business-critical workloads. Spot VMs are the newer version of preemptible VMs and are Google's recommended approach for interruptible workloads.",
"Risk": "Preemptible and Spot VMs may be **terminated at any time** by Google Cloud, causing:\n\n- **Service disruptions** for production workloads\n- **Data loss** if workloads are not fault-tolerant\n- **Availability issues** for business-critical applications\n\nThey are designed for batch jobs and fault-tolerant workloads only.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://cloud.google.com/compute/docs/instances/preemptible",
"https://cloud.google.com/compute/docs/instances/spot",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/disable-preemptibility.html"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Go to Compute Engine console\n2. Select the preemptible or Spot VM instance\n3. Create a machine image from the instance\n4. Create a new instance from the machine image\n5. During creation, set **VM provisioning model** to **Standard** (not Spot)\n6. Delete the original preemptible or Spot VM instance",
"Terraform": "```hcl\nresource \"google_compute_instance\" \"example_resource\" {\n name = \"example-instance\"\n machine_type = \"e2-medium\"\n zone = \"us-central1-a\"\n\n scheduling {\n # Use standard provisioning model for production workloads (not Spot)\n provisioning_model = \"STANDARD\"\n # Also ensure preemptible is false (legacy field)\n preemptible = false\n }\n}\n```"
},
"Recommendation": {
"Text": "Use standard provisioning model for production and business-critical VM instances. Preemptible and Spot VMs should only be used for fault-tolerant, batch processing, or non-critical workloads that can handle interruptions.",
"Url": "https://hub.prowler.com/checks/compute_instance_preemptible_vm_disabled"
}
},
"Categories": [
"resilience"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,32 @@
from prowler.lib.check.models import Check, Check_Report_GCP
from prowler.providers.gcp.services.compute.compute_client import compute_client
class compute_instance_preemptible_vm_disabled(Check):
"""
Ensure GCP Compute Engine VM instances are not preemptible or Spot VMs.
- PASS: VM instance is not preemptible (preemptible=False) and not Spot
(provisioningModel != "SPOT").
- FAIL: VM instance is preemptible (preemptible=True) or Spot
(provisioningModel="SPOT").
"""
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 preemptible or Spot VM."
)
if instance.preemptible or instance.provisioning_model == "SPOT":
report.status = "FAIL"
vm_type = "preemptible" if instance.preemptible else "Spot VM"
report.status_extended = (
f"VM Instance {instance.name} is configured as {vm_type}."
)
findings.append(report)
return findings
@@ -136,13 +136,13 @@ class Compute(GCPService):
automatic_restart=instance.get("scheduling", {}).get(
"automaticRestart", False
),
preemptible=instance.get("scheduling", {}).get(
"preemptible", False
),
provisioning_model=instance.get("scheduling", {}).get(
"provisioningModel", "STANDARD"
),
project_id=project_id,
preemptible=instance.get("scheduling", {}).get(
"preemptible", False
),
)
)
@@ -0,0 +1,394 @@
from unittest import mock
from tests.providers.gcp.gcp_fixtures import GCP_PROJECT_ID, set_mocked_gcp_provider
class TestComputeInstancePreemptibleVmDisabled:
def test_no_instances(self):
compute_client = mock.MagicMock()
compute_client.project_ids = [GCP_PROJECT_ID]
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_preemptible_vm_disabled.compute_instance_preemptible_vm_disabled.compute_client",
new=compute_client,
),
):
from prowler.providers.gcp.services.compute.compute_instance_preemptible_vm_disabled.compute_instance_preemptible_vm_disabled import (
compute_instance_preemptible_vm_disabled,
)
check = compute_instance_preemptible_vm_disabled()
result = check.execute()
assert len(result) == 0
def test_instance_not_preemptible(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_preemptible_vm_disabled.compute_instance_preemptible_vm_disabled.compute_client",
new=compute_client,
),
):
from prowler.providers.gcp.services.compute.compute_instance_preemptible_vm_disabled.compute_instance_preemptible_vm_disabled import (
compute_instance_preemptible_vm_disabled,
)
from prowler.providers.gcp.services.compute.compute_service import Instance
compute_client.project_ids = [GCP_PROJECT_ID]
compute_client.instances = [
Instance(
name="test",
id="1234567890",
zone="us-central1-a",
region="us-central1",
public_ip=True,
metadata={},
shielded_enabled_vtpm=True,
shielded_enabled_integrity_monitoring=True,
confidential_computing=True,
service_accounts=[],
ip_forward=False,
disks_encryption=[("disk1", False), ("disk2", False)],
project_id=GCP_PROJECT_ID,
preemptible=False,
provisioning_model="",
)
]
check = compute_instance_preemptible_vm_disabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"VM Instance {compute_client.instances[0].name} is not preemptible or Spot VM."
)
assert result[0].resource_id == "1234567890"
assert result[0].resource_name == "test"
assert result[0].location == "us-central1"
assert result[0].project_id == GCP_PROJECT_ID
def test_instance_preemptible(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_preemptible_vm_disabled.compute_instance_preemptible_vm_disabled.compute_client",
new=compute_client,
),
):
from prowler.providers.gcp.services.compute.compute_instance_preemptible_vm_disabled.compute_instance_preemptible_vm_disabled import (
compute_instance_preemptible_vm_disabled,
)
from prowler.providers.gcp.services.compute.compute_service import Instance
compute_client.project_ids = [GCP_PROJECT_ID]
compute_client.instances = [
Instance(
name="test",
id="1234567890",
zone="us-central1-a",
region="us-central1",
public_ip=True,
metadata={},
shielded_enabled_vtpm=False,
shielded_enabled_integrity_monitoring=True,
confidential_computing=False,
service_accounts=[],
ip_forward=False,
disks_encryption=[("disk1", False), ("disk2", False)],
project_id=GCP_PROJECT_ID,
preemptible=True,
provisioning_model="",
)
]
check = compute_instance_preemptible_vm_disabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"VM Instance {compute_client.instances[0].name} is configured as preemptible."
)
assert result[0].resource_id == "1234567890"
assert result[0].resource_name == "test"
assert result[0].location == "us-central1"
assert result[0].project_id == GCP_PROJECT_ID
def test_multiple_instances_mixed_preemptible_and_standard(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_preemptible_vm_disabled.compute_instance_preemptible_vm_disabled.compute_client",
new=compute_client,
),
):
from prowler.providers.gcp.services.compute.compute_instance_preemptible_vm_disabled.compute_instance_preemptible_vm_disabled import (
compute_instance_preemptible_vm_disabled,
)
from prowler.providers.gcp.services.compute.compute_service import Instance
compute_client.project_ids = [GCP_PROJECT_ID]
compute_client.instances = [
Instance(
name="preemptible-instance",
id="111111111",
zone="us-central1-a",
region="us-central1",
public_ip=False,
metadata={},
shielded_enabled_vtpm=True,
shielded_enabled_integrity_monitoring=True,
confidential_computing=False,
service_accounts=[],
ip_forward=False,
disks_encryption=[],
project_id=GCP_PROJECT_ID,
preemptible=True,
provisioning_model="",
),
Instance(
name="standard-instance",
id="222222222",
zone="europe-west1-b",
region="europe-west1",
public_ip=True,
metadata={},
shielded_enabled_vtpm=True,
shielded_enabled_integrity_monitoring=True,
confidential_computing=True,
service_accounts=[],
ip_forward=False,
disks_encryption=[],
project_id=GCP_PROJECT_ID,
preemptible=False,
provisioning_model="",
),
]
check = compute_instance_preemptible_vm_disabled()
result = check.execute()
assert len(result) == 2
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "VM Instance preemptible-instance is configured as preemptible."
)
assert result[0].resource_id == "111111111"
assert result[0].resource_name == "preemptible-instance"
assert result[1].status == "PASS"
assert (
result[1].status_extended
== "VM Instance standard-instance is not preemptible or Spot VM."
)
assert result[1].resource_id == "222222222"
assert result[1].resource_name == "standard-instance"
def test_instance_spot_vm(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_preemptible_vm_disabled.compute_instance_preemptible_vm_disabled.compute_client",
new=compute_client,
),
):
from prowler.providers.gcp.services.compute.compute_instance_preemptible_vm_disabled.compute_instance_preemptible_vm_disabled import (
compute_instance_preemptible_vm_disabled,
)
from prowler.providers.gcp.services.compute.compute_service import Instance
compute_client.project_ids = [GCP_PROJECT_ID]
compute_client.instances = [
Instance(
name="spot-vm",
id="3333333333",
zone="us-west1-a",
region="us-west1",
public_ip=False,
metadata={},
shielded_enabled_vtpm=True,
shielded_enabled_integrity_monitoring=True,
confidential_computing=False,
service_accounts=[],
ip_forward=False,
disks_encryption=[],
project_id=GCP_PROJECT_ID,
preemptible=False,
provisioning_model="SPOT",
)
]
check = compute_instance_preemptible_vm_disabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "VM Instance spot-vm is configured as Spot VM."
)
assert result[0].resource_id == "3333333333"
assert result[0].resource_name == "spot-vm"
assert result[0].location == "us-west1"
assert result[0].project_id == GCP_PROJECT_ID
def test_instance_standard_provisioning_model(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_preemptible_vm_disabled.compute_instance_preemptible_vm_disabled.compute_client",
new=compute_client,
),
):
from prowler.providers.gcp.services.compute.compute_instance_preemptible_vm_disabled.compute_instance_preemptible_vm_disabled import (
compute_instance_preemptible_vm_disabled,
)
from prowler.providers.gcp.services.compute.compute_service import Instance
compute_client.project_ids = [GCP_PROJECT_ID]
compute_client.instances = [
Instance(
name="standard-vm",
id="4444444444",
zone="asia-east1-a",
region="asia-east1",
public_ip=True,
metadata={},
shielded_enabled_vtpm=True,
shielded_enabled_integrity_monitoring=True,
confidential_computing=False,
service_accounts=[],
ip_forward=False,
disks_encryption=[],
project_id=GCP_PROJECT_ID,
preemptible=False,
provisioning_model="STANDARD",
)
]
check = compute_instance_preemptible_vm_disabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "VM Instance standard-vm is not preemptible or Spot VM."
)
assert result[0].resource_id == "4444444444"
assert result[0].resource_name == "standard-vm"
assert result[0].location == "asia-east1"
assert result[0].project_id == GCP_PROJECT_ID
def test_multiple_instances_spot_and_standard(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_preemptible_vm_disabled.compute_instance_preemptible_vm_disabled.compute_client",
new=compute_client,
),
):
from prowler.providers.gcp.services.compute.compute_instance_preemptible_vm_disabled.compute_instance_preemptible_vm_disabled import (
compute_instance_preemptible_vm_disabled,
)
from prowler.providers.gcp.services.compute.compute_service import Instance
compute_client.project_ids = [GCP_PROJECT_ID]
compute_client.instances = [
Instance(
name="spot-instance",
id="5555555555",
zone="us-central1-c",
region="us-central1",
public_ip=False,
metadata={},
shielded_enabled_vtpm=True,
shielded_enabled_integrity_monitoring=True,
confidential_computing=False,
service_accounts=[],
ip_forward=False,
disks_encryption=[],
project_id=GCP_PROJECT_ID,
preemptible=False,
provisioning_model="SPOT",
),
Instance(
name="standard-instance-2",
id="6666666666",
zone="europe-west2-a",
region="europe-west2",
public_ip=True,
metadata={},
shielded_enabled_vtpm=True,
shielded_enabled_integrity_monitoring=True,
confidential_computing=True,
service_accounts=[],
ip_forward=False,
disks_encryption=[],
project_id=GCP_PROJECT_ID,
preemptible=False,
provisioning_model="STANDARD",
),
]
check = compute_instance_preemptible_vm_disabled()
result = check.execute()
assert len(result) == 2
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "VM Instance spot-instance is configured as Spot VM."
)
assert result[0].resource_id == "5555555555"
assert result[0].resource_name == "spot-instance"
assert result[1].status == "PASS"
assert (
result[1].status_extended
== "VM Instance standard-instance-2 is not preemptible or Spot VM."
)
assert result[1].resource_id == "6666666666"
assert result[1].resource_name == "standard-instance-2"