Files
prowler/tests/providers/stackit/services/iaas/iaas_service_test.py
2026-07-08 17:13:36 +01:00

860 lines
31 KiB
Python

from unittest.mock import MagicMock, patch
from uuid import UUID
import pytest
from prowler.providers.stackit.exceptions.exceptions import StackITInvalidTokenError
from prowler.providers.stackit.services.iaas.iaas_service import (
IaaSService,
SecurityGroupRule,
)
from tests.providers.stackit.stackit_fixtures import (
STACKIT_PROJECT_ID,
set_mocked_stackit_provider,
)
def mock_iaas_fetch_all_regions(_):
"""Mock the _fetch_all_regions method to avoid real API calls."""
@patch(
"prowler.providers.stackit.services.iaas.iaas_service.IaaSService._fetch_all_regions",
new=mock_iaas_fetch_all_regions,
)
class Test_IaaS_Service:
def test_service_initialization(self):
"""Test that the IaaS service initializes correctly."""
iaas_service = IaaSService(set_mocked_stackit_provider())
assert iaas_service.project_id == STACKIT_PROJECT_ID
assert iaas_service.service_account_key_path is not None
assert isinstance(iaas_service.security_groups, list)
assert isinstance(iaas_service.server_nics, list)
assert isinstance(iaas_service.in_use_sg_ids, set)
assert iaas_service.scan_unused_services is False
assert isinstance(iaas_service.servers, list)
assert isinstance(iaas_service._nic_device_index, dict)
assert isinstance(iaas_service._public_ip_server_ids, set)
def test_service_project_id(self):
"""Test that the service correctly extracts project_id from provider."""
iaas_service = IaaSService(set_mocked_stackit_provider())
assert iaas_service.project_id == STACKIT_PROJECT_ID
def test_service_service_account_key_path(self):
"""Test that the service correctly extracts the SA key path from provider."""
custom_path = "/tmp/custom-sa.json"
provider = set_mocked_stackit_provider(service_account_key_path=custom_path)
iaas_service = IaaSService(provider)
assert iaas_service.service_account_key_path == custom_path
def test_security_groups_list_structure(self):
"""Test that security_groups is properly initialized as a list."""
iaas_service = IaaSService(set_mocked_stackit_provider())
assert hasattr(iaas_service, "security_groups")
assert isinstance(iaas_service.security_groups, list)
def test_in_use_sg_ids_set_structure(self):
"""Test that in_use_sg_ids is properly initialized as a set."""
iaas_service = IaaSService(set_mocked_stackit_provider())
assert hasattr(iaas_service, "in_use_sg_ids")
assert isinstance(iaas_service.in_use_sg_ids, set)
@pytest.mark.parametrize(
"method_name,client_method_name",
[
("_list_server_nics", "list_project_nics"),
("_list_security_groups", "list_security_groups"),
("_list_security_group_rules", "list_security_group_rules"),
("_list_public_ips", "list_public_ips"),
("_list_servers", "list_servers"),
],
)
def test_list_methods_propagate_api_errors(self, method_name, client_method_name):
"""API/auth failures must fail the scan instead of returning empty data."""
iaas_service = IaaSService(set_mocked_stackit_provider())
client = MagicMock()
getattr(client, client_method_name).side_effect = StackITInvalidTokenError(
message="Invalid token"
)
with pytest.raises(StackITInvalidTokenError):
if method_name == "_list_security_group_rules":
getattr(iaas_service, method_name)(client, "eu01", "sg-1")
else:
getattr(iaas_service, method_name)(client, "eu01")
def test_security_group_parsing_errors_are_skipped_locally(self):
"""Malformed resources are skipped while valid resources are retained."""
class MalformedSecurityGroup:
@property
def id(self):
raise ValueError("malformed security group")
iaas_service = IaaSService(set_mocked_stackit_provider())
client = MagicMock()
client.list_security_groups.return_value = [
MalformedSecurityGroup(),
{"id": "sg-1", "name": "valid-sg"},
]
client.list_security_group_rules.return_value = []
iaas_service._list_security_groups(client, "eu01")
assert len(iaas_service.security_groups) == 1
assert iaas_service.security_groups[0].id == "sg-1"
def test_in_use_considers_all_nics_not_only_public(self):
"""A SG attached to any NIC (public or private) counts as in_use."""
iaas_service = IaaSService(set_mocked_stackit_provider())
# NIC without a public IP, but has a security group attached
private_nic = {"id": "nic-private", "security_groups": ["sg-private"]}
used = iaas_service._get_used_security_group_ids([private_nic])
assert "sg-private" in used
def test_in_use_sg_ids_populated_via_list_server_nics(self):
"""_list_server_nics marks SGs on any NIC as in_use."""
iaas_service = IaaSService(set_mocked_stackit_provider())
client = MagicMock()
client.list_project_nics.return_value = [
{"id": "nic-1", "security_groups": ["sg-1"]},
{"id": "nic-2", "security_groups": ["sg-2"]},
]
iaas_service._list_server_nics(client, "eu01")
assert "sg-1" in iaas_service.in_use_sg_ids
assert "sg-2" in iaas_service.in_use_sg_ids
# Test SecurityGroupRule helper methods
class Test_SecurityGroupRule:
def test_is_unrestricted_with_none(self):
"""Test that None ip_range is considered unrestricted."""
rule = SecurityGroupRule(
id="rule-1",
direction="ingress",
protocol="tcp",
ip_range=None,
port_range_min=22,
port_range_max=22,
)
assert rule.is_unrestricted() is True
def test_is_unrestricted_with_cidr(self):
"""Test that 0.0.0.0/0 is considered unrestricted."""
rule = SecurityGroupRule(
id="rule-1",
direction="ingress",
protocol="tcp",
ip_range="0.0.0.0/0",
port_range_min=22,
port_range_max=22,
)
assert rule.is_unrestricted() is True
def test_is_unrestricted_with_ipv6(self):
"""Test that ::/0 is considered unrestricted."""
rule = SecurityGroupRule(
id="rule-1",
direction="ingress",
protocol="tcp",
ip_range="::/0",
port_range_min=22,
port_range_max=22,
)
assert rule.is_unrestricted() is True
def test_is_restricted_with_specific_ip(self):
"""Test that specific IP range is considered restricted."""
rule = SecurityGroupRule(
id="rule-1",
direction="ingress",
protocol="tcp",
ip_range="10.0.0.0/8",
port_range_min=22,
port_range_max=22,
)
assert rule.is_unrestricted() is False
def test_is_ingress_true(self):
"""Test that ingress direction is properly detected."""
rule = SecurityGroupRule(
id="rule-1",
direction="ingress",
protocol="tcp",
ip_range="0.0.0.0/0",
port_range_min=22,
port_range_max=22,
)
assert rule.is_ingress() is True
def test_is_ingress_false(self):
"""Test that egress direction returns false for is_ingress."""
rule = SecurityGroupRule(
id="rule-1",
direction="egress",
protocol="tcp",
ip_range="0.0.0.0/0",
port_range_min=22,
port_range_max=22,
)
assert rule.is_ingress() is False
def test_is_tcp_with_tcp_protocol(self):
"""Test that TCP protocol is detected correctly."""
rule = SecurityGroupRule(
id="rule-1",
direction="ingress",
protocol="tcp",
ip_range="0.0.0.0/0",
port_range_min=22,
port_range_max=22,
)
assert rule.is_tcp() is True
def test_is_tcp_with_none_protocol(self):
"""Test that None protocol is treated as TCP (all protocols)."""
rule = SecurityGroupRule(
id="rule-1",
direction="ingress",
protocol=None,
ip_range="0.0.0.0/0",
port_range_min=22,
port_range_max=22,
)
assert rule.is_tcp() is True
def test_is_tcp_with_all_protocol(self):
"""Test that 'all' protocol is treated as TCP."""
rule = SecurityGroupRule(
id="rule-1",
direction="ingress",
protocol="all",
ip_range="0.0.0.0/0",
port_range_min=22,
port_range_max=22,
)
assert rule.is_tcp() is True
def test_is_tcp_with_udp_protocol(self):
"""Test that UDP protocol returns false for is_tcp."""
rule = SecurityGroupRule(
id="rule-1",
direction="ingress",
protocol="udp",
ip_range="0.0.0.0/0",
port_range_min=22,
port_range_max=22,
)
assert rule.is_tcp() is False
def test_includes_port_exact_match(self):
"""Test that exact port match is detected."""
rule = SecurityGroupRule(
id="rule-1",
direction="ingress",
protocol="tcp",
ip_range="0.0.0.0/0",
port_range_min=22,
port_range_max=22,
)
assert rule.includes_port(22) is True
def test_includes_port_in_range(self):
"""Test that port within range is detected."""
rule = SecurityGroupRule(
id="rule-1",
direction="ingress",
protocol="tcp",
ip_range="0.0.0.0/0",
port_range_min=20,
port_range_max=25,
)
assert rule.includes_port(22) is True
assert rule.includes_port(20) is True
assert rule.includes_port(25) is True
def test_includes_port_outside_range(self):
"""Test that port outside range is not detected."""
rule = SecurityGroupRule(
id="rule-1",
direction="ingress",
protocol="tcp",
ip_range="0.0.0.0/0",
port_range_min=80,
port_range_max=443,
)
assert rule.includes_port(22) is False
def test_includes_port_with_none_range(self):
"""Test that None port range means all ports."""
rule = SecurityGroupRule(
id="rule-1",
direction="ingress",
protocol="tcp",
ip_range="0.0.0.0/0",
port_range_min=None,
port_range_max=None,
)
assert rule.includes_port(22) is True
assert rule.includes_port(443) is True
assert rule.includes_port(65535) is True
class Test_IaaS_Service_Extract_Items:
"""Cover ``IaaSService._extract_items`` against the three response shapes the
StackIT SDK can return. The previous implementation matched ``dict`` via
``hasattr(response, "items")`` and returned the bound method instead of the
items field.
"""
def test_extract_items_from_sdk_model(self):
"""SDK models expose ``items`` as a non-callable attribute."""
response = MagicMock(spec=["items"])
sentinel = [{"id": "sg-1"}, {"id": "sg-2"}]
response.items = sentinel
assert IaaSService._extract_items(response, "endpoint") is sentinel
def test_extract_items_from_dict_with_items_key(self):
"""Dict responses must use the ``items`` key, not ``dict.items()``."""
response = {"items": [{"id": "sg-1"}]}
assert IaaSService._extract_items(response, "endpoint") == [{"id": "sg-1"}]
def test_extract_items_from_empty_dict(self):
"""An empty dict yields an empty list, not the ``dict.items`` method."""
assert IaaSService._extract_items({}, "endpoint") == []
def test_extract_items_from_list(self):
"""A plain list response is returned as-is."""
response = [{"id": "sg-1"}]
assert IaaSService._extract_items(response, "endpoint") is response
def test_extract_items_unknown_shape_returns_empty(self):
"""Unknown shapes fall back to an empty list and log a warning."""
assert IaaSService._extract_items(42, "endpoint") == []
def test_extract_items_ignores_dict_items_method(self):
"""Regression: ``dict`` exposes ``items`` as a method; ensure the
``isinstance(dict)`` branch wins and we do not return the bound method.
"""
result = IaaSService._extract_items({"items": ["ok"]}, "endpoint")
assert result == ["ok"]
assert not callable(result)
class Test_IaaS_Service_Used_Security_Group_Ids:
"""Cover ``_get_used_security_group_ids``. The SDK returns NIC security
group references as ``uuid.UUID`` while a security group id is a ``str``;
they must be normalized to ``str`` so the in-use membership test matches.
"""
def _service(self):
return object.__new__(IaaSService)
def test_uuid_references_are_normalized_to_str(self):
from uuid import UUID
sg_uuid = UUID("2040c1fa-72a6-47bc-a53b-f62075ae6d35")
nic = MagicMock(spec=["security_groups"])
nic.security_groups = [sg_uuid]
used = self._service()._get_used_security_group_ids([nic])
# Stored as the string form so `str(sg.id) in used` matches.
assert used == {"2040c1fa-72a6-47bc-a53b-f62075ae6d35"}
assert all(isinstance(x, str) for x in used)
def test_dict_nic_camelcase_security_groups_key(self):
nic = {"securityGroups": ["sg-aaaa", "sg-bbbb"]}
used = self._service()._get_used_security_group_ids([nic])
assert used == {"sg-aaaa", "sg-bbbb"}
def test_empty_nic_security_groups(self):
nic = MagicMock(spec=["security_groups"])
nic.security_groups = []
assert self._service()._get_used_security_group_ids([nic]) == set()
def test_in_use_matches_uuid_reference_end_to_end(self):
"""A security group whose id (str) matches a NIC reference (UUID) must
be flagged in_use=True after a full region fetch.
"""
from uuid import UUID
sg_id = "2040c1fa-72a6-47bc-a53b-f62075ae6d35"
client = MagicMock()
nic = MagicMock(spec=["security_groups"])
nic.security_groups = [UUID(sg_id)]
client.list_project_nics.return_value = {"items": [nic]}
client.list_security_groups.return_value = {"items": [{"id": sg_id}]}
client.list_security_group_rules.return_value = {"items": []}
from prowler.providers.stackit.stackit_provider import StackitProvider
service = object.__new__(IaaSService)
service.provider = MagicMock()
service.provider.handle_api_error = StackitProvider.handle_api_error
service.project_id = STACKIT_PROJECT_ID
service.scan_unused_services = False
service.regional_clients = {"eu01": client}
service.security_groups = []
service.server_nics = []
service.in_use_sg_ids = set()
service._fetch_all_regions()
assert len(service.security_groups) == 1
assert service.security_groups[0].in_use is True
class Test_IaaS_Service_Fetch_All_Regions:
"""Cover ``_fetch_all_regions`` multi-region behaviour. A project is not
provisioned in every StackIT region; the region where it is absent answers
with HTTP 404. That must be skipped, not abort the whole scan (which
previously left every check failing to load with an empty report).
"""
class _NotFound(Exception):
status = 404
class _Forbidden(Exception):
status = 403
def _service(self, regional_clients):
from prowler.providers.stackit.stackit_provider import StackitProvider
service = object.__new__(IaaSService)
service.provider = MagicMock()
# Reuse the real centralized error handler so 401/403/404 semantics
# match production.
service.provider.handle_api_error = StackitProvider.handle_api_error
service.project_id = STACKIT_PROJECT_ID
service.scan_unused_services = True
service.regional_clients = regional_clients
service.security_groups = []
service.server_nics = []
service.in_use_sg_ids = set()
service.servers = []
service._nic_device_index = {}
service._public_ip_server_ids = set()
return service
def _good_client(self, sg_id="sg-eu01"):
client = MagicMock()
client.list_project_nics.return_value = {"items": []}
client.list_security_groups.return_value = {"items": [{"id": sg_id}]}
client.list_security_group_rules.return_value = {"items": []}
client.list_public_ips.return_value = {"items": []}
client.list_servers.return_value = {"items": []}
return client
def _missing_region_client(self):
client = MagicMock()
client.list_project_nics.side_effect = self._NotFound()
client.list_security_groups.side_effect = self._NotFound()
return client
def test_skips_region_where_project_is_absent(self):
service = self._service(
{"eu01": self._good_client(), "eu02": self._missing_region_client()}
)
service._fetch_all_regions()
# eu01 security group is collected; the eu02 404 is skipped silently.
assert [sg.id for sg in service.security_groups] == ["sg-eu01"]
def test_403_still_aborts(self):
bad = MagicMock()
bad.list_project_nics.side_effect = self._Forbidden()
service = self._service({"eu01": bad})
with pytest.raises(StackITInvalidTokenError):
service._fetch_all_regions()
class Test_IaaS_Service_Log_Skipped_Security_Groups:
"""``_log_skipped_security_groups`` should emit a hint only when groups
exist, none are in use, and ``scan_unused_services`` is off.
"""
def _service(self, security_groups, scan_unused_services):
service = object.__new__(IaaSService)
service.scan_unused_services = scan_unused_services
service.security_groups = security_groups
return service
def _sg(self, in_use):
sg = MagicMock()
sg.in_use = in_use
return sg
def test_logs_when_all_skipped(self, caplog):
import logging
service = self._service([self._sg(False), self._sg(False)], False)
with caplog.at_level(logging.INFO):
service._log_skipped_security_groups()
assert "scan-unused-services" in caplog.text
def test_no_log_when_scan_unused_services_enabled(self, caplog):
import logging
service = self._service([self._sg(False)], True)
with caplog.at_level(logging.INFO):
service._log_skipped_security_groups()
assert "scan-unused-services" not in caplog.text
def test_no_log_when_a_group_is_in_use(self, caplog):
import logging
service = self._service([self._sg(False), self._sg(True)], False)
with caplog.at_level(logging.INFO):
service._log_skipped_security_groups()
assert "scan-unused-services" not in caplog.text
def test_no_log_when_no_security_groups(self, caplog):
import logging
service = self._service([], False)
with caplog.at_level(logging.INFO):
service._log_skipped_security_groups()
assert "scan-unused-services" not in caplog.text
class Test_IaaS_Service_NIC_Device_Index:
"""NIC device index is built during _list_server_nics to enable
public IP → server cross-reference.
"""
def _service(self):
from prowler.providers.stackit.stackit_provider import StackitProvider
service = object.__new__(IaaSService)
service.provider = MagicMock()
service.provider.handle_api_error = StackitProvider.handle_api_error
service.project_id = STACKIT_PROJECT_ID
service.server_nics = []
service.in_use_sg_ids = set()
service._nic_device_index = {}
return service
def test_nic_with_id_and_device_is_indexed(self):
service = self._service()
nic_id = UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
device_id = UUID("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb")
nic = MagicMock()
nic.id = nic_id
nic.device = device_id
nic.security_groups = []
client = MagicMock()
client.list_project_nics.return_value = {"items": [nic]}
service._list_server_nics(client, "eu01")
assert str(nic_id) in service._nic_device_index
assert service._nic_device_index[str(nic_id)] == str(device_id)
def test_nic_without_device_is_not_indexed(self):
service = self._service()
nic = MagicMock()
nic.id = UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
nic.device = None
nic.security_groups = []
client = MagicMock()
client.list_project_nics.return_value = {"items": [nic]}
service._list_server_nics(client, "eu01")
assert service._nic_device_index == {}
def test_nic_indexing_error_is_skipped(self):
"""A NIC that raises while reading its id is skipped, not fatal."""
class MalformedNIC:
@property
def id(self):
raise ValueError("malformed nic")
service = self._service()
client = MagicMock()
client.list_project_nics.return_value = {"items": [MalformedNIC()]}
service._list_server_nics(client, "eu01")
assert service._nic_device_index == {}
def test_dict_shaped_nic_is_indexed(self):
"""Raw dict NICs are indexed the same as SDK model NICs."""
service = self._service()
client = MagicMock()
client.list_project_nics.return_value = {
"items": [{"id": "nic-1", "device": "server-1", "security_groups": []}]
}
service._list_server_nics(client, "eu01")
assert service._nic_device_index == {"nic-1": "server-1"}
class Test_IaaS_Service_PublicIps:
"""Tests for _list_public_ips."""
def _service(self):
from prowler.providers.stackit.stackit_provider import StackitProvider
service = object.__new__(IaaSService)
service.provider = MagicMock()
service.provider.handle_api_error = StackitProvider.handle_api_error
service.project_id = STACKIT_PROJECT_ID
service._nic_device_index = {}
service._public_ip_server_ids = set()
return service
def test_unattached_ip_is_ignored(self):
service = self._service()
ip = MagicMock()
ip.network_interface = None
client = MagicMock()
client.list_public_ips.return_value = {"items": [ip]}
service._list_public_ips(client, "eu01")
assert service._public_ip_server_ids == set()
def test_attached_ip_with_known_nic_marks_server(self):
nic_id = UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
server_id = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
service = self._service()
service._nic_device_index = {str(nic_id): server_id}
ip = MagicMock()
ip.network_interface = nic_id
client = MagicMock()
client.list_public_ips.return_value = {"items": [ip]}
service._list_public_ips(client, "eu01")
assert server_id in service._public_ip_server_ids
def test_attached_ip_with_unknown_nic_is_ignored(self):
service = self._service()
service._nic_device_index = {}
ip = MagicMock()
ip.network_interface = UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
client = MagicMock()
client.list_public_ips.return_value = {"items": [ip]}
service._list_public_ips(client, "eu01")
assert service._public_ip_server_ids == set()
def test_list_public_ips_without_client_is_noop(self):
"""A missing regional client is logged and skipped, not fatal."""
service = self._service()
service._list_public_ips(None, "eu01")
assert service._public_ip_server_ids == set()
def test_public_ip_processing_error_is_skipped(self):
"""A public IP that raises while being read is skipped, not fatal."""
class MalformedIP:
@property
def network_interface(self):
raise ValueError("malformed public ip")
service = self._service()
client = MagicMock()
client.list_public_ips.return_value = {"items": [MalformedIP()]}
service._list_public_ips(client, "eu01")
assert service._public_ip_server_ids == set()
def test_dict_shaped_public_ip_marks_server(self):
"""Raw dict public IPs (camelCase networkInterface) mark the server."""
service = self._service()
service._nic_device_index = {"nic-1": "server-1"}
client = MagicMock()
client.list_public_ips.return_value = {"items": [{"networkInterface": "nic-1"}]}
service._list_public_ips(client, "eu01")
assert "server-1" in service._public_ip_server_ids
class Test_IaaS_Service_Servers:
"""Tests for _list_servers."""
def _service(self):
from prowler.providers.stackit.stackit_provider import StackitProvider
service = object.__new__(IaaSService)
service.provider = MagicMock()
service.provider.handle_api_error = StackitProvider.handle_api_error
service.project_id = STACKIT_PROJECT_ID
service.servers = []
service._public_ip_server_ids = set()
return service
def test_server_without_public_ip(self):
server_id = UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
service = self._service()
server_data = MagicMock()
server_data.id = server_id
server_data.name = "my-server"
client = MagicMock()
client.list_servers.return_value = {"items": [server_data]}
service._list_servers(client, "eu01")
assert len(service.servers) == 1
assert service.servers[0].has_public_ip is False
def test_server_with_public_ip(self):
server_id = UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
service = self._service()
service._public_ip_server_ids = {str(server_id)}
server_data = MagicMock()
server_data.id = server_id
server_data.name = "my-server"
client = MagicMock()
client.list_servers.return_value = {"items": [server_data]}
service._list_servers(client, "eu01")
assert len(service.servers) == 1
assert service.servers[0].has_public_ip is True
def test_empty_response(self):
service = self._service()
client = MagicMock()
client.list_servers.return_value = {"items": []}
service._list_servers(client, "eu01")
assert service.servers == []
def test_server_public_ip_detected_end_to_end(self):
"""Full cross-reference: NIC → public IP → server flagged has_public_ip."""
from prowler.providers.stackit.stackit_provider import StackitProvider
nic_id = UUID("cccccccc-cccc-cccc-cccc-cccccccccccc")
server_id = UUID("dddddddd-dddd-dddd-dddd-dddddddddddd")
nic = MagicMock()
nic.id = nic_id
nic.device = server_id
nic.security_groups = []
ip = MagicMock()
ip.network_interface = nic_id
server_data = MagicMock()
server_data.id = server_id
server_data.name = "internet-server"
client = MagicMock()
client.list_project_nics.return_value = {"items": [nic]}
client.list_security_groups.return_value = {"items": []}
client.list_public_ips.return_value = {"items": [ip]}
client.list_servers.return_value = {"items": [server_data]}
service = object.__new__(IaaSService)
service.provider = MagicMock()
service.provider.handle_api_error = StackitProvider.handle_api_error
service.project_id = STACKIT_PROJECT_ID
service.scan_unused_services = False
service.regional_clients = {"eu01": client}
service.security_groups = []
service.server_nics = []
service.in_use_sg_ids = set()
service.servers = []
service._nic_device_index = {}
service._public_ip_server_ids = set()
service._fetch_all_regions()
assert len(service.servers) == 1
assert service.servers[0].has_public_ip is True
def test_list_servers_without_client_is_noop(self):
"""A missing regional client is logged and skipped, not fatal."""
service = self._service()
service._list_servers(None, "eu01")
assert service.servers == []
def test_server_processing_error_is_skipped(self):
"""A server that raises while being read is skipped, not fatal."""
class MalformedServer:
@property
def id(self):
raise ValueError("malformed server")
service = self._service()
client = MagicMock()
client.list_servers.return_value = {"items": [MalformedServer()]}
service._list_servers(client, "eu01")
assert service.servers == []
def test_dict_shaped_server_is_created(self):
"""Raw dict servers are created and flagged from _public_ip_server_ids."""
service = self._service()
service._public_ip_server_ids = {"server-1"}
client = MagicMock()
client.list_servers.return_value = {
"items": [{"id": "server-1", "name": "dict-server"}]
}
service._list_servers(client, "eu01")
assert len(service.servers) == 1
assert service.servers[0].name == "dict-server"
assert service.servers[0].has_public_ip is True
def test_dict_shaped_public_ip_detected_end_to_end(self):
"""Dict-shaped NIC/IP/server still correlate to has_public_ip=True.
Regression for the getattr-only correlation path: a dict-shaped
response must not silently report a PASS for an exposed server.
"""
from prowler.providers.stackit.stackit_provider import StackitProvider
client = MagicMock()
client.list_project_nics.return_value = {
"items": [{"id": "nic-1", "device": "server-1", "security_groups": []}]
}
client.list_security_groups.return_value = {"items": []}
client.list_public_ips.return_value = {"items": [{"networkInterface": "nic-1"}]}
client.list_servers.return_value = {
"items": [{"id": "server-1", "name": "dict-server"}]
}
service = object.__new__(IaaSService)
service.provider = MagicMock()
service.provider.handle_api_error = StackitProvider.handle_api_error
service.project_id = STACKIT_PROJECT_ID
service.scan_unused_services = False
service.regional_clients = {"eu01": client}
service.security_groups = []
service.server_nics = []
service.in_use_sg_ids = set()
service.servers = []
service._nic_device_index = {}
service._public_ip_server_ids = set()
service._fetch_all_regions()
assert len(service.servers) == 1
assert service.servers[0].has_public_ip is True