mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
feat(gcp): add cloudfunction_function_not_publicly_accessible check (#11022)
Co-authored-by: Lydia Vilchez <lydiavilchezlopez@gmail.com>
This commit is contained in:
@@ -12,6 +12,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- `sagemaker_clarify_exists` check for AWS provider [(#11211)](https://github.com/prowler-cloud/prowler/pull/11211)
|
||||
- `cloudsql_instance_high_availability_enabled` check for GCP provider, verifying Cloud SQL primary instances use `REGIONAL` availability for automatic zone failover [(#11024)](https://github.com/prowler-cloud/prowler/pull/11024)
|
||||
- `cloudfunction_function_inside_vpc` check for GCP provider, verifying Cloud Functions have a Serverless VPC Access connector for private egress [(#11021)](https://github.com/prowler-cloud/prowler/pull/11021)
|
||||
- `cloudfunction_function_not_publicly_accessible` check for GCP provider, detecting Cloud Functions with `allUsers` or `allAuthenticatedUsers` IAM invocation bindings [(#11022)](https://github.com/prowler-cloud/prowler/pull/11022)
|
||||
- `identity_storage_service_level_admins_scoped` check for OCI provider CIS 3.1 control 1.15, ensuring storage service-level administrators exclude delete permissions [(#11523)](https://github.com/prowler-cloud/prowler/pull/11523)
|
||||
- `cosmosdb_account_automatic_failover_enabled` check for Azure provider [(#11031)](https://github.com/prowler-cloud/prowler/pull/11031)
|
||||
- `cosmosdb_account_backup_policy_continuous` check for Azure provider [(#11032)](https://github.com/prowler-cloud/prowler/pull/11032)
|
||||
|
||||
+5
-1
@@ -25,7 +25,11 @@ class cloudfunction_function_inside_vpc(Check):
|
||||
for function in cloudfunction_client.functions:
|
||||
if function.state != "ACTIVE":
|
||||
continue
|
||||
report = Check_Report_GCP(metadata=self.metadata(), resource=function)
|
||||
report = Check_Report_GCP(
|
||||
metadata=self.metadata(),
|
||||
resource=function,
|
||||
resource_id=function.name,
|
||||
)
|
||||
if function.vpc_connector:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"Provider": "gcp",
|
||||
"CheckID": "cloudfunction_function_not_publicly_accessible",
|
||||
"CheckTitle": "Cloud Function is not publicly invocable",
|
||||
"CheckType": [],
|
||||
"ServiceName": "cloudfunction",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "high",
|
||||
"ResourceType": "cloudfunctions.googleapis.com/Function",
|
||||
"Description": "Cloud Functions deny invocation to `allUsers` and `allAuthenticatedUsers`, so only **explicitly authorized identities or services** can trigger them.\n\nThe evaluation reviews each function's IAM policy bindings to confirm no public principals are granted invoker access.",
|
||||
"Risk": "Publicly invocable Cloud Functions expose **business logic** to the internet and let any caller trigger execution. This enables **unauthorized data access** when the function returns sensitive output, **code execution** in shared environments, and **denial-of-wallet** attacks driven by uncontrolled invocation costs.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://cloud.google.com/functions/docs/securing/authenticating",
|
||||
"https://cloud.google.com/iam/docs/overview"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "gcloud functions remove-iam-policy-binding <FUNCTION_NAME> --region=<REGION> --member=<allUsers|allAuthenticatedUsers> --role=roles/cloudfunctions.invoker",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. In Google Cloud Console, go to Cloud Functions\n2. Select the function and open the Permissions tab\n3. Remove any binding with allUsers or allAuthenticatedUsers\n4. Grant invocation rights only to specific service accounts or user groups",
|
||||
"Terraform": "```hcl\nresource \"google_cloudfunctions2_function_iam_binding\" \"<example_resource_name>\" {\n project = \"<example_resource_id>\"\n location = \"<example_resource_id>\"\n cloud_function = \"<example_resource_name>\"\n role = \"roles/cloudfunctions.invoker\"\n members = [\"serviceAccount:<example_resource_id>\"] # Critical: never include allUsers or allAuthenticatedUsers\n}\n```"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Apply **least privilege** to Cloud Function invocation: grant `roles/cloudfunctions.invoker` only to specific service accounts or groups.\n\nFor externally exposed functions, front them with **API Gateway** or **Cloud Endpoints** that enforce authentication and rate limiting.",
|
||||
"Url": "https://hub.prowler.com/check/cloudfunction_function_not_publicly_accessible"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"internet-exposed"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [
|
||||
"cloudfunction_function_inside_vpc",
|
||||
"secretmanager_secret_not_publicly_accessible",
|
||||
"cloudstorage_bucket_public_access"
|
||||
],
|
||||
"Notes": "This check evaluates function-level IAM policies. Organization policy constraints/iam.allowedPolicyMemberDomains can prevent public bindings at the org level."
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_GCP
|
||||
from prowler.providers.gcp.services.cloudfunction.cloudfunction_client import (
|
||||
cloudfunction_client,
|
||||
)
|
||||
|
||||
|
||||
class cloudfunction_function_not_publicly_accessible(Check):
|
||||
"""Check that Cloud Functions do not grant invocation rights to all users.
|
||||
|
||||
Verifies that no active Cloud Function has an IAM binding granting access
|
||||
to `allUsers` or `allAuthenticatedUsers`. Non-`ACTIVE` functions are
|
||||
skipped because their IAM bindings are transient.
|
||||
"""
|
||||
|
||||
def execute(self) -> list[Check_Report_GCP]:
|
||||
"""Execute the public-access check across all Cloud Functions.
|
||||
|
||||
Returns:
|
||||
A list of `Check_Report_GCP` findings, one per active Cloud
|
||||
Function. Status is `FAIL` when the function is invokable by
|
||||
`allUsers` or `allAuthenticatedUsers` and `PASS` otherwise.
|
||||
"""
|
||||
findings = []
|
||||
for function in cloudfunction_client.functions:
|
||||
if function.state != "ACTIVE":
|
||||
continue
|
||||
report = Check_Report_GCP(
|
||||
metadata=self.metadata(),
|
||||
resource=function,
|
||||
resource_id=function.name,
|
||||
)
|
||||
if function.publicly_accessible:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Cloud Function {function.name} is publicly invocable "
|
||||
f"(allUsers or allAuthenticatedUsers IAM binding detected)."
|
||||
)
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Cloud Function {function.name} is not publicly accessible."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Optional
|
||||
|
||||
from googleapiclient import discovery
|
||||
from pydantic.v1 import BaseModel
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
@@ -20,7 +21,9 @@ class CloudFunction(GCPService):
|
||||
"""Initialize the service and preload Cloud Functions."""
|
||||
super().__init__("cloudfunctions", provider, api_version="v2")
|
||||
self.functions = []
|
||||
self._run_client = None
|
||||
self._get_functions()
|
||||
self._get_functions_iam_policy()
|
||||
|
||||
def _get_functions(self) -> None:
|
||||
"""Fetch Cloud Functions for every project and location."""
|
||||
@@ -47,10 +50,13 @@ class CloudFunction(GCPService):
|
||||
service_config = fn.get("serviceConfig", {})
|
||||
self.functions.append(
|
||||
Function(
|
||||
id=fn["name"],
|
||||
name=fn["name"].split("/")[-1],
|
||||
project_id=project_id,
|
||||
location=location_id,
|
||||
state=fn.get("state", "UNKNOWN"),
|
||||
environment=fn.get("environment", "GEN_1"),
|
||||
service=service_config.get("service"),
|
||||
vpc_connector=service_config.get(
|
||||
"vpcConnector"
|
||||
),
|
||||
@@ -73,12 +79,68 @@ class CloudFunction(GCPService):
|
||||
f"{project_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_functions_iam_policy(self) -> None:
|
||||
"""Fetch IAM policy for every Cloud Function in parallel.
|
||||
|
||||
For gen2 functions, IAM is delegated to the underlying Cloud Run
|
||||
service, so a `run.googleapis.com` v2 client is required.
|
||||
"""
|
||||
if any(f.environment == "GEN_2" for f in self.functions):
|
||||
self._run_client = discovery.build(
|
||||
"run",
|
||||
"v2",
|
||||
credentials=self.credentials,
|
||||
num_retries=DEFAULT_RETRY_ATTEMPTS,
|
||||
)
|
||||
self.__threading_call__(self._get_function_iam_policy, self.functions)
|
||||
|
||||
def _get_function_iam_policy(self, function: "Function") -> None:
|
||||
"""Mark a Cloud Function as publicly accessible when bound to `allUsers` or `allAuthenticatedUsers`.
|
||||
|
||||
Cloud Functions gen2 delegates invocation IAM to its backing Cloud Run
|
||||
service, so the binding is queried via the Run API. Gen1 functions are
|
||||
queried through the Cloud Functions API directly.
|
||||
"""
|
||||
try:
|
||||
if function.environment == "GEN_2" and function.service:
|
||||
response = (
|
||||
self._run_client.projects()
|
||||
.locations()
|
||||
.services()
|
||||
.getIamPolicy(resource=function.service)
|
||||
.execute(num_retries=DEFAULT_RETRY_ATTEMPTS)
|
||||
)
|
||||
else:
|
||||
response = (
|
||||
self.client.projects()
|
||||
.locations()
|
||||
.functions()
|
||||
.getIamPolicy(resource=function.id)
|
||||
.execute(
|
||||
http=self.__get_AuthorizedHttp_client__(),
|
||||
num_retries=DEFAULT_RETRY_ATTEMPTS,
|
||||
)
|
||||
)
|
||||
for binding in response.get("bindings", []):
|
||||
members = binding.get("members", [])
|
||||
if "allUsers" in members or "allAuthenticatedUsers" in members:
|
||||
function.publicly_accessible = True
|
||||
break
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{function.location} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
class Function(BaseModel):
|
||||
"""Cloud Function resource consumed by GCP checks."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
project_id: str
|
||||
location: str
|
||||
state: str
|
||||
environment: str = "GEN_1"
|
||||
service: Optional[str] = None
|
||||
vpc_connector: Optional[str] = None
|
||||
publicly_accessible: bool = False
|
||||
|
||||
+11
@@ -13,6 +13,13 @@ _CHECK_PATH = (
|
||||
_CLIENT_PATH = f"{_CHECK_PATH}.cloudfunction_client"
|
||||
|
||||
|
||||
def _function_id(name: str) -> str:
|
||||
return (
|
||||
f"projects/{GCP_PROJECT_ID}/locations/{GCP_US_CENTER1_LOCATION}"
|
||||
f"/functions/{name}"
|
||||
)
|
||||
|
||||
|
||||
class Test_cloudfunction_function_inside_vpc:
|
||||
def test_no_functions(self):
|
||||
cloudfunction_client = mock.MagicMock()
|
||||
@@ -63,6 +70,7 @@ class Test_cloudfunction_function_inside_vpc:
|
||||
)
|
||||
cloudfunction_client.functions = [
|
||||
Function(
|
||||
id=_function_id("fn-vpc"),
|
||||
name="fn-vpc",
|
||||
project_id=GCP_PROJECT_ID,
|
||||
location=GCP_US_CENTER1_LOCATION,
|
||||
@@ -106,6 +114,7 @@ class Test_cloudfunction_function_inside_vpc:
|
||||
|
||||
cloudfunction_client.functions = [
|
||||
Function(
|
||||
id=_function_id("fn-public"),
|
||||
name="fn-public",
|
||||
project_id=GCP_PROJECT_ID,
|
||||
location=GCP_US_CENTER1_LOCATION,
|
||||
@@ -149,6 +158,7 @@ class Test_cloudfunction_function_inside_vpc:
|
||||
|
||||
cloudfunction_client.functions = [
|
||||
Function(
|
||||
id=_function_id("fn-empty"),
|
||||
name="fn-empty",
|
||||
project_id=GCP_PROJECT_ID,
|
||||
location=GCP_US_CENTER1_LOCATION,
|
||||
@@ -184,6 +194,7 @@ class Test_cloudfunction_function_inside_vpc:
|
||||
|
||||
cloudfunction_client.functions = [
|
||||
Function(
|
||||
id=_function_id("fn-deploy"),
|
||||
name="fn-deploy",
|
||||
project_id=GCP_PROJECT_ID,
|
||||
location=GCP_US_CENTER1_LOCATION,
|
||||
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
from unittest import mock
|
||||
|
||||
from tests.providers.gcp.gcp_fixtures import (
|
||||
GCP_PROJECT_ID,
|
||||
GCP_US_CENTER1_LOCATION,
|
||||
set_mocked_gcp_provider,
|
||||
)
|
||||
|
||||
_CHECK_PATH = (
|
||||
"prowler.providers.gcp.services.cloudfunction."
|
||||
"cloudfunction_function_not_publicly_accessible."
|
||||
"cloudfunction_function_not_publicly_accessible"
|
||||
)
|
||||
_CLIENT_PATH = f"{_CHECK_PATH}.cloudfunction_client"
|
||||
|
||||
|
||||
def _function_id(name: str) -> str:
|
||||
return (
|
||||
f"projects/{GCP_PROJECT_ID}/locations/{GCP_US_CENTER1_LOCATION}"
|
||||
f"/functions/{name}"
|
||||
)
|
||||
|
||||
|
||||
class Test_cloudfunction_function_not_publicly_accessible:
|
||||
def test_no_functions(self):
|
||||
cloudfunction_client = mock.MagicMock()
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
_CLIENT_PATH,
|
||||
new=cloudfunction_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.cloudfunction.cloudfunction_function_not_publicly_accessible.cloudfunction_function_not_publicly_accessible import (
|
||||
cloudfunction_function_not_publicly_accessible,
|
||||
)
|
||||
|
||||
cloudfunction_client.functions = []
|
||||
|
||||
check = cloudfunction_function_not_publicly_accessible()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_function_private(self):
|
||||
cloudfunction_client = mock.MagicMock()
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
_CLIENT_PATH,
|
||||
new=cloudfunction_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.cloudfunction.cloudfunction_function_not_publicly_accessible.cloudfunction_function_not_publicly_accessible import (
|
||||
cloudfunction_function_not_publicly_accessible,
|
||||
)
|
||||
from prowler.providers.gcp.services.cloudfunction.cloudfunction_service import (
|
||||
Function,
|
||||
)
|
||||
|
||||
cloudfunction_client.functions = [
|
||||
Function(
|
||||
id=_function_id("fn-private"),
|
||||
name="fn-private",
|
||||
project_id=GCP_PROJECT_ID,
|
||||
location=GCP_US_CENTER1_LOCATION,
|
||||
state="ACTIVE",
|
||||
publicly_accessible=False,
|
||||
)
|
||||
]
|
||||
|
||||
check = cloudfunction_function_not_publicly_accessible()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Cloud Function fn-private is not publicly accessible."
|
||||
)
|
||||
assert result[0].resource_id == "fn-private"
|
||||
assert result[0].resource_name == "fn-private"
|
||||
assert result[0].location == GCP_US_CENTER1_LOCATION
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
|
||||
def test_function_publicly_accessible(self):
|
||||
cloudfunction_client = mock.MagicMock()
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
_CLIENT_PATH,
|
||||
new=cloudfunction_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.cloudfunction.cloudfunction_function_not_publicly_accessible.cloudfunction_function_not_publicly_accessible import (
|
||||
cloudfunction_function_not_publicly_accessible,
|
||||
)
|
||||
from prowler.providers.gcp.services.cloudfunction.cloudfunction_service import (
|
||||
Function,
|
||||
)
|
||||
|
||||
cloudfunction_client.functions = [
|
||||
Function(
|
||||
id=_function_id("fn-public"),
|
||||
name="fn-public",
|
||||
project_id=GCP_PROJECT_ID,
|
||||
location=GCP_US_CENTER1_LOCATION,
|
||||
state="ACTIVE",
|
||||
publicly_accessible=True,
|
||||
)
|
||||
]
|
||||
|
||||
check = cloudfunction_function_not_publicly_accessible()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].status_extended == (
|
||||
"Cloud Function fn-public is publicly invocable "
|
||||
"(allUsers or allAuthenticatedUsers IAM binding detected)."
|
||||
)
|
||||
assert result[0].resource_id == "fn-public"
|
||||
assert result[0].resource_name == "fn-public"
|
||||
assert result[0].location == GCP_US_CENTER1_LOCATION
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
|
||||
def test_multiple_functions_mixed(self):
|
||||
cloudfunction_client = mock.MagicMock()
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
_CLIENT_PATH,
|
||||
new=cloudfunction_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.cloudfunction.cloudfunction_function_not_publicly_accessible.cloudfunction_function_not_publicly_accessible import (
|
||||
cloudfunction_function_not_publicly_accessible,
|
||||
)
|
||||
from prowler.providers.gcp.services.cloudfunction.cloudfunction_service import (
|
||||
Function,
|
||||
)
|
||||
|
||||
cloudfunction_client.functions = [
|
||||
Function(
|
||||
id=_function_id("fn-private"),
|
||||
name="fn-private",
|
||||
project_id=GCP_PROJECT_ID,
|
||||
location=GCP_US_CENTER1_LOCATION,
|
||||
state="ACTIVE",
|
||||
publicly_accessible=False,
|
||||
),
|
||||
Function(
|
||||
id=_function_id("fn-public"),
|
||||
name="fn-public",
|
||||
project_id=GCP_PROJECT_ID,
|
||||
location=GCP_US_CENTER1_LOCATION,
|
||||
state="ACTIVE",
|
||||
publicly_accessible=True,
|
||||
),
|
||||
]
|
||||
|
||||
check = cloudfunction_function_not_publicly_accessible()
|
||||
result = check.execute()
|
||||
assert len(result) == 2
|
||||
|
||||
by_id = {r.resource_id: r for r in result}
|
||||
assert by_id["fn-private"].status == "PASS"
|
||||
assert by_id["fn-public"].status == "FAIL"
|
||||
|
||||
def test_inactive_function_skipped(self):
|
||||
cloudfunction_client = mock.MagicMock()
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
_CLIENT_PATH,
|
||||
new=cloudfunction_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.cloudfunction.cloudfunction_function_not_publicly_accessible.cloudfunction_function_not_publicly_accessible import (
|
||||
cloudfunction_function_not_publicly_accessible,
|
||||
)
|
||||
from prowler.providers.gcp.services.cloudfunction.cloudfunction_service import (
|
||||
Function,
|
||||
)
|
||||
|
||||
cloudfunction_client.functions = [
|
||||
Function(
|
||||
id=_function_id("fn-deleting"),
|
||||
name="fn-deleting",
|
||||
project_id=GCP_PROJECT_ID,
|
||||
location=GCP_US_CENTER1_LOCATION,
|
||||
state="DELETING",
|
||||
publicly_accessible=True,
|
||||
)
|
||||
]
|
||||
|
||||
check = cloudfunction_function_not_publicly_accessible()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
@@ -11,12 +11,18 @@ from tests.providers.gcp.gcp_fixtures import (
|
||||
|
||||
_LOCATION_ID = "us-central1"
|
||||
_FUNCTION_NAME = "my-function"
|
||||
_FUNCTION_ID = (
|
||||
f"projects/{GCP_PROJECT_ID}/locations/{_LOCATION_ID}/functions/{_FUNCTION_NAME}"
|
||||
)
|
||||
_RUN_SERVICE = (
|
||||
f"projects/{GCP_PROJECT_ID}/locations/{_LOCATION_ID}/services/{_FUNCTION_NAME}"
|
||||
)
|
||||
_CONNECTOR = (
|
||||
f"projects/{GCP_PROJECT_ID}/locations/{_LOCATION_ID}/connectors/my-connector"
|
||||
)
|
||||
|
||||
|
||||
def _make_cloudfunction_client(functions_list):
|
||||
def _make_cloudfunction_client(functions_list, iam_bindings=None):
|
||||
"""Return a mock GCP API client for the Cloud Functions v2 service."""
|
||||
client = MagicMock()
|
||||
|
||||
@@ -30,6 +36,29 @@ def _make_cloudfunction_client(functions_list):
|
||||
}
|
||||
client.projects().locations().functions().list_next.return_value = None
|
||||
|
||||
iam_response = {"bindings": iam_bindings or []}
|
||||
|
||||
def mock_get_iam_policy(resource):
|
||||
rv = MagicMock()
|
||||
rv.execute.return_value = iam_response
|
||||
return rv
|
||||
|
||||
client.projects().locations().functions().getIamPolicy = mock_get_iam_policy
|
||||
|
||||
return client
|
||||
|
||||
|
||||
def _make_run_client(iam_bindings=None):
|
||||
"""Return a mock Cloud Run v2 client for gen2 IAM policy lookups."""
|
||||
client = MagicMock()
|
||||
iam_response = {"bindings": iam_bindings or []}
|
||||
|
||||
def mock_get_iam_policy(resource):
|
||||
rv = MagicMock()
|
||||
rv.execute.return_value = iam_response
|
||||
return rv
|
||||
|
||||
client.projects().locations().services().getIamPolicy = mock_get_iam_policy
|
||||
return client
|
||||
|
||||
|
||||
@@ -39,9 +68,11 @@ class TestCloudFunctionService:
|
||||
return _make_cloudfunction_client(
|
||||
functions_list=[
|
||||
{
|
||||
"name": f"projects/{GCP_PROJECT_ID}/locations/{_LOCATION_ID}/functions/{_FUNCTION_NAME}",
|
||||
"name": _FUNCTION_ID,
|
||||
"state": "ACTIVE",
|
||||
"environment": "GEN_2",
|
||||
"serviceConfig": {
|
||||
"service": _RUN_SERVICE,
|
||||
"vpcConnector": _CONNECTOR,
|
||||
},
|
||||
}
|
||||
@@ -57,6 +88,10 @@ class TestCloudFunctionService:
|
||||
"prowler.providers.gcp.lib.service.service.GCPService.__generate_client__",
|
||||
new=mock_api_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.gcp.services.cloudfunction.cloudfunction_service.discovery.build",
|
||||
return_value=_make_run_client(),
|
||||
),
|
||||
):
|
||||
cf_client = CloudFunction(
|
||||
set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID])
|
||||
@@ -64,11 +99,15 @@ class TestCloudFunctionService:
|
||||
|
||||
assert len(cf_client.functions) == 1
|
||||
fn = cf_client.functions[0]
|
||||
assert fn.id == _FUNCTION_ID
|
||||
assert fn.name == _FUNCTION_NAME
|
||||
assert fn.project_id == GCP_PROJECT_ID
|
||||
assert fn.location == _LOCATION_ID
|
||||
assert fn.state == "ACTIVE"
|
||||
assert fn.environment == "GEN_2"
|
||||
assert fn.service == _RUN_SERVICE
|
||||
assert fn.vpc_connector == _CONNECTOR
|
||||
assert fn.publicly_accessible is False
|
||||
|
||||
def test_get_functions_without_vpc_connector(self):
|
||||
def mock_api_client(*args, **kwargs):
|
||||
@@ -77,11 +116,190 @@ class TestCloudFunctionService:
|
||||
{
|
||||
"name": f"projects/{GCP_PROJECT_ID}/locations/{_LOCATION_ID}/functions/no-vpc-func",
|
||||
"state": "ACTIVE",
|
||||
"serviceConfig": {},
|
||||
"environment": "GEN_2",
|
||||
"serviceConfig": {
|
||||
"service": f"projects/{GCP_PROJECT_ID}/locations/{_LOCATION_ID}/services/no-vpc-func",
|
||||
},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.gcp.lib.service.service.GCPService.__is_api_active__",
|
||||
new=mock_is_api_active,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.gcp.lib.service.service.GCPService.__generate_client__",
|
||||
new=mock_api_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.gcp.services.cloudfunction.cloudfunction_service.discovery.build",
|
||||
return_value=_make_run_client(),
|
||||
),
|
||||
):
|
||||
cf_client = CloudFunction(
|
||||
set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID])
|
||||
)
|
||||
|
||||
assert len(cf_client.functions) == 1
|
||||
fn = cf_client.functions[0]
|
||||
assert fn.name == "no-vpc-func"
|
||||
assert fn.vpc_connector is None
|
||||
assert fn.publicly_accessible is False
|
||||
|
||||
def test_get_functions_iam_policy_gen2_all_users(self):
|
||||
"""Gen2 functions: allUsers binding lives on the Cloud Run service."""
|
||||
|
||||
def mock_api_client(*args, **kwargs):
|
||||
return _make_cloudfunction_client(
|
||||
functions_list=[
|
||||
{
|
||||
"name": _FUNCTION_ID,
|
||||
"state": "ACTIVE",
|
||||
"environment": "GEN_2",
|
||||
"serviceConfig": {"service": _RUN_SERVICE},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
run_client = _make_run_client(
|
||||
iam_bindings=[
|
||||
{
|
||||
"role": "roles/run.invoker",
|
||||
"members": ["allUsers"],
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.gcp.lib.service.service.GCPService.__is_api_active__",
|
||||
new=mock_is_api_active,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.gcp.lib.service.service.GCPService.__generate_client__",
|
||||
new=mock_api_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.gcp.services.cloudfunction.cloudfunction_service.discovery.build",
|
||||
return_value=run_client,
|
||||
),
|
||||
):
|
||||
cf_client = CloudFunction(
|
||||
set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID])
|
||||
)
|
||||
|
||||
assert len(cf_client.functions) == 1
|
||||
assert cf_client.functions[0].publicly_accessible is True
|
||||
|
||||
def test_get_functions_iam_policy_gen2_all_authenticated_users(self):
|
||||
def mock_api_client(*args, **kwargs):
|
||||
return _make_cloudfunction_client(
|
||||
functions_list=[
|
||||
{
|
||||
"name": _FUNCTION_ID,
|
||||
"state": "ACTIVE",
|
||||
"environment": "GEN_2",
|
||||
"serviceConfig": {"service": _RUN_SERVICE},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
run_client = _make_run_client(
|
||||
iam_bindings=[
|
||||
{
|
||||
"role": "roles/run.invoker",
|
||||
"members": ["allAuthenticatedUsers"],
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.gcp.lib.service.service.GCPService.__is_api_active__",
|
||||
new=mock_is_api_active,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.gcp.lib.service.service.GCPService.__generate_client__",
|
||||
new=mock_api_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.gcp.services.cloudfunction.cloudfunction_service.discovery.build",
|
||||
return_value=run_client,
|
||||
),
|
||||
):
|
||||
cf_client = CloudFunction(
|
||||
set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID])
|
||||
)
|
||||
|
||||
assert len(cf_client.functions) == 1
|
||||
assert cf_client.functions[0].publicly_accessible is True
|
||||
|
||||
def test_get_functions_iam_policy_gen2_not_public(self):
|
||||
def mock_api_client(*args, **kwargs):
|
||||
return _make_cloudfunction_client(
|
||||
functions_list=[
|
||||
{
|
||||
"name": _FUNCTION_ID,
|
||||
"state": "ACTIVE",
|
||||
"environment": "GEN_2",
|
||||
"serviceConfig": {"service": _RUN_SERVICE},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
run_client = _make_run_client(
|
||||
iam_bindings=[
|
||||
{
|
||||
"role": "roles/run.invoker",
|
||||
"members": ["serviceAccount:sa@project.iam.gserviceaccount.com"],
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.gcp.lib.service.service.GCPService.__is_api_active__",
|
||||
new=mock_is_api_active,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.gcp.lib.service.service.GCPService.__generate_client__",
|
||||
new=mock_api_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.gcp.services.cloudfunction.cloudfunction_service.discovery.build",
|
||||
return_value=run_client,
|
||||
),
|
||||
):
|
||||
cf_client = CloudFunction(
|
||||
set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID])
|
||||
)
|
||||
|
||||
assert len(cf_client.functions) == 1
|
||||
assert cf_client.functions[0].publicly_accessible is False
|
||||
|
||||
def test_get_functions_iam_policy_gen1_all_users(self):
|
||||
"""Gen1 functions: IAM binding lives on the Cloud Functions resource itself."""
|
||||
|
||||
def mock_api_client(*args, **kwargs):
|
||||
return _make_cloudfunction_client(
|
||||
functions_list=[
|
||||
{
|
||||
"name": _FUNCTION_ID,
|
||||
"state": "ACTIVE",
|
||||
"environment": "GEN_1",
|
||||
"serviceConfig": {},
|
||||
}
|
||||
],
|
||||
iam_bindings=[
|
||||
{
|
||||
"role": "roles/cloudfunctions.invoker",
|
||||
"members": ["allUsers"],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.gcp.lib.service.service.GCPService.__is_api_active__",
|
||||
@@ -97,6 +315,5 @@ class TestCloudFunctionService:
|
||||
)
|
||||
|
||||
assert len(cf_client.functions) == 1
|
||||
fn = cf_client.functions[0]
|
||||
assert fn.name == "no-vpc-func"
|
||||
assert fn.vpc_connector is None
|
||||
assert cf_client.functions[0].environment == "GEN_1"
|
||||
assert cf_client.functions[0].publicly_accessible is True
|
||||
|
||||
Reference in New Issue
Block a user