From ec6878c8241fcbf629feeb2e8b549b7aa636cab8 Mon Sep 17 00:00:00 2001 From: tomitobio Date: Wed, 29 Jul 2026 04:11:52 +0800 Subject: [PATCH] feat(providers/huaweicloud): add functiongraph_function_vpc_configured check --- .../services/functiongraph/__init__.py | 0 .../functiongraph/functiongraph_client.py | 6 + .../__init__.py | 0 ...raph_function_vpc_configured.metadata.json | 34 ++++ .../functiongraph_function_vpc_configured.py | 30 ++++ .../functiongraph/functiongraph_service.py | 95 ++++++++++ ...ctiongraph_function_vpc_configured_test.py | 163 ++++++++++++++++++ 7 files changed, 328 insertions(+) create mode 100644 prowler/providers/huaweicloud/services/functiongraph/__init__.py create mode 100644 prowler/providers/huaweicloud/services/functiongraph/functiongraph_client.py create mode 100644 prowler/providers/huaweicloud/services/functiongraph/functiongraph_function_vpc_configured/__init__.py create mode 100644 prowler/providers/huaweicloud/services/functiongraph/functiongraph_function_vpc_configured/functiongraph_function_vpc_configured.metadata.json create mode 100644 prowler/providers/huaweicloud/services/functiongraph/functiongraph_function_vpc_configured/functiongraph_function_vpc_configured.py create mode 100644 prowler/providers/huaweicloud/services/functiongraph/functiongraph_service.py create mode 100644 tests/providers/huaweicloud/services/functiongraph/functiongraph_function_vpc_configured/functiongraph_function_vpc_configured_test.py diff --git a/prowler/providers/huaweicloud/services/functiongraph/__init__.py b/prowler/providers/huaweicloud/services/functiongraph/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/huaweicloud/services/functiongraph/functiongraph_client.py b/prowler/providers/huaweicloud/services/functiongraph/functiongraph_client.py new file mode 100644 index 0000000000..a47eb908f5 --- /dev/null +++ b/prowler/providers/huaweicloud/services/functiongraph/functiongraph_client.py @@ -0,0 +1,6 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.huaweicloud.services.functiongraph.functiongraph_service import ( + FunctionGraph, +) + +functiongraph_client = FunctionGraph(Provider.get_global_provider()) diff --git a/prowler/providers/huaweicloud/services/functiongraph/functiongraph_function_vpc_configured/__init__.py b/prowler/providers/huaweicloud/services/functiongraph/functiongraph_function_vpc_configured/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/huaweicloud/services/functiongraph/functiongraph_function_vpc_configured/functiongraph_function_vpc_configured.metadata.json b/prowler/providers/huaweicloud/services/functiongraph/functiongraph_function_vpc_configured/functiongraph_function_vpc_configured.metadata.json new file mode 100644 index 0000000000..9bf10d290a --- /dev/null +++ b/prowler/providers/huaweicloud/services/functiongraph/functiongraph_function_vpc_configured/functiongraph_function_vpc_configured.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "huaweicloud", + "CheckID": "functiongraph_function_vpc_configured", + "CheckTitle": "FunctionGraph functions are configured within a VPC", + "CheckType": [], + "ServiceName": "functiongraph", + "SubServiceName": "", + "ResourceIdTemplate": "function/{function_id}", + "Severity": "medium", + "ResourceType": "FunctionGraphFunction", + "ResourceGroup": "serverless", + "Description": "Ensure FunctionGraph functions are associated with a VPC to restrict network access", + "Risk": "Functions not associated with a VPC have direct internet access without network restrictions, increasing the attack surface", + "RelatedUrl": "", + "AdditionalURLs": [], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Associate the FunctionGraph function with a VPC to restrict network access", + "Url": "https://hub.prowler.com/check/functiongraph_function_vpc_configured" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/huaweicloud/services/functiongraph/functiongraph_function_vpc_configured/functiongraph_function_vpc_configured.py b/prowler/providers/huaweicloud/services/functiongraph/functiongraph_function_vpc_configured/functiongraph_function_vpc_configured.py new file mode 100644 index 0000000000..5e92d85bd8 --- /dev/null +++ b/prowler/providers/huaweicloud/services/functiongraph/functiongraph_function_vpc_configured/functiongraph_function_vpc_configured.py @@ -0,0 +1,30 @@ +from prowler.lib.check.models import Check, CheckReportHuaweiCloud +from prowler.providers.huaweicloud.services.functiongraph.functiongraph_client import ( + functiongraph_client, +) + + +class functiongraph_function_vpc_configured(Check): + """Check if FunctionGraph functions are configured within a VPC.""" + + def execute(self) -> list[CheckReportHuaweiCloud]: + findings = [] + for function in functiongraph_client.functions: + report = CheckReportHuaweiCloud( + 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: + report.status = "PASS" + report.status_extended = f"Function '{function.name}' is configured within VPC '{function.func_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." + + findings.append(report) + + return findings diff --git a/prowler/providers/huaweicloud/services/functiongraph/functiongraph_service.py b/prowler/providers/huaweicloud/services/functiongraph/functiongraph_service.py new file mode 100644 index 0000000000..b9802cbd35 --- /dev/null +++ b/prowler/providers/huaweicloud/services/functiongraph/functiongraph_service.py @@ -0,0 +1,95 @@ +from typing import List, Optional + +from pydantic.v1 import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.huaweicloud.lib.service.service import HuaweiCloudService + + +class FunctionGraph(HuaweiCloudService): + """ + FunctionGraph service class for Huawei Cloud. + + This class provides methods to interact with Huawei Cloud FunctionGraph service + to retrieve serverless functions and their security configuration. + """ + + def __init__(self, provider): + super().__init__(__class__.__name__, provider) + + self.functions: List[FunctionGraphFunction] = [] + + if self.session.is_mock: + self._load_mock_data() + return + + 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", + name="function-secure", + runtime="Python3.9", + timeout=30, + memory_size=128, + func_vpc_id="vpc-12345", + region=region, + ), + FunctionGraphFunction( + function_id="fg-mock-002", + name="function-insecure", + runtime="Python3.9", + timeout=30, + memory_size=128, + func_vpc_id=None, + region=region, + ), + ] + + def _list_functions(self): + """List all FunctionGraph functions across regions.""" + if not self.regional_clients: + return + + for region, client in self.regional_clients.items(): + logger.info(f"FunctionGraph - Listing Functions in {region}...") + + 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}" + ) + + +class FunctionGraphFunction(BaseModel): + """FunctionGraph function model.""" + + function_id: str + name: str = "" + runtime: str = "" + timeout: int = 0 + memory_size: int = 0 + func_vpc_id: Optional[str] = None + region: str = "" diff --git a/tests/providers/huaweicloud/services/functiongraph/functiongraph_function_vpc_configured/functiongraph_function_vpc_configured_test.py b/tests/providers/huaweicloud/services/functiongraph/functiongraph_function_vpc_configured/functiongraph_function_vpc_configured_test.py new file mode 100644 index 0000000000..88b600a2b9 --- /dev/null +++ b/tests/providers/huaweicloud/services/functiongraph/functiongraph_function_vpc_configured/functiongraph_function_vpc_configured_test.py @@ -0,0 +1,163 @@ +from unittest import mock + +from tests.providers.huaweicloud.huaweicloud_fixtures import ( + set_mocked_huaweicloud_provider, +) + + +class Test_functiongraph_function_vpc_configured: + def test_functiongraph_vpc_configured_pass(self): + functiongraph_client = mock.MagicMock() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_huaweicloud_provider(), + ), + mock.patch( + "prowler.providers.huaweicloud.services.functiongraph.functiongraph_function_vpc_configured.functiongraph_function_vpc_configured.functiongraph_client", + new=functiongraph_client, + ), + ): + from prowler.providers.huaweicloud.services.functiongraph.functiongraph_function_vpc_configured.functiongraph_function_vpc_configured import ( + functiongraph_function_vpc_configured, + ) + from prowler.providers.huaweicloud.services.functiongraph.functiongraph_service import ( + FunctionGraphFunction, + ) + + functiongraph_client.functions = [ + FunctionGraphFunction( + function_id="fg-001", + name="function-secure", + runtime="Python3.9", + timeout=30, + memory_size=128, + func_vpc_id="vpc-12345", + region="la-south-2", + ), + ] + functiongraph_client.audited_account = "123456789012" + + check = functiongraph_function_vpc_configured() + results = check.execute() + + assert len(results) == 1 + assert results[0].status == "PASS" + assert results[0].resource_id == "fg-001" + assert "configured within VPC" in results[0].status_extended + + def test_functiongraph_vpc_configured_fail(self): + functiongraph_client = mock.MagicMock() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_huaweicloud_provider(), + ), + mock.patch( + "prowler.providers.huaweicloud.services.functiongraph.functiongraph_function_vpc_configured.functiongraph_function_vpc_configured.functiongraph_client", + new=functiongraph_client, + ), + ): + from prowler.providers.huaweicloud.services.functiongraph.functiongraph_function_vpc_configured.functiongraph_function_vpc_configured import ( + functiongraph_function_vpc_configured, + ) + from prowler.providers.huaweicloud.services.functiongraph.functiongraph_service import ( + FunctionGraphFunction, + ) + + functiongraph_client.functions = [ + FunctionGraphFunction( + function_id="fg-002", + name="function-insecure", + runtime="Python3.9", + timeout=30, + memory_size=128, + func_vpc_id=None, + region="la-south-2", + ), + ] + functiongraph_client.audited_account = "123456789012" + + check = functiongraph_function_vpc_configured() + results = check.execute() + + 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 + + def test_functiongraph_vpc_configured_mixed(self): + functiongraph_client = mock.MagicMock() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_huaweicloud_provider(), + ), + mock.patch( + "prowler.providers.huaweicloud.services.functiongraph.functiongraph_function_vpc_configured.functiongraph_function_vpc_configured.functiongraph_client", + new=functiongraph_client, + ), + ): + from prowler.providers.huaweicloud.services.functiongraph.functiongraph_function_vpc_configured.functiongraph_function_vpc_configured import ( + functiongraph_function_vpc_configured, + ) + from prowler.providers.huaweicloud.services.functiongraph.functiongraph_service import ( + FunctionGraphFunction, + ) + + functiongraph_client.functions = [ + FunctionGraphFunction( + function_id="fg-001", + name="function-secure", + runtime="Python3.9", + timeout=30, + memory_size=128, + func_vpc_id="vpc-12345", + region="la-south-2", + ), + FunctionGraphFunction( + function_id="fg-002", + name="function-insecure", + runtime="Python3.9", + timeout=30, + memory_size=128, + func_vpc_id=None, + region="la-south-2", + ), + ] + functiongraph_client.audited_account = "123456789012" + + check = functiongraph_function_vpc_configured() + results = check.execute() + + assert len(results) == 2 + assert results[0].status == "PASS" + assert results[1].status == "FAIL" + + def test_functiongraph_vpc_configured_empty(self): + functiongraph_client = mock.MagicMock() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_huaweicloud_provider(), + ), + mock.patch( + "prowler.providers.huaweicloud.services.functiongraph.functiongraph_function_vpc_configured.functiongraph_function_vpc_configured.functiongraph_client", + new=functiongraph_client, + ), + ): + from prowler.providers.huaweicloud.services.functiongraph.functiongraph_function_vpc_configured.functiongraph_function_vpc_configured import ( + functiongraph_function_vpc_configured, + ) + + functiongraph_client.functions = [] + functiongraph_client.audited_account = "123456789012" + + check = functiongraph_function_vpc_configured() + results = check.execute() + + assert len(results) == 0