mirror of
https://github.com/prowler-cloud/prowler.git
synced 2025-12-19 05:17:47 +00:00
feat(gcp-compute): add automatic restart check for VM instances (#9271)
This commit is contained in:
@@ -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_automatic_restart_enabled` check for GCP provider [(#9271)](https://github.com/prowler-cloud/prowler/pull/9271)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"Provider": "gcp",
|
||||
"CheckID": "compute_instance_automatic_restart_enabled",
|
||||
"CheckTitle": "Compute Engine VM instances have Automatic Restart enabled",
|
||||
"CheckType": [],
|
||||
"ServiceName": "compute",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "compute.googleapis.com/Instance",
|
||||
"Description": "**Google Compute Engine virtual machine instances** are evaluated to ensure that **Automatic Restart** is enabled. This feature allows the Google Cloud Compute Engine service to automatically restart VM instances when they are terminated due to non-user-initiated reasons such as maintenance events, hardware failures, or software failures.",
|
||||
"Risk": "VM instances without Automatic Restart enabled will not recover automatically from host maintenance events or unexpected failures, potentially leading to prolonged service downtime and requiring manual intervention to restore services.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/enable-automatic-restart.html",
|
||||
"https://cloud.google.com/compute/docs/instances/setting-instance-scheduling-options"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "gcloud compute instances update <INSTANCE_NAME> --restart-on-failure --zone=<ZONE>",
|
||||
"NativeIaC": "",
|
||||
"Other": "1) Open Google Cloud Console → Compute Engine → VM instances\n2) Click on the instance name to view details\n3) Click 'Edit' at the top of the page\n4) Under 'Availability policies', set 'Automatic restart' to 'On (recommended)'\n5) Click 'Save' at the bottom of the page",
|
||||
"Terraform": "```hcl\n# Example: enable Automatic Restart for a Compute Engine VM instance\nresource \"google_compute_instance\" \"example\" {\n name = var.instance_name\n machine_type = var.machine_type\n zone = var.zone\n\n scheduling {\n automatic_restart = true\n on_host_maintenance = \"MIGRATE\"\n }\n}\n```"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable the Automatic Restart feature for Compute Engine VM instances to enhance system reliability by automatically recovering from crashes or system-initiated terminations. This setting does not interfere with user-initiated shutdowns or stops.",
|
||||
"Url": "https://hub.prowler.com/check/compute_instance_automatic_restart_enabled"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"resilience"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "VM instances missing the 'scheduling.automaticRestart' field are treated as having Automatic Restart enabled (defaults to true). Preemptible instances and instances with provisioning model set to SPOT are automatically marked as PASS, as they cannot have Automatic Restart enabled by design."
|
||||
}
|
||||
@@ -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_automatic_restart_enabled(Check):
|
||||
"""
|
||||
Ensure Compute Engine VM instances have Automatic Restart enabled.
|
||||
|
||||
Reports PASS if a VM instance has automatic restart enabled, otherwise FAIL.
|
||||
"""
|
||||
|
||||
def execute(self) -> list[Check_Report_GCP]:
|
||||
findings = []
|
||||
for instance in compute_client.instances:
|
||||
report = Check_Report_GCP(metadata=self.metadata(), resource=instance)
|
||||
|
||||
# Preemptible and Spot VMs cannot have automatic restart enabled
|
||||
if instance.preemptible or instance.provisioning_model == "SPOT":
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"VM Instance {instance.name} is a Preemptible or Spot instance, "
|
||||
"which cannot have Automatic Restart enabled by design."
|
||||
)
|
||||
elif instance.automatic_restart:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"VM Instance {instance.name} has Automatic Restart enabled."
|
||||
)
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"VM Instance {instance.name} does not have Automatic Restart enabled."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -133,6 +133,15 @@ class Compute(GCPService):
|
||||
)
|
||||
for disk in instance.get("disks", [])
|
||||
],
|
||||
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,
|
||||
)
|
||||
)
|
||||
@@ -365,6 +374,9 @@ class Instance(BaseModel):
|
||||
service_accounts: list
|
||||
ip_forward: bool
|
||||
disks_encryption: list
|
||||
automatic_restart: bool = False
|
||||
preemptible: bool = False
|
||||
provisioning_model: str = "STANDARD"
|
||||
|
||||
|
||||
class Network(BaseModel):
|
||||
|
||||
@@ -759,6 +759,11 @@ def mock_api_instances_calls(client: MagicMock, service: str):
|
||||
"diskType": "disk_type",
|
||||
}
|
||||
],
|
||||
"scheduling": {
|
||||
"automaticRestart": False,
|
||||
"preemptible": False,
|
||||
"provisioningModel": "STANDARD",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "instance2",
|
||||
@@ -785,6 +790,11 @@ def mock_api_instances_calls(client: MagicMock, service: str):
|
||||
"diskType": "disk_type",
|
||||
}
|
||||
],
|
||||
"scheduling": {
|
||||
"automaticRestart": False,
|
||||
"preemptible": False,
|
||||
"provisioningModel": "STANDARD",
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
from unittest import mock
|
||||
|
||||
from tests.providers.gcp.gcp_fixtures import GCP_PROJECT_ID, set_mocked_gcp_provider
|
||||
|
||||
|
||||
class TestComputeInstanceAutomaticRestartEnabled:
|
||||
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_automatic_restart_enabled.compute_instance_automatic_restart_enabled.compute_client",
|
||||
new=compute_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.compute.compute_instance_automatic_restart_enabled.compute_instance_automatic_restart_enabled import (
|
||||
compute_instance_automatic_restart_enabled,
|
||||
)
|
||||
|
||||
check = compute_instance_automatic_restart_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_instance_with_automatic_restart_enabled(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_automatic_restart_enabled.compute_instance_automatic_restart_enabled.compute_client",
|
||||
new=compute_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.compute.compute_instance_automatic_restart_enabled.compute_instance_automatic_restart_enabled import (
|
||||
compute_instance_automatic_restart_enabled,
|
||||
)
|
||||
from prowler.providers.gcp.services.compute.compute_service import Instance
|
||||
|
||||
compute_client.project_ids = [GCP_PROJECT_ID]
|
||||
compute_client.instances = [
|
||||
Instance(
|
||||
name="test-instance",
|
||||
id="1234567890",
|
||||
zone="us-central1-a",
|
||||
region="us-central1",
|
||||
public_ip=True,
|
||||
metadata={},
|
||||
shielded_enabled_vtpm=True,
|
||||
shielded_enabled_integrity_monitoring=True,
|
||||
confidential_computing=False,
|
||||
service_accounts=[],
|
||||
ip_forward=False,
|
||||
disks_encryption=[("disk1", False)],
|
||||
automatic_restart=True,
|
||||
project_id=GCP_PROJECT_ID,
|
||||
)
|
||||
]
|
||||
|
||||
check = compute_instance_automatic_restart_enabled()
|
||||
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} has Automatic Restart enabled."
|
||||
)
|
||||
assert result[0].resource_id == compute_client.instances[0].id
|
||||
assert result[0].resource_name == compute_client.instances[0].name
|
||||
assert result[0].location == "us-central1"
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
|
||||
def test_instance_without_automatic_restart(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_automatic_restart_enabled.compute_instance_automatic_restart_enabled.compute_client",
|
||||
new=compute_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.compute.compute_instance_automatic_restart_enabled.compute_instance_automatic_restart_enabled import (
|
||||
compute_instance_automatic_restart_enabled,
|
||||
)
|
||||
from prowler.providers.gcp.services.compute.compute_service import Instance
|
||||
|
||||
compute_client.project_ids = [GCP_PROJECT_ID]
|
||||
compute_client.instances = [
|
||||
Instance(
|
||||
name="test-instance-disabled",
|
||||
id="0987654321",
|
||||
zone="us-west1-b",
|
||||
region="us-west1",
|
||||
public_ip=False,
|
||||
metadata={},
|
||||
shielded_enabled_vtpm=False,
|
||||
shielded_enabled_integrity_monitoring=False,
|
||||
confidential_computing=False,
|
||||
service_accounts=[],
|
||||
ip_forward=False,
|
||||
disks_encryption=[],
|
||||
automatic_restart=False,
|
||||
project_id=GCP_PROJECT_ID,
|
||||
)
|
||||
]
|
||||
|
||||
check = compute_instance_automatic_restart_enabled()
|
||||
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} does not have Automatic Restart enabled."
|
||||
)
|
||||
assert result[0].resource_id == compute_client.instances[0].id
|
||||
assert result[0].resource_name == compute_client.instances[0].name
|
||||
assert result[0].location == "us-west1"
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
|
||||
def test_multiple_instances_mixed(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_automatic_restart_enabled.compute_instance_automatic_restart_enabled.compute_client",
|
||||
new=compute_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.compute.compute_instance_automatic_restart_enabled.compute_instance_automatic_restart_enabled import (
|
||||
compute_instance_automatic_restart_enabled,
|
||||
)
|
||||
from prowler.providers.gcp.services.compute.compute_service import Instance
|
||||
|
||||
compute_client.project_ids = [GCP_PROJECT_ID]
|
||||
compute_client.instances = [
|
||||
Instance(
|
||||
name="compliant-instance",
|
||||
id="1111111111",
|
||||
zone="us-central1-a",
|
||||
region="us-central1",
|
||||
public_ip=True,
|
||||
metadata={},
|
||||
shielded_enabled_vtpm=True,
|
||||
shielded_enabled_integrity_monitoring=True,
|
||||
confidential_computing=False,
|
||||
service_accounts=[],
|
||||
ip_forward=False,
|
||||
disks_encryption=[],
|
||||
automatic_restart=True,
|
||||
project_id=GCP_PROJECT_ID,
|
||||
),
|
||||
Instance(
|
||||
name="non-compliant-instance",
|
||||
id="2222222222",
|
||||
zone="us-west1-b",
|
||||
region="us-west1",
|
||||
public_ip=False,
|
||||
metadata={},
|
||||
shielded_enabled_vtpm=False,
|
||||
shielded_enabled_integrity_monitoring=False,
|
||||
confidential_computing=False,
|
||||
service_accounts=[],
|
||||
ip_forward=False,
|
||||
disks_encryption=[],
|
||||
automatic_restart=False,
|
||||
project_id=GCP_PROJECT_ID,
|
||||
),
|
||||
]
|
||||
|
||||
check = compute_instance_automatic_restart_enabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 2
|
||||
|
||||
compliant_result = next(r for r in result if r.resource_id == "1111111111")
|
||||
non_compliant_result = next(
|
||||
r for r in result if r.resource_id == "2222222222"
|
||||
)
|
||||
|
||||
assert compliant_result.status == "PASS"
|
||||
assert (
|
||||
compliant_result.status_extended
|
||||
== "VM Instance compliant-instance has Automatic Restart enabled."
|
||||
)
|
||||
assert compliant_result.resource_id == "1111111111"
|
||||
assert compliant_result.resource_name == "compliant-instance"
|
||||
assert compliant_result.location == "us-central1"
|
||||
assert compliant_result.project_id == GCP_PROJECT_ID
|
||||
|
||||
assert non_compliant_result.status == "FAIL"
|
||||
assert (
|
||||
non_compliant_result.status_extended
|
||||
== "VM Instance non-compliant-instance does not have Automatic Restart enabled."
|
||||
)
|
||||
assert non_compliant_result.resource_id == "2222222222"
|
||||
assert non_compliant_result.resource_name == "non-compliant-instance"
|
||||
assert non_compliant_result.location == "us-west1"
|
||||
assert non_compliant_result.project_id == GCP_PROJECT_ID
|
||||
|
||||
def test_preemptible_instance_fails(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_automatic_restart_enabled.compute_instance_automatic_restart_enabled.compute_client",
|
||||
new=compute_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.compute.compute_instance_automatic_restart_enabled.compute_instance_automatic_restart_enabled import (
|
||||
compute_instance_automatic_restart_enabled,
|
||||
)
|
||||
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="3333333333",
|
||||
zone="us-central1-a",
|
||||
region="us-central1",
|
||||
public_ip=False,
|
||||
metadata={},
|
||||
shielded_enabled_vtpm=False,
|
||||
shielded_enabled_integrity_monitoring=False,
|
||||
confidential_computing=False,
|
||||
service_accounts=[],
|
||||
ip_forward=False,
|
||||
disks_encryption=[],
|
||||
automatic_restart=False,
|
||||
preemptible=True,
|
||||
provisioning_model="STANDARD",
|
||||
project_id=GCP_PROJECT_ID,
|
||||
)
|
||||
]
|
||||
|
||||
check = compute_instance_automatic_restart_enabled()
|
||||
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 a Preemptible or Spot instance, which cannot have Automatic Restart enabled by design."
|
||||
)
|
||||
assert result[0].resource_id == compute_client.instances[0].id
|
||||
assert result[0].resource_name == compute_client.instances[0].name
|
||||
assert result[0].location == "us-central1"
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
|
||||
def test_spot_instance_fails(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_automatic_restart_enabled.compute_instance_automatic_restart_enabled.compute_client",
|
||||
new=compute_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.compute.compute_instance_automatic_restart_enabled.compute_instance_automatic_restart_enabled import (
|
||||
compute_instance_automatic_restart_enabled,
|
||||
)
|
||||
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="4444444444",
|
||||
zone="us-west1-b",
|
||||
region="us-west1",
|
||||
public_ip=False,
|
||||
metadata={},
|
||||
shielded_enabled_vtpm=False,
|
||||
shielded_enabled_integrity_monitoring=False,
|
||||
confidential_computing=False,
|
||||
service_accounts=[],
|
||||
ip_forward=False,
|
||||
disks_encryption=[],
|
||||
automatic_restart=False,
|
||||
preemptible=False,
|
||||
provisioning_model="SPOT",
|
||||
project_id=GCP_PROJECT_ID,
|
||||
)
|
||||
]
|
||||
|
||||
check = compute_instance_automatic_restart_enabled()
|
||||
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 a Preemptible or Spot instance, which cannot have Automatic Restart enabled by design."
|
||||
)
|
||||
assert result[0].resource_id == compute_client.instances[0].id
|
||||
assert result[0].resource_name == compute_client.instances[0].name
|
||||
assert result[0].location == "us-west1"
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
@@ -57,6 +57,9 @@ class TestComputeService:
|
||||
]
|
||||
assert compute_client.instances[0].ip_forward
|
||||
assert compute_client.instances[0].disks_encryption == [("disk1", True)]
|
||||
assert not compute_client.instances[0].automatic_restart
|
||||
assert not compute_client.instances[0].preemptible
|
||||
assert compute_client.instances[0].provisioning_model == "STANDARD"
|
||||
|
||||
assert compute_client.instances[1].name == "instance2"
|
||||
assert compute_client.instances[1].id.__class__.__name__ == "str"
|
||||
@@ -78,6 +81,9 @@ class TestComputeService:
|
||||
]
|
||||
assert not compute_client.instances[1].ip_forward
|
||||
assert compute_client.instances[1].disks_encryption == [("disk2", False)]
|
||||
assert not compute_client.instances[1].automatic_restart
|
||||
assert not compute_client.instances[1].preemptible
|
||||
assert compute_client.instances[1].provisioning_model == "STANDARD"
|
||||
|
||||
assert len(compute_client.networks) == 3
|
||||
assert compute_client.networks[0].name == "network1"
|
||||
|
||||
Reference in New Issue
Block a user