mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 21:11:53 +00:00
feat(azure/vm): add new check vm_scaleset_not_empty (#8192)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
This commit is contained in:
committed by
GitHub
parent
d9a9236ab7
commit
1cfe610d47
@@ -9,6 +9,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- Support App Key Content in GitHub provider [(#8271)](https://github.com/prowler-cloud/prowler/pull/8271)
|
||||
- CIS 4.0 for the Azure provider [(#7782)](https://github.com/prowler-cloud/prowler/pull/7782)
|
||||
- `vm_desired_sku_size` check for Azure provider [(#8191)](https://github.com/prowler-cloud/prowler/pull/8191)
|
||||
- `vm_scaleset_not_empty` check for Azure provider [(#8192)](https://github.com/prowler-cloud/prowler/pull/8192)
|
||||
|
||||
### Changed
|
||||
- Handle some AWS errors as warnings instead of errors [(#8347)](https://github.com/prowler-cloud/prowler/pull/8347)
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"Provider": "azure",
|
||||
"CheckID": "vm_scaleset_not_empty",
|
||||
"CheckTitle": "Check for Empty Virtual Machine Scale Sets",
|
||||
"CheckType": [],
|
||||
"ServiceName": "vm",
|
||||
"SubServiceName": "scaleset",
|
||||
"ResourceIdTemplate": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachineScaleSets/{vmScaleSetName}",
|
||||
"Severity": "low",
|
||||
"ResourceType": "Microsoft.Compute/virtualMachineScaleSets",
|
||||
"Description": "Identify and remove empty virtual machine scale sets from your Azure cloud account.",
|
||||
"Risk": "Empty virtual machine scale sets may incur unnecessary costs and complicate cloud resource management, impacting cost optimization and compliance.",
|
||||
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/virtual-machine-scale-sets/overview",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "az vmss delete --name <scale-set-name> --resource-group <resource-group>",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/empty-vm-scale-sets.html",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Remove empty Azure virtual machine scale sets to optimize costs and simplify management.",
|
||||
"Url": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/empty-vm-scale-sets.html"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_Azure
|
||||
from prowler.providers.azure.services.vm.vm_client import vm_client
|
||||
|
||||
|
||||
class vm_scaleset_not_empty(Check):
|
||||
"""
|
||||
Ensure that Azure virtual machine scale sets are not empty (i.e., have no VM instances and no load balancer attached).
|
||||
|
||||
This check evaluates whether each VM scale set has zero VM instances and is not associated with any load balancer backend pool.
|
||||
- PASS: The scale set has at least one VM instance or is associated with a load balancer backend pool.
|
||||
- FAIL: The scale set has no VM instances and is not associated with any load balancer backend pool (i.e., it is empty).
|
||||
"""
|
||||
|
||||
def execute(self):
|
||||
findings = []
|
||||
for subscription, scale_sets in vm_client.vm_scale_sets.items():
|
||||
for scale_set in scale_sets.values():
|
||||
report = Check_Report_Azure(
|
||||
metadata=self.metadata(), resource=scale_set
|
||||
)
|
||||
report.subscription = subscription
|
||||
if not scale_set.instance_ids:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Scale set '{scale_set.resource_name}' in subscription '{subscription}' is empty: no VM instances present."
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Scale set '{scale_set.resource_name}' in subscription '{subscription}' has {len(scale_set.instance_ids)} VM instances."
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -234,12 +234,17 @@ class VirtualMachines(AzureService):
|
||||
for pool in pools:
|
||||
if getattr(pool, "id", None):
|
||||
backend_pools.append(pool.id)
|
||||
# Get instance IDs using the private method
|
||||
instance_ids = self._get_vmss_instance_ids(
|
||||
subscription_name, scale_set.id
|
||||
)
|
||||
vm_scale_sets[subscription_name][scale_set.id] = (
|
||||
VirtualMachineScaleSet(
|
||||
resource_id=scale_set.id,
|
||||
resource_name=scale_set.name,
|
||||
location=scale_set.location,
|
||||
load_balancer_backend_pools=backend_pools,
|
||||
instance_ids=instance_ids,
|
||||
)
|
||||
)
|
||||
except Exception as error:
|
||||
@@ -248,6 +253,46 @@ class VirtualMachines(AzureService):
|
||||
)
|
||||
return vm_scale_sets
|
||||
|
||||
def _get_vmss_instance_ids(
|
||||
self, subscription_name: str, scale_set_id: str
|
||||
) -> list[str]:
|
||||
"""
|
||||
Given a subscription and scale set ID, return the list of VM instance IDs in the scale set.
|
||||
|
||||
Args:
|
||||
subscription_name: The name of the subscription.
|
||||
scale_set_id: The ID of the scale set.
|
||||
|
||||
Returns:
|
||||
A list of VM instance IDs that compose the scale set.
|
||||
"""
|
||||
logger.info(
|
||||
f"VirtualMachines - Getting VM scale set instance IDs for {scale_set_id} in {subscription_name}..."
|
||||
)
|
||||
vm_instance_ids = []
|
||||
client = self.clients.get(subscription_name, None)
|
||||
try:
|
||||
resource_id_parts = scale_set_id.split("/")
|
||||
resource_group = ""
|
||||
scale_set_name = ""
|
||||
for i, part in enumerate(resource_id_parts):
|
||||
if part.lower() == "resourcegroups" and i + 1 < len(resource_id_parts):
|
||||
resource_group = resource_id_parts[i + 1]
|
||||
if part.lower() == "virtualmachinescalesets" and i + 1 < len(
|
||||
resource_id_parts
|
||||
):
|
||||
scale_set_name = resource_id_parts[i + 1]
|
||||
if resource_group and scale_set_name:
|
||||
instances = client.virtual_machine_scale_set_vms.list(
|
||||
resource_group, scale_set_name
|
||||
)
|
||||
vm_instance_ids = [instance.instance_id for instance in instances]
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to list instances for scale set {scale_set_name} in {resource_group}: {e}"
|
||||
)
|
||||
return vm_instance_ids
|
||||
|
||||
|
||||
@dataclass
|
||||
class UefiSettings:
|
||||
@@ -320,3 +365,4 @@ class VirtualMachineScaleSet(BaseModel):
|
||||
resource_name: str
|
||||
location: str
|
||||
load_balancer_backend_pools: list[str]
|
||||
instance_ids: list[str]
|
||||
|
||||
+5
@@ -59,6 +59,7 @@ class Test_vm_scaleset_associated_with_load_balancer:
|
||||
resource_name="compliant-vmss",
|
||||
location="eastus",
|
||||
load_balancer_backend_pools=[backend_pool_id],
|
||||
instance_ids=[],
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -98,6 +99,7 @@ class Test_vm_scaleset_associated_with_load_balancer:
|
||||
resource_name="noncompliant-vmss",
|
||||
location="westeurope",
|
||||
load_balancer_backend_pools=[],
|
||||
instance_ids=[],
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -139,12 +141,14 @@ class Test_vm_scaleset_associated_with_load_balancer:
|
||||
resource_name="compliant-vmss",
|
||||
location="eastus",
|
||||
load_balancer_backend_pools=[backend_pool_id],
|
||||
instance_ids=[],
|
||||
),
|
||||
noncompliant_id: VirtualMachineScaleSet(
|
||||
resource_id=noncompliant_id,
|
||||
resource_name="noncompliant-vmss",
|
||||
location="westeurope",
|
||||
load_balancer_backend_pools=[],
|
||||
instance_ids=[],
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -191,6 +195,7 @@ class Test_vm_scaleset_associated_with_load_balancer:
|
||||
resource_name="",
|
||||
location="",
|
||||
load_balancer_backend_pools=[],
|
||||
instance_ids=[],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
from prowler.providers.azure.services.vm.vm_service import VirtualMachineScaleSet
|
||||
from tests.providers.azure.azure_fixtures import (
|
||||
AZURE_SUBSCRIPTION_ID,
|
||||
set_mocked_azure_provider,
|
||||
)
|
||||
|
||||
|
||||
class Test_vm_scaleset_not_empty:
|
||||
def test_no_subscriptions(self):
|
||||
vm_scale_sets = {}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_client.vm_client.vm_scale_sets",
|
||||
new=vm_scale_sets,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.vm.vm_scaleset_not_empty.vm_scaleset_not_empty import (
|
||||
vm_scaleset_not_empty,
|
||||
)
|
||||
|
||||
check = vm_scaleset_not_empty()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_empty_scale_sets(self):
|
||||
vm_scale_sets = {AZURE_SUBSCRIPTION_ID: {}}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_client.vm_client.vm_scale_sets",
|
||||
new=vm_scale_sets,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.vm.vm_scaleset_not_empty.vm_scaleset_not_empty import (
|
||||
vm_scaleset_not_empty,
|
||||
)
|
||||
|
||||
check = vm_scaleset_not_empty()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_scale_set_with_no_instances(self):
|
||||
vmss_id = str(uuid4())
|
||||
vm_scale_sets = {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
vmss_id: VirtualMachineScaleSet(
|
||||
resource_id=vmss_id,
|
||||
resource_name="empty-vmss",
|
||||
location="eastus",
|
||||
load_balancer_backend_pools=[],
|
||||
instance_ids=[],
|
||||
)
|
||||
}
|
||||
}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_client.vm_client.vm_scale_sets",
|
||||
new=vm_scale_sets,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.vm.vm_scaleset_not_empty.vm_scaleset_not_empty import (
|
||||
vm_scaleset_not_empty,
|
||||
)
|
||||
|
||||
check = vm_scaleset_not_empty()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].resource_id == vmss_id
|
||||
assert result[0].resource_name == "empty-vmss"
|
||||
assert result[0].location == "eastus"
|
||||
expected_status_extended = f"Scale set 'empty-vmss' in subscription '{AZURE_SUBSCRIPTION_ID}' is empty: no VM instances present."
|
||||
assert result[0].status_extended == expected_status_extended
|
||||
|
||||
def test_scale_set_with_instances(self):
|
||||
vmss_id = str(uuid4())
|
||||
instance_ids = ["1", "2"]
|
||||
vm_scale_sets = {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
vmss_id: VirtualMachineScaleSet(
|
||||
resource_id=vmss_id,
|
||||
resource_name="nonempty-vmss",
|
||||
location="westeurope",
|
||||
load_balancer_backend_pools=[],
|
||||
instance_ids=instance_ids,
|
||||
)
|
||||
}
|
||||
}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_client.vm_client.vm_scale_sets",
|
||||
new=vm_scale_sets,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.vm.vm_scaleset_not_empty.vm_scaleset_not_empty import (
|
||||
vm_scaleset_not_empty,
|
||||
)
|
||||
|
||||
check = vm_scaleset_not_empty()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].resource_id == vmss_id
|
||||
assert result[0].resource_name == "nonempty-vmss"
|
||||
assert result[0].location == "westeurope"
|
||||
expected_status_extended = f"Scale set 'nonempty-vmss' in subscription '{AZURE_SUBSCRIPTION_ID}' has {len(instance_ids)} VM instances."
|
||||
assert result[0].status_extended == expected_status_extended
|
||||
|
||||
def test_multiple_scale_sets(self):
|
||||
empty_id = str(uuid4())
|
||||
nonempty_id = str(uuid4())
|
||||
instance_ids = ["1"]
|
||||
vm_scale_sets = {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
empty_id: VirtualMachineScaleSet(
|
||||
resource_id=empty_id,
|
||||
resource_name="empty-vmss",
|
||||
location="eastus",
|
||||
load_balancer_backend_pools=[],
|
||||
instance_ids=[],
|
||||
),
|
||||
nonempty_id: VirtualMachineScaleSet(
|
||||
resource_id=nonempty_id,
|
||||
resource_name="nonempty-vmss",
|
||||
location="westeurope",
|
||||
load_balancer_backend_pools=[],
|
||||
instance_ids=instance_ids,
|
||||
),
|
||||
}
|
||||
}
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.vm.vm_client.vm_client.vm_scale_sets",
|
||||
new=vm_scale_sets,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.vm.vm_scaleset_not_empty.vm_scaleset_not_empty import (
|
||||
vm_scaleset_not_empty,
|
||||
)
|
||||
|
||||
check = vm_scaleset_not_empty()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
for r in result:
|
||||
if r.resource_name == "empty-vmss":
|
||||
expected_status_extended = f"Scale set 'empty-vmss' in subscription '{AZURE_SUBSCRIPTION_ID}' is empty: no VM instances present."
|
||||
assert r.status == "FAIL"
|
||||
assert r.status_extended == expected_status_extended
|
||||
elif r.resource_name == "nonempty-vmss":
|
||||
expected_status_extended = f"Scale set 'nonempty-vmss' in subscription '{AZURE_SUBSCRIPTION_ID}' has {len(instance_ids)} VM instances."
|
||||
assert r.status == "PASS"
|
||||
assert r.status_extended == expected_status_extended
|
||||
Reference in New Issue
Block a user