feat(gcp): add check to detect VMs with multiple network interfaces (#9702)

This commit is contained in:
lydiavilchez
2026-01-07 17:04:53 +01:00
committed by GitHub
parent e12e0dc1aa
commit 3e4458c8f3
7 changed files with 472 additions and 1 deletions
+1
View File
@@ -13,6 +13,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Bedrock service pagination [(#9606)](https://github.com/prowler-cloud/prowler/pull/9606)
- `ResourceGroup` field to all check metadata for resource classification [(#9656)](https://github.com/prowler-cloud/prowler/pull/9656)
- `compute_instance_group_load_balancer_attached` check for GCP provider [(#9695)](https://github.com/prowler-cloud/prowler/pull/9695)
- `compute_instance_single_network_interface` check for GCP provider [(#9702)](https://github.com/prowler-cloud/prowler/pull/9702)
- `compute_image_not_publicly_shared` check for GCP provider [(#9718)](https://github.com/prowler-cloud/prowler/pull/9718)
### Changed
@@ -0,0 +1,40 @@
{
"Provider": "gcp",
"CheckID": "compute_instance_single_network_interface",
"CheckTitle": "VM instance has a single network interface",
"CheckType": [],
"ServiceName": "compute",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "compute.googleapis.com/Instance",
"ResourceGroup": "compute",
"Description": "VM instances should be configured with only **one network interface** unless multiple interfaces are explicitly required for complex network configurations.\n\nMultiple network interfaces expand the attack surface and create additional network pathways that may be exploited.",
"Risk": "Multiple network interfaces on a VM instance can:\n\n- **Expand attack surface** by providing additional entry points for unauthorized access\n- **Create unintended network paths** that bypass security controls\n- **Increase management complexity** leading to potential misconfigurations",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/vms-with-multiple-enis.html",
"https://cloud.google.com/vpc/docs/multiple-interfaces-concepts"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Create a machine image from the non-compliant VM instance\n2. Create a new VM instance from the machine image with only one network interface\n3. Verify the new instance is functioning correctly\n4. Delete the original multi-interface 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 boot_disk {\n initialize_params {\n image = \"debian-cloud/debian-11\"\n }\n }\n\n # Only one network interface\n network_interface {\n network = \"default\"\n }\n}\n```"
},
"Recommendation": {
"Text": "Configure VM instances with only the **minimum network connectivity** required for their intended purpose. Review instances with multiple network interfaces and consolidate to a single interface unless multi-NIC configuration is explicitly required for network appliance or routing purposes.",
"Url": "https://hub.prowler.com/check/compute_instance_single_network_interface"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [
"compute_instance_public_ip",
"compute_instance_ip_forwarding_is_enabled"
],
"Notes": "Instances created by GKE or used as network virtual appliances may legitimately require multiple network interfaces."
}
@@ -0,0 +1,43 @@
from prowler.lib.check.models import Check, Check_Report_GCP
from prowler.providers.gcp.services.compute.compute_client import compute_client
class compute_instance_single_network_interface(Check):
"""
Ensure that VM instances have a single network interface.
This check evaluates whether Compute Engine instances are configured with only
one network interface to minimize network complexity and reduce attack surface.
- PASS: The VM instance has a single network interface.
- MANUAL: The VM instance is a GKE-managed instance with multiple network interfaces
(manual review recommended as these may legitimately require multiple interfaces).
- FAIL: The VM instance has multiple network interfaces (excluding GKE instances).
"""
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"
interface_names = [nic.name for nic in instance.network_interfaces]
interface_count = len(instance.network_interfaces)
if interface_count == 1:
report.status_extended = f"VM Instance {instance.name} has a single network interface: {interface_names[0]}."
elif interface_count > 1:
# GKE instances may legitimately require multiple network interfaces
if instance.name.startswith("gke-"):
report.status = "MANUAL"
report.status_extended = f"VM Instance {instance.name} has {interface_count} network interfaces: {', '.join(interface_names)}. This is a GKE-managed instance which may legitimately require multiple interfaces. Manual review recommended."
else:
report.status = "FAIL"
report.status_extended = f"VM Instance {instance.name} has {interface_count} network interfaces: {', '.join(interface_names)}."
else:
report.status_extended = (
f"VM Instance {instance.name} has no network interfaces."
)
findings.append(report)
return findings
@@ -105,10 +105,30 @@ class Compute(GCPService):
for instance in response.get("items", []):
public_ip = False
for interface in instance.get("networkInterfaces", []):
network_interfaces_raw = instance.get("networkInterfaces", [])
network_interfaces = []
for interface in network_interfaces_raw:
for config in interface.get("accessConfigs", []):
if "natIP" in config:
public_ip = True
network_interfaces.append(
NetworkInterface(
name=interface.get("name", ""),
network=(
interface.get("network", "").split("/")[-1]
if interface.get("network")
else ""
),
subnetwork=(
interface.get("subnetwork", "").split("/")[-1]
if interface.get("subnetwork")
else ""
),
)
)
self.instances.append(
Instance(
name=instance["name"],
@@ -167,6 +187,7 @@ class Compute(GCPService):
deletion_protection=instance.get(
"deletionProtection", False
),
network_interfaces=network_interfaces,
)
)
@@ -582,6 +603,12 @@ class Compute(GCPService):
)
class NetworkInterface(BaseModel):
name: str
network: str = ""
subnetwork: str = ""
class Disk(BaseModel):
name: str
auto_delete: bool = False
@@ -608,6 +635,7 @@ class Instance(BaseModel):
preemptible: bool = False
provisioning_model: str = "STANDARD"
deletion_protection: bool = False
network_interfaces: list[NetworkInterface] = []
class Network(BaseModel):
@@ -0,0 +1,357 @@
from unittest import mock
from tests.providers.gcp.gcp_fixtures import GCP_PROJECT_ID, set_mocked_gcp_provider
class Test_compute_instance_single_network_interface:
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_single_network_interface.compute_instance_single_network_interface.compute_client",
new=compute_client,
),
):
from prowler.providers.gcp.services.compute.compute_instance_single_network_interface.compute_instance_single_network_interface import (
compute_instance_single_network_interface,
)
check = compute_instance_single_network_interface()
result = check.execute()
assert len(result) == 0
def test_single_network_interface(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_single_network_interface.compute_instance_single_network_interface.compute_client",
new=compute_client,
),
):
from prowler.providers.gcp.services.compute.compute_instance_single_network_interface.compute_instance_single_network_interface import (
compute_instance_single_network_interface,
)
from prowler.providers.gcp.services.compute.compute_service import (
Instance,
NetworkInterface,
)
instance = Instance(
name="test-instance",
id="1234567890",
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=[
{"email": "123-compute@developer.gserviceaccount.com"}
],
ip_forward=False,
disks_encryption=[],
project_id=GCP_PROJECT_ID,
network_interfaces=[
NetworkInterface(
name="nic0", network="default", subnetwork="default"
)
],
)
compute_client.project_ids = [GCP_PROJECT_ID]
compute_client.instances = [instance]
check = compute_instance_single_network_interface()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "VM Instance test-instance has a single network interface: nic0."
)
assert result[0].resource_id == "1234567890"
assert result[0].project_id == GCP_PROJECT_ID
assert result[0].resource_name == "test-instance"
assert result[0].location == "us-central1"
def test_multiple_network_interfaces(self):
from prowler.providers.gcp.services.compute.compute_service import (
Instance,
NetworkInterface,
)
instance = Instance(
name="multi-nic-instance",
id="9876543210",
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=[
{"email": f"{GCP_PROJECT_ID}-compute@developer.gserviceaccount.com"}
],
ip_forward=False,
disks_encryption=[],
project_id=GCP_PROJECT_ID,
network_interfaces=[
NetworkInterface(name="nic0", network="default", subnetwork="subnet-1"),
NetworkInterface(name="nic1", network="vpc-2", subnetwork="subnet-2"),
NetworkInterface(name="nic2", network="vpc-3", subnetwork="subnet-3"),
],
)
compute_client = mock.MagicMock()
compute_client.project_ids = [GCP_PROJECT_ID]
compute_client.instances = [instance]
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_single_network_interface.compute_instance_single_network_interface.compute_client",
new=compute_client,
),
):
from prowler.providers.gcp.services.compute.compute_instance_single_network_interface.compute_instance_single_network_interface import (
compute_instance_single_network_interface,
)
check = compute_instance_single_network_interface()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "VM Instance multi-nic-instance has 3 network interfaces: nic0, nic1, nic2."
)
assert result[0].resource_id == "9876543210"
assert result[0].project_id == GCP_PROJECT_ID
assert result[0].resource_name == "multi-nic-instance"
assert result[0].location == "us-central1"
def test_two_network_interfaces(self):
from prowler.providers.gcp.services.compute.compute_service import (
Instance,
NetworkInterface,
)
instance = Instance(
name="dual-nic-instance",
id="1111111111",
zone="europe-west1-b",
region="europe-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,
network_interfaces=[
NetworkInterface(name="nic0", network="default", subnetwork="default"),
NetworkInterface(name="nic1", network="vpc-2", subnetwork="subnet-2"),
],
)
compute_client = mock.MagicMock()
compute_client.project_ids = [GCP_PROJECT_ID]
compute_client.instances = [instance]
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_single_network_interface.compute_instance_single_network_interface.compute_client",
new=compute_client,
),
):
from prowler.providers.gcp.services.compute.compute_instance_single_network_interface.compute_instance_single_network_interface import (
compute_instance_single_network_interface,
)
check = compute_instance_single_network_interface()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "VM Instance dual-nic-instance has 2 network interfaces: nic0, nic1."
)
assert result[0].resource_id == "1111111111"
assert result[0].project_id == GCP_PROJECT_ID
assert result[0].resource_name == "dual-nic-instance"
assert result[0].location == "europe-west1"
def test_mixed_instances(self):
from prowler.providers.gcp.services.compute.compute_service import (
Instance,
NetworkInterface,
)
instance_single_nic = Instance(
name="single-nic-instance",
id="1111111111",
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,
network_interfaces=[
NetworkInterface(name="nic0", network="default", subnetwork="default")
],
)
instance_multi_nic = Instance(
name="multi-nic-instance",
id="2222222222",
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=[],
project_id=GCP_PROJECT_ID,
network_interfaces=[
NetworkInterface(name="nic0", network="default", subnetwork="default"),
NetworkInterface(name="nic1", network="vpc-2", subnetwork="subnet-2"),
NetworkInterface(name="nic2", network="vpc-3", subnetwork="subnet-3"),
NetworkInterface(name="nic3", network="vpc-4", subnetwork="subnet-4"),
],
)
compute_client = mock.MagicMock()
compute_client.project_ids = [GCP_PROJECT_ID]
compute_client.instances = [instance_single_nic, instance_multi_nic]
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_single_network_interface.compute_instance_single_network_interface.compute_client",
new=compute_client,
),
):
from prowler.providers.gcp.services.compute.compute_instance_single_network_interface.compute_instance_single_network_interface import (
compute_instance_single_network_interface,
)
check = compute_instance_single_network_interface()
result = check.execute()
assert len(result) == 2
# First instance: single NIC (PASS)
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "VM Instance single-nic-instance has a single network interface: nic0."
)
assert result[0].resource_id == "1111111111"
assert result[0].resource_name == "single-nic-instance"
# Second instance: multiple NICs (FAIL)
assert result[1].status == "FAIL"
assert (
result[1].status_extended
== "VM Instance multi-nic-instance has 4 network interfaces: nic0, nic1, nic2, nic3."
)
assert result[1].resource_id == "2222222222"
assert result[1].resource_name == "multi-nic-instance"
def test_gke_instance_multiple_network_interfaces(self):
from prowler.providers.gcp.services.compute.compute_service import (
Instance,
NetworkInterface,
)
instance = Instance(
name="gke-cluster-default-pool-12345678-abcd",
id="9999999999",
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,
network_interfaces=[
NetworkInterface(
name="nic0", network="gke-network", subnetwork="gke-subnet"
),
NetworkInterface(
name="nic1", network="gke-network-2", subnetwork="gke-subnet-2"
),
],
)
compute_client = mock.MagicMock()
compute_client.project_ids = [GCP_PROJECT_ID]
compute_client.instances = [instance]
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_single_network_interface.compute_instance_single_network_interface.compute_client",
new=compute_client,
),
):
from prowler.providers.gcp.services.compute.compute_instance_single_network_interface.compute_instance_single_network_interface import (
compute_instance_single_network_interface,
)
check = compute_instance_single_network_interface()
result = check.execute()
assert len(result) == 1
assert result[0].status == "MANUAL"
assert (
result[0].status_extended
== "VM Instance gke-cluster-default-pool-12345678-abcd has 2 network interfaces: nic0, nic1. This is a GKE-managed instance which may legitimately require multiple interfaces. Manual review recommended."
)
assert result[0].resource_id == "9999999999"
assert result[0].project_id == GCP_PROJECT_ID
assert result[0].resource_name == "gke-cluster-default-pool-12345678-abcd"
assert result[0].location == "us-central1"
@@ -60,6 +60,7 @@ class TestComputeService:
assert not compute_client.instances[0].automatic_restart
assert not compute_client.instances[0].preemptible
assert compute_client.instances[0].provisioning_model == "STANDARD"
assert len(compute_client.instances[0].network_interfaces) == 1
assert compute_client.instances[1].name == "instance2"
assert compute_client.instances[1].id.__class__.__name__ == "str"
@@ -84,6 +85,7 @@ class TestComputeService:
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.instances[1].network_interfaces) == 0
assert len(compute_client.networks) == 3
assert compute_client.networks[0].name == "network1"