mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 21:11:53 +00:00
feat(gcp): add check to detect VMs with multiple network interfaces (#9702)
This commit is contained in:
@@ -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
|
||||
|
||||
+40
@@ -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."
|
||||
}
|
||||
+43
@@ -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):
|
||||
|
||||
+357
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user