mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-29 06:44:21 +00:00
fix(huaweicloud): enable FunctionGraph runtime discovery
- Add the pinned FunctionGraph SDK and regional session client - Paginate discovery without publishing partial API results - Preserve canonical function identities and VPC state in reports
This commit is contained in:
8 files changed
+261
-55
No files matched your search
@@ -384,6 +384,21 @@ class HuaweiCloudSession:
|
||||
.build()
|
||||
)
|
||||
|
||||
elif service == "functiongraph":
|
||||
from huaweicloudsdkfunctiongraph.v2 import FunctionGraphClient
|
||||
from huaweicloudsdkfunctiongraph.v2.region.functiongraph_region import (
|
||||
FunctionGraphRegion,
|
||||
)
|
||||
|
||||
client_region = region or self._region
|
||||
return (
|
||||
FunctionGraphClient.new_builder()
|
||||
.with_credentials(self._get_basic_credentials(client_region))
|
||||
.with_http_config(self._http_config())
|
||||
.with_region(_aligned_region(FunctionGraphRegion, client_region))
|
||||
.build()
|
||||
)
|
||||
|
||||
else:
|
||||
raise HuaweiCloudServiceError(
|
||||
message=f"Huawei Cloud service '{service}' is not supported"
|
||||
|
||||
+5
-6
@@ -14,16 +14,15 @@ class functiongraph_function_vpc_configured(Check):
|
||||
metadata=self.metadata(),
|
||||
resource=function,
|
||||
)
|
||||
report.region = function.region
|
||||
report.resource_id = function.function_id
|
||||
report.resource_arn = f"huaweicloud:functiongraph:{function.region}:{functiongraph_client.audited_account}:function/{function.function_id}"
|
||||
|
||||
if function.func_vpc_id:
|
||||
if function.vpc_id:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Function '{function.name}' is configured within VPC '{function.func_vpc_id}'."
|
||||
report.status_extended = f"Function '{function.name}' is configured within VPC '{function.vpc_id}'."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Function '{function.name}' is not associated with a VPC and has direct internet access without network restrictions."
|
||||
report.status_extended = (
|
||||
f"Function '{function.name}' is not configured within a VPC."
|
||||
)
|
||||
|
||||
findings.append(report)
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic.v1 import BaseModel
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.scan_filters.scan_filters import is_resource_filtered
|
||||
from prowler.providers.huaweicloud.lib.service.service import HuaweiCloudService
|
||||
from prowler.providers.huaweicloud.models import HuaweiCloudBaseModel
|
||||
|
||||
|
||||
class FunctionGraph(HuaweiCloudService):
|
||||
@@ -19,77 +19,93 @@ class FunctionGraph(HuaweiCloudService):
|
||||
|
||||
self.functions: List[FunctionGraphFunction] = []
|
||||
|
||||
if self.session.is_mock:
|
||||
if getattr(self.session, "is_mock", False):
|
||||
self._load_mock_data()
|
||||
return
|
||||
|
||||
self._list_functions()
|
||||
self.__threading_call__(self._list_functions)
|
||||
|
||||
def _load_mock_data(self):
|
||||
"""Load mock data for testing."""
|
||||
region = "la-south-2"
|
||||
self.functions = [
|
||||
FunctionGraphFunction(
|
||||
function_id="fg-mock-001",
|
||||
id="fg-mock-001",
|
||||
name="function-secure",
|
||||
arn="urn:fss:la-south-2:project:function:default:function-secure:latest",
|
||||
runtime="Python3.9",
|
||||
timeout=30,
|
||||
memory_size=128,
|
||||
func_vpc_id="vpc-12345",
|
||||
vpc_id="vpc-12345",
|
||||
region=region,
|
||||
),
|
||||
FunctionGraphFunction(
|
||||
function_id="fg-mock-002",
|
||||
id="fg-mock-002",
|
||||
name="function-insecure",
|
||||
arn="urn:fss:la-south-2:project:function:default:function-insecure:latest",
|
||||
runtime="Python3.9",
|
||||
timeout=30,
|
||||
memory_size=128,
|
||||
func_vpc_id=None,
|
||||
vpc_id=None,
|
||||
region=region,
|
||||
),
|
||||
]
|
||||
|
||||
def _list_functions(self):
|
||||
"""List all FunctionGraph functions across regions."""
|
||||
if not self.regional_clients:
|
||||
return
|
||||
def _list_functions(self, regional_client):
|
||||
"""List every FunctionGraph function in one region."""
|
||||
region = getattr(regional_client, "region", "unknown")
|
||||
logger.info(f"FunctionGraph - Listing Functions in {region}...")
|
||||
discovered_functions = []
|
||||
marker = None
|
||||
|
||||
for region, client in self.regional_clients.items():
|
||||
logger.info(f"FunctionGraph - Listing Functions in {region}...")
|
||||
try:
|
||||
from huaweicloudsdkfunctiongraph.v2 import ListFunctionsRequest
|
||||
|
||||
try:
|
||||
from huaweicloudsdkfunctiongraph.v2 import ListFunctionsRequest
|
||||
|
||||
request = ListFunctionsRequest()
|
||||
response = self._call_with_retries(client.list_functions, request)
|
||||
|
||||
if response and response.functions:
|
||||
for func in response.functions:
|
||||
self.functions.append(
|
||||
FunctionGraphFunction(
|
||||
function_id=getattr(func, "resource_id", ""),
|
||||
name=getattr(func, "func_name", ""),
|
||||
runtime=getattr(func, "runtime", ""),
|
||||
timeout=getattr(func, "timeout", 0),
|
||||
memory_size=getattr(func, "memory_size", 0),
|
||||
func_vpc_id=getattr(func, "func_vpc_id", None),
|
||||
region=region,
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
while True:
|
||||
request = ListFunctionsRequest(marker=marker, maxitems="400")
|
||||
response = self._call_with_retries(
|
||||
regional_client.list_functions, request
|
||||
)
|
||||
|
||||
for function in getattr(response, "functions", None) or []:
|
||||
resource_id = getattr(function, "resource_id", None) or ""
|
||||
if self.audit_resources and not is_resource_filtered(
|
||||
resource_id, self.audit_resources
|
||||
):
|
||||
continue
|
||||
discovered_functions.append(
|
||||
FunctionGraphFunction(
|
||||
id=resource_id,
|
||||
name=getattr(function, "func_name", None) or resource_id,
|
||||
arn=getattr(function, "func_urn", None) or "",
|
||||
runtime=getattr(function, "runtime", None) or "",
|
||||
timeout=getattr(function, "timeout", None) or 0,
|
||||
memory_size=getattr(function, "memory_size", None) or 0,
|
||||
vpc_id=getattr(function, "func_vpc_id", None),
|
||||
region=region,
|
||||
)
|
||||
)
|
||||
|
||||
class FunctionGraphFunction(BaseModel):
|
||||
next_marker = getattr(response, "next_marker", None)
|
||||
if not next_marker or str(next_marker) == marker:
|
||||
break
|
||||
marker = str(next_marker)
|
||||
|
||||
self.functions.extend(discovered_functions)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
class FunctionGraphFunction(HuaweiCloudBaseModel):
|
||||
"""FunctionGraph function model."""
|
||||
|
||||
function_id: str
|
||||
id: str
|
||||
name: str = ""
|
||||
arn: str = ""
|
||||
runtime: str = ""
|
||||
timeout: int = 0
|
||||
memory_size: int = 0
|
||||
func_vpc_id: Optional[str] = None
|
||||
vpc_id: Optional[str] = None
|
||||
region: str = ""
|
||||
@@ -123,6 +123,7 @@ dependencies = [
|
||||
"huaweicloudsdkecs==3.1.204",
|
||||
"huaweicloudsdkelb==3.1.204",
|
||||
"huaweicloudsdkevs==3.1.204",
|
||||
"huaweicloudsdkfunctiongraph==3.1.204",
|
||||
"huaweicloudsdkiam==3.1.204",
|
||||
"huaweicloudsdkkms==3.1.204",
|
||||
"huaweicloudsdkobs==3.1.204",
|
||||
@@ -260,6 +261,7 @@ constraint-dependencies = [
|
||||
"huaweicloudsdkecs==3.1.204",
|
||||
"huaweicloudsdkelb==3.1.204",
|
||||
"huaweicloudsdkevs==3.1.204",
|
||||
"huaweicloudsdkfunctiongraph==3.1.204",
|
||||
"huaweicloudsdkiam==3.1.204",
|
||||
"huaweicloudsdkkms==3.1.204",
|
||||
"huaweicloudsdkobs==3.1.204",
|
||||
|
||||
@@ -50,6 +50,31 @@ class TestHuaweiCloudProviderSetupSession:
|
||||
session = HuaweicloudProvider.setup_session()
|
||||
assert session.get_credentials().ak == ACCESS_KEY
|
||||
|
||||
def test_creates_functiongraph_client_for_europe(self):
|
||||
session = HuaweiCloudSession(
|
||||
HuaweiCloudCredentials(ak=ACCESS_KEY, sk=SECRET_KEY)
|
||||
)
|
||||
functiongraph_client = mock.MagicMock()
|
||||
builder = mock.MagicMock()
|
||||
builder.with_credentials.return_value.with_http_config.return_value.with_region.return_value.build.return_value = (
|
||||
functiongraph_client
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
"huaweicloudsdkfunctiongraph.v2.FunctionGraphClient.new_builder",
|
||||
return_value=builder,
|
||||
):
|
||||
client = session.client("functiongraph", "eu-west-101")
|
||||
|
||||
assert client is functiongraph_client
|
||||
region = builder.with_credentials.return_value.with_http_config.return_value.with_region.call_args.args[
|
||||
0
|
||||
]
|
||||
assert region.id == "eu-west-101"
|
||||
assert region.endpoints == [
|
||||
"https://functiongraph.eu-west-101.myhuaweicloud.eu"
|
||||
]
|
||||
|
||||
|
||||
class TestHuaweiCloudProviderValidateCredentials:
|
||||
def test_resolves_caller_identity_from_iam(self):
|
||||
|
||||
+15
-9
@@ -28,12 +28,13 @@ class Test_functiongraph_function_vpc_configured:
|
||||
|
||||
functiongraph_client.functions = [
|
||||
FunctionGraphFunction(
|
||||
function_id="fg-001",
|
||||
id="fg-001",
|
||||
name="function-secure",
|
||||
arn="urn:fss:la-south-2:project:function:default:function-secure:latest",
|
||||
runtime="Python3.9",
|
||||
timeout=30,
|
||||
memory_size=128,
|
||||
func_vpc_id="vpc-12345",
|
||||
vpc_id="vpc-12345",
|
||||
region="la-south-2",
|
||||
),
|
||||
]
|
||||
@@ -45,6 +46,8 @@ class Test_functiongraph_function_vpc_configured:
|
||||
assert len(results) == 1
|
||||
assert results[0].status == "PASS"
|
||||
assert results[0].resource_id == "fg-001"
|
||||
assert results[0].resource_name == "function-secure"
|
||||
assert results[0].resource_arn.endswith("function-secure:latest")
|
||||
assert "configured within VPC" in results[0].status_extended
|
||||
|
||||
def test_functiongraph_vpc_configured_fail(self):
|
||||
@@ -69,12 +72,13 @@ class Test_functiongraph_function_vpc_configured:
|
||||
|
||||
functiongraph_client.functions = [
|
||||
FunctionGraphFunction(
|
||||
function_id="fg-002",
|
||||
id="fg-002",
|
||||
name="function-insecure",
|
||||
arn="urn:fss:la-south-2:project:function:default:function-insecure:latest",
|
||||
runtime="Python3.9",
|
||||
timeout=30,
|
||||
memory_size=128,
|
||||
func_vpc_id=None,
|
||||
vpc_id=None,
|
||||
region="la-south-2",
|
||||
),
|
||||
]
|
||||
@@ -86,7 +90,7 @@ class Test_functiongraph_function_vpc_configured:
|
||||
assert len(results) == 1
|
||||
assert results[0].status == "FAIL"
|
||||
assert results[0].resource_id == "fg-002"
|
||||
assert "not associated with a VPC" in results[0].status_extended
|
||||
assert "not configured within a VPC" in results[0].status_extended
|
||||
|
||||
def test_functiongraph_vpc_configured_mixed(self):
|
||||
functiongraph_client = mock.MagicMock()
|
||||
@@ -110,21 +114,23 @@ class Test_functiongraph_function_vpc_configured:
|
||||
|
||||
functiongraph_client.functions = [
|
||||
FunctionGraphFunction(
|
||||
function_id="fg-001",
|
||||
id="fg-001",
|
||||
name="function-secure",
|
||||
arn="urn:fss:la-south-2:project:function:default:function-secure:latest",
|
||||
runtime="Python3.9",
|
||||
timeout=30,
|
||||
memory_size=128,
|
||||
func_vpc_id="vpc-12345",
|
||||
vpc_id="vpc-12345",
|
||||
region="la-south-2",
|
||||
),
|
||||
FunctionGraphFunction(
|
||||
function_id="fg-002",
|
||||
id="fg-002",
|
||||
name="function-insecure",
|
||||
arn="urn:fss:la-south-2:project:function:default:function-insecure:latest",
|
||||
runtime="Python3.9",
|
||||
timeout=30,
|
||||
memory_size=128,
|
||||
func_vpc_id=None,
|
||||
vpc_id=None,
|
||||
region="la-south-2",
|
||||
),
|
||||
]
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.huaweicloud.services.functiongraph.functiongraph_service import (
|
||||
FunctionGraph,
|
||||
)
|
||||
from tests.providers.huaweicloud.huaweicloud_fixtures import (
|
||||
set_mocked_huaweicloud_provider,
|
||||
)
|
||||
|
||||
|
||||
class TestFunctionGraphService:
|
||||
def test_real_session_lists_functions(self):
|
||||
regional_client = mock.MagicMock(region="eu-west-101")
|
||||
regional_client.list_functions.return_value = SimpleNamespace(
|
||||
functions=[], next_marker=None
|
||||
)
|
||||
provider = set_mocked_huaweicloud_provider()
|
||||
provider.session = SimpleNamespace(client=mock.MagicMock())
|
||||
provider.generate_regional_clients = mock.MagicMock(
|
||||
return_value={"eu-west-101": regional_client}
|
||||
)
|
||||
provider.get_default_region = mock.MagicMock(return_value="eu-west-101")
|
||||
|
||||
service = FunctionGraph(provider)
|
||||
|
||||
assert service.functions == []
|
||||
regional_client.list_functions.assert_called_once()
|
||||
|
||||
def test_lists_every_page_and_maps_sdk_fields(self):
|
||||
first_function = SimpleNamespace(
|
||||
resource_id="resource-1",
|
||||
func_urn="urn:fss:eu-west-101:project-1:function:default:first:latest",
|
||||
func_name="first",
|
||||
runtime=None,
|
||||
timeout=None,
|
||||
memory_size=None,
|
||||
func_vpc_id=None,
|
||||
)
|
||||
second_function = SimpleNamespace(
|
||||
resource_id="resource-2",
|
||||
func_urn="urn:fss:eu-west-101:project-1:function:default:second:latest",
|
||||
func_name="second",
|
||||
runtime="Python3.9",
|
||||
timeout=30,
|
||||
memory_size=128,
|
||||
func_vpc_id="vpc-1",
|
||||
)
|
||||
regional_client = mock.MagicMock(region="eu-west-101")
|
||||
regional_client.list_functions.side_effect = [
|
||||
SimpleNamespace(functions=[first_function], next_marker=400),
|
||||
SimpleNamespace(functions=[second_function], next_marker=None),
|
||||
]
|
||||
service = FunctionGraph.__new__(FunctionGraph)
|
||||
service.functions = []
|
||||
service.audit_resources = []
|
||||
service._call_with_retries = mock.MagicMock(
|
||||
side_effect=regional_client.list_functions.side_effect
|
||||
)
|
||||
|
||||
service._list_functions(regional_client)
|
||||
|
||||
assert [function.id for function in service.functions] == [
|
||||
"resource-1",
|
||||
"resource-2",
|
||||
]
|
||||
assert service.functions[0].arn == first_function.func_urn
|
||||
assert service.functions[0].runtime == ""
|
||||
assert service.functions[0].timeout == 0
|
||||
assert service.functions[0].memory_size == 0
|
||||
assert service.functions[0].vpc_id is None
|
||||
assert service.functions[1].vpc_id == "vpc-1"
|
||||
assert service._call_with_retries.call_count == 2
|
||||
first_request = service._call_with_retries.call_args_list[0].args[1]
|
||||
second_request = service._call_with_retries.call_args_list[1].args[1]
|
||||
assert first_request.maxitems == "400"
|
||||
assert first_request.marker is None
|
||||
assert second_request.marker == "400"
|
||||
|
||||
def test_api_error_does_not_fabricate_functions(self):
|
||||
function = SimpleNamespace(
|
||||
resource_id="resource-1",
|
||||
func_urn="urn:fss:eu-west-101:project-1:function:default:first:latest",
|
||||
func_name="first",
|
||||
runtime="Python3.9",
|
||||
timeout=30,
|
||||
memory_size=128,
|
||||
func_vpc_id=None,
|
||||
)
|
||||
regional_client = mock.MagicMock(region="eu-west-101")
|
||||
service = FunctionGraph.__new__(FunctionGraph)
|
||||
service.functions = []
|
||||
service.audit_resources = []
|
||||
service._call_with_retries = mock.MagicMock(
|
||||
side_effect=[
|
||||
SimpleNamespace(functions=[function], next_marker=400),
|
||||
Exception("boom"),
|
||||
]
|
||||
)
|
||||
|
||||
service._list_functions(regional_client)
|
||||
|
||||
assert service.functions == []
|
||||
|
||||
def test_repeated_marker_stops_pagination(self):
|
||||
function = SimpleNamespace(
|
||||
resource_id="resource-1",
|
||||
func_urn="urn:fss:eu-west-101:project-1:function:default:first:latest",
|
||||
func_name="first",
|
||||
runtime="Python3.9",
|
||||
timeout=30,
|
||||
memory_size=128,
|
||||
func_vpc_id=None,
|
||||
)
|
||||
regional_client = mock.MagicMock(region="eu-west-101")
|
||||
service = FunctionGraph.__new__(FunctionGraph)
|
||||
service.functions = []
|
||||
service.audit_resources = []
|
||||
service._call_with_retries = mock.MagicMock(
|
||||
side_effect=[
|
||||
SimpleNamespace(functions=[function], next_marker=400),
|
||||
SimpleNamespace(functions=[], next_marker=400),
|
||||
]
|
||||
)
|
||||
|
||||
service._list_functions(regional_client)
|
||||
|
||||
assert [item.id for item in service.functions] == ["resource-1"]
|
||||
assert service._call_with_retries.call_count == 2
|
||||
@@ -100,6 +100,7 @@ constraints = [
|
||||
{ name = "huaweicloudsdkecs", specifier = "==3.1.204" },
|
||||
{ name = "huaweicloudsdkelb", specifier = "==3.1.204" },
|
||||
{ name = "huaweicloudsdkevs", specifier = "==3.1.204" },
|
||||
{ name = "huaweicloudsdkfunctiongraph", specifier = "==3.1.204" },
|
||||
{ name = "huaweicloudsdkiam", specifier = "==3.1.204" },
|
||||
{ name = "huaweicloudsdkkms", specifier = "==3.1.204" },
|
||||
{ name = "huaweicloudsdkobs", specifier = "==3.1.204" },
|
||||
@@ -2310,6 +2311,17 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/cf/531dc55fd9d0f3bbd3eef24c7e4d78c6a1ba8eb80fa506e3574d72bcc98a/huaweicloudsdkevs-3.1.204-py3-none-any.whl", hash = "sha256:9118ac4c576e54aa7eaa926949e2b6824c5f038a2274b51d9a304d37fc0d7e2f", size = 251404, upload-time = "2026-07-09T09:02:50.05Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "huaweicloudsdkfunctiongraph"
|
||||
version = "3.1.204"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "huaweicloudsdkcore" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/10/19e2f8a0f15aa6c3c1b2f67eb58e5d06aac0b3eef527283624acfd946b0a/huaweicloudsdkfunctiongraph-3.1.204-py3-none-any.whl", hash = "sha256:0999e57f56b157817c9d8d68940279feff6c1170693e2f41eef21a1fd12a7de9", size = 556873, upload-time = "2026-07-09T09:02:52.402Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "huaweicloudsdkiam"
|
||||
version = "3.1.204"
|
||||
@@ -3753,6 +3765,7 @@ dependencies = [
|
||||
{ name = "huaweicloudsdkecs" },
|
||||
{ name = "huaweicloudsdkelb" },
|
||||
{ name = "huaweicloudsdkevs" },
|
||||
{ name = "huaweicloudsdkfunctiongraph" },
|
||||
{ name = "huaweicloudsdkiam" },
|
||||
{ name = "huaweicloudsdkkms" },
|
||||
{ name = "huaweicloudsdkobs" },
|
||||
@@ -3873,6 +3886,7 @@ requires-dist = [
|
||||
{ name = "huaweicloudsdkecs", specifier = "==3.1.204" },
|
||||
{ name = "huaweicloudsdkelb", specifier = "==3.1.204" },
|
||||
{ name = "huaweicloudsdkevs", specifier = "==3.1.204" },
|
||||
{ name = "huaweicloudsdkfunctiongraph", specifier = "==3.1.204" },
|
||||
{ name = "huaweicloudsdkiam", specifier = "==3.1.204" },
|
||||
{ name = "huaweicloudsdkkms", specifier = "==3.1.204" },
|
||||
{ name = "huaweicloudsdkobs", specifier = "==3.1.204" },
|
||||
|
||||
Reference in new issue
Block a user