From 1c9afc714e3580fb0d847f0fbb6384290cf8acb5 Mon Sep 17 00:00:00 2001 From: Aline Almeida Date: Mon, 8 Jun 2026 16:46:48 +0200 Subject: [PATCH] fix(gcp): honour org-aggregated sinks in metric-filter checks (#11488) Co-authored-by: Hugo P.Brito --- prowler/CHANGELOG.md | 1 + ...for_audit_configuration_changes_enabled.py | 22 ++- ...t_for_bucket_permission_changes_enabled.py | 20 +- ...r_compute_configuration_changes_enabled.py | 17 +- ...d_alert_for_custom_role_changes_enabled.py | 20 +- ...t_for_project_ownership_changes_enabled.py | 20 +- ..._instance_configuration_changes_enabled.py | 17 +- ...t_for_vpc_firewall_rule_changes_enabled.py | 20 +- ...d_alert_for_vpc_network_changes_enabled.py | 20 +- ...t_for_vpc_network_route_changes_enabled.py | 20 +- .../gcp/services/logging/logging_service.py | 57 ++++++ ...udit_configuration_changes_enabled_test.py | 173 ++++++++++++++++++ ..._bucket_permission_changes_enabled_test.py | 170 +++++++++++++++++ ...pute_configuration_changes_enabled_test.py | 170 +++++++++++++++++ ...rt_for_custom_role_changes_enabled_test.py | 170 +++++++++++++++++ ..._project_ownership_changes_enabled_test.py | 170 +++++++++++++++++ ...ance_configuration_changes_enabled_test.py | 170 +++++++++++++++++ ..._vpc_firewall_rule_changes_enabled_test.py | 170 +++++++++++++++++ ...rt_for_vpc_network_changes_enabled_test.py | 170 +++++++++++++++++ ..._vpc_network_route_changes_enabled_test.py | 170 +++++++++++++++++ .../services/logging/logging_service_test.py | 163 +++++++++++++++++ 21 files changed, 1882 insertions(+), 48 deletions(-) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 45cf485716..1389b684cf 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -18,6 +18,7 @@ All notable changes to the **Prowler SDK** are documented in this file. ### 🐞 Fixed - GCP `logging_sink_created` now recognizes organization-level aggregated sinks with `includeChildren=True`, avoiding false failures for covered projects [(#11355)](https://github.com/prowler-cloud/prowler/pull/11355) +- GCP `logging_log_metric_filter_and_alert_*` checks now recognize organization-level aggregated sinks with `includeChildren=True`, no longer false-failing projects covered by a central bucket-scoped metric + alert [(#11488)](https://github.com/prowler-cloud/prowler/pull/11488) - Jira integration no longer fails with `400 INVALID_INPUT` when a finding has empty fields [(#11474)](https://github.com/prowler-cloud/prowler/pull/11474) - GCP `iam_service_account_unused` now passes disabled service accounts instead of failing them, since a disabled account cannot authenticate or be used [(#11467)](https://github.com/prowler-cloud/prowler/pull/11467) - AWS AI Security Framework now renders in the dashboard instead of showing "No data found for this compliance", by adding the missing compliance view module [(#11470)](https://github.com/prowler-cloud/prowler/pull/11470) diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.py b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.py index 4654be4d29..84bf078dac 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.py +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.py @@ -1,5 +1,8 @@ from prowler.lib.check.models import Check, Check_Report_GCP from prowler.providers.gcp.services.logging.logging_client import logging_client +from prowler.providers.gcp.services.logging.logging_service import ( + get_projects_covered_by_aggregated_metric, +) from prowler.providers.gcp.services.monitoring.monitoring_client import ( monitoring_client, ) @@ -10,12 +13,10 @@ class logging_log_metric_filter_and_alert_for_audit_configuration_changes_enable ): def execute(self) -> Check_Report_GCP: findings = [] + metric_filter = 'protoPayload.methodName="SetIamPolicy" AND protoPayload.serviceData.policyDelta.auditConfigDeltas:*' projects_with_metric = set() for metric in logging_client.metrics: - if ( - 'protoPayload.methodName="SetIamPolicy" AND protoPayload.serviceData.policyDelta.auditConfigDeltas:*' - in metric.filter - ): + if metric_filter in metric.filter: report = Check_Report_GCP( metadata=self.metadata(), resource=metric, @@ -33,6 +34,11 @@ class logging_log_metric_filter_and_alert_for_audit_configuration_changes_enable break findings.append(report) + # Credit projects whose logs are centrally monitored via an org-level + # aggregated sink to a bucket-scoped metric + alert (instead of failing them). + centrally_covered = get_projects_covered_by_aggregated_metric( + logging_client, monitoring_client, metric_filter + ) for project in logging_client.project_ids: if project not in projects_with_metric: report = Check_Report_GCP( @@ -46,8 +52,12 @@ class logging_log_metric_filter_and_alert_for_audit_configuration_changes_enable else "GCP Project" ), ) - report.status = "FAIL" - report.status_extended = f"There are no log metric filters or alerts associated in project {project}." + if project in centrally_covered: + report.status = "PASS" + report.status_extended = f"Log metric filter {centrally_covered[project]} found with an alert, covering project {project} via an organization-level aggregated sink." + else: + report.status = "FAIL" + report.status_extended = f"There are no log metric filters or alerts associated in project {project}." findings.append(report) return findings diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.py b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.py index 166f7b7ee8..e7d74f3f8e 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.py +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.py @@ -1,5 +1,8 @@ from prowler.lib.check.models import Check, Check_Report_GCP from prowler.providers.gcp.services.logging.logging_client import logging_client +from prowler.providers.gcp.services.logging.logging_service import ( + get_projects_covered_by_aggregated_metric, +) from prowler.providers.gcp.services.monitoring.monitoring_client import ( monitoring_client, ) @@ -8,12 +11,10 @@ from prowler.providers.gcp.services.monitoring.monitoring_client import ( class logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled(Check): def execute(self) -> Check_Report_GCP: findings = [] + metric_filter = 'resource.type="gcs_bucket" AND protoPayload.methodName="storage.setIamPermissions"' projects_with_metric = set() for metric in logging_client.metrics: - if ( - 'resource.type="gcs_bucket" AND protoPayload.methodName="storage.setIamPermissions"' - in metric.filter - ): + if metric_filter in metric.filter: metric_name = getattr(metric, "name", None) or "unknown" report = Check_Report_GCP( metadata=self.metadata(), @@ -36,6 +37,9 @@ class logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled( break findings.append(report) + centrally_covered = get_projects_covered_by_aggregated_metric( + logging_client, monitoring_client, metric_filter + ) for project in logging_client.project_ids: if project not in projects_with_metric: project_obj = logging_client.projects.get(project) @@ -46,8 +50,12 @@ class logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled( location=logging_client.region, resource_name=(getattr(project_obj, "name", None) or "GCP Project"), ) - report.status = "FAIL" - report.status_extended = f"There are no log metric filters or alerts associated in project {project}." + if project in centrally_covered: + report.status = "PASS" + report.status_extended = f"Log metric filter {centrally_covered[project]} found with an alert, covering project {project} via an organization-level aggregated sink." + else: + report.status = "FAIL" + report.status_extended = f"There are no log metric filters or alerts associated in project {project}." findings.append(report) return findings diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.py b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.py index 7902f9ed72..cf7cdb1679 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.py +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.py @@ -1,5 +1,8 @@ from prowler.lib.check.models import Check, Check_Report_GCP from prowler.providers.gcp.services.logging.logging_client import logging_client +from prowler.providers.gcp.services.logging.logging_service import ( + get_projects_covered_by_aggregated_metric, +) from prowler.providers.gcp.services.monitoring.monitoring_client import ( monitoring_client, ) @@ -10,9 +13,10 @@ class logging_log_metric_filter_and_alert_for_compute_configuration_changes_enab ): def execute(self) -> Check_Report_GCP: findings = [] + metric_filter = 'protoPayload.serviceName="compute.googleapis.com"' projects_with_metric = set() for metric in logging_client.metrics: - if 'protoPayload.serviceName="compute.googleapis.com"' in metric.filter: + if metric_filter in metric.filter: report = Check_Report_GCP( metadata=self.metadata(), resource=metric, @@ -30,6 +34,9 @@ class logging_log_metric_filter_and_alert_for_compute_configuration_changes_enab break findings.append(report) + centrally_covered = get_projects_covered_by_aggregated_metric( + logging_client, monitoring_client, metric_filter + ) for project in logging_client.project_ids: if project not in projects_with_metric: report = Check_Report_GCP( @@ -43,8 +50,12 @@ class logging_log_metric_filter_and_alert_for_compute_configuration_changes_enab else "GCP Project" ), ) - report.status = "FAIL" - report.status_extended = f"There are no log metric filters or alerts associated for Compute Engine configuration changes in project {project}." + if project in centrally_covered: + report.status = "PASS" + report.status_extended = f"Log metric filter {centrally_covered[project]} found with an alert, covering project {project} via an organization-level aggregated sink." + else: + report.status = "FAIL" + report.status_extended = f"There are no log metric filters or alerts associated for Compute Engine configuration changes in project {project}." findings.append(report) return findings diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.py b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.py index 1e6584e5fb..f836dc25b2 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.py +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.py @@ -1,5 +1,8 @@ from prowler.lib.check.models import Check, Check_Report_GCP from prowler.providers.gcp.services.logging.logging_client import logging_client +from prowler.providers.gcp.services.logging.logging_service import ( + get_projects_covered_by_aggregated_metric, +) from prowler.providers.gcp.services.monitoring.monitoring_client import ( monitoring_client, ) @@ -8,12 +11,10 @@ from prowler.providers.gcp.services.monitoring.monitoring_client import ( class logging_log_metric_filter_and_alert_for_custom_role_changes_enabled(Check): def execute(self) -> Check_Report_GCP: findings = [] + metric_filter = 'resource.type="iam_role" AND (protoPayload.methodName="google.iam.admin.v1.CreateRole" OR protoPayload.methodName="google.iam.admin.v1.DeleteRole" OR protoPayload.methodName="google.iam.admin.v1.UpdateRole")' projects_with_metric = set() for metric in logging_client.metrics: - if ( - 'resource.type="iam_role" AND (protoPayload.methodName="google.iam.admin.v1.CreateRole" OR protoPayload.methodName="google.iam.admin.v1.DeleteRole" OR protoPayload.methodName="google.iam.admin.v1.UpdateRole")' - in metric.filter - ): + if metric_filter in metric.filter: report = Check_Report_GCP( metadata=self.metadata(), resource=metric, @@ -31,6 +32,9 @@ class logging_log_metric_filter_and_alert_for_custom_role_changes_enabled(Check) break findings.append(report) + centrally_covered = get_projects_covered_by_aggregated_metric( + logging_client, monitoring_client, metric_filter + ) for project in logging_client.project_ids: if project not in projects_with_metric: report = Check_Report_GCP( @@ -44,8 +48,12 @@ class logging_log_metric_filter_and_alert_for_custom_role_changes_enabled(Check) else "GCP Project" ), ) - report.status = "FAIL" - report.status_extended = f"There are no log metric filters or alerts associated in project {project}." + if project in centrally_covered: + report.status = "PASS" + report.status_extended = f"Log metric filter {centrally_covered[project]} found with an alert, covering project {project} via an organization-level aggregated sink." + else: + report.status = "FAIL" + report.status_extended = f"There are no log metric filters or alerts associated in project {project}." findings.append(report) return findings diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.py b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.py index 8c8927ec32..b7bc619ea4 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.py +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.py @@ -1,5 +1,8 @@ from prowler.lib.check.models import Check, Check_Report_GCP from prowler.providers.gcp.services.logging.logging_client import logging_client +from prowler.providers.gcp.services.logging.logging_service import ( + get_projects_covered_by_aggregated_metric, +) from prowler.providers.gcp.services.monitoring.monitoring_client import ( monitoring_client, ) @@ -8,12 +11,10 @@ from prowler.providers.gcp.services.monitoring.monitoring_client import ( class logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled(Check): def execute(self) -> Check_Report_GCP: findings = [] + metric_filter = '(protoPayload.serviceName="cloudresourcemanager.googleapis.com") AND (ProjectOwnership OR projectOwnerInvitee) OR (protoPayload.serviceData.policyDelta.bindingDeltas.action="REMOVE" AND protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/owner") OR (protoPayload.serviceData.policyDelta.bindingDeltas.action="ADD" AND protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/owner")' projects_with_metric = set() for metric in logging_client.metrics: - if ( - '(protoPayload.serviceName="cloudresourcemanager.googleapis.com") AND (ProjectOwnership OR projectOwnerInvitee) OR (protoPayload.serviceData.policyDelta.bindingDeltas.action="REMOVE" AND protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/owner") OR (protoPayload.serviceData.policyDelta.bindingDeltas.action="ADD" AND protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/owner")' - in metric.filter - ): + if metric_filter in metric.filter: metric_name = getattr(metric, "name", None) or "unknown" report = Check_Report_GCP( metadata=self.metadata(), @@ -36,6 +37,9 @@ class logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled( break findings.append(report) + centrally_covered = get_projects_covered_by_aggregated_metric( + logging_client, monitoring_client, metric_filter + ) for project in logging_client.project_ids: if project not in projects_with_metric: project_obj = logging_client.projects.get(project) @@ -47,8 +51,12 @@ class logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled( location=logging_client.region, resource_name=(getattr(project_obj, "name", None) or "GCP Project"), ) - report.status = "FAIL" - report.status_extended = f"There are no log metric filters or alerts associated in project {project}." + if project in centrally_covered: + report.status = "PASS" + report.status_extended = f"Log metric filter {centrally_covered[project]} found with an alert, covering project {project} via an organization-level aggregated sink." + else: + report.status = "FAIL" + report.status_extended = f"There are no log metric filters or alerts associated in project {project}." findings.append(report) return findings diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.py b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.py index 3e499db10a..3c03ab0fde 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.py +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.py @@ -1,5 +1,8 @@ from prowler.lib.check.models import Check, Check_Report_GCP from prowler.providers.gcp.services.logging.logging_client import logging_client +from prowler.providers.gcp.services.logging.logging_service import ( + get_projects_covered_by_aggregated_metric, +) from prowler.providers.gcp.services.monitoring.monitoring_client import ( monitoring_client, ) @@ -10,9 +13,10 @@ class logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes ): def execute(self) -> Check_Report_GCP: findings = [] + metric_filter = 'protoPayload.methodName="cloudsql.instances.update"' projects_with_metric = set() for metric in logging_client.metrics: - if 'protoPayload.methodName="cloudsql.instances.update"' in metric.filter: + if metric_filter in metric.filter: report = Check_Report_GCP( metadata=self.metadata(), resource=metric, @@ -30,6 +34,9 @@ class logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes break findings.append(report) + centrally_covered = get_projects_covered_by_aggregated_metric( + logging_client, monitoring_client, metric_filter + ) for project in logging_client.project_ids: if project not in projects_with_metric: report = Check_Report_GCP( @@ -43,8 +50,12 @@ class logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes else "GCP Project" ), ) - report.status = "FAIL" - report.status_extended = f"There are no log metric filters or alerts associated in project {project}." + if project in centrally_covered: + report.status = "PASS" + report.status_extended = f"Log metric filter {centrally_covered[project]} found with an alert, covering project {project} via an organization-level aggregated sink." + else: + report.status = "FAIL" + report.status_extended = f"There are no log metric filters or alerts associated in project {project}." findings.append(report) return findings diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.py b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.py index e2b7cdcc13..0e05838f05 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.py +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.py @@ -1,5 +1,8 @@ from prowler.lib.check.models import Check, Check_Report_GCP from prowler.providers.gcp.services.logging.logging_client import logging_client +from prowler.providers.gcp.services.logging.logging_service import ( + get_projects_covered_by_aggregated_metric, +) from prowler.providers.gcp.services.monitoring.monitoring_client import ( monitoring_client, ) @@ -8,12 +11,10 @@ from prowler.providers.gcp.services.monitoring.monitoring_client import ( class logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled(Check): def execute(self) -> Check_Report_GCP: findings = [] + metric_filter = 'resource.type="gce_firewall_rule" AND (protoPayload.methodName:"compute.firewalls.patch" OR protoPayload.methodName:"compute.firewalls.insert" OR protoPayload.methodName:"compute.firewalls.delete")' projects_with_metric = set() for metric in logging_client.metrics: - if ( - 'resource.type="gce_firewall_rule" AND (protoPayload.methodName:"compute.firewalls.patch" OR protoPayload.methodName:"compute.firewalls.insert" OR protoPayload.methodName:"compute.firewalls.delete")' - in metric.filter - ): + if metric_filter in metric.filter: report = Check_Report_GCP( metadata=self.metadata(), resource=metric, @@ -31,6 +32,9 @@ class logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled( break findings.append(report) + centrally_covered = get_projects_covered_by_aggregated_metric( + logging_client, monitoring_client, metric_filter + ) for project in logging_client.project_ids: if project not in projects_with_metric: report = Check_Report_GCP( @@ -44,8 +48,12 @@ class logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled( else "GCP Project" ), ) - report.status = "FAIL" - report.status_extended = f"There are no log metric filters or alerts associated in project {project}." + if project in centrally_covered: + report.status = "PASS" + report.status_extended = f"Log metric filter {centrally_covered[project]} found with an alert, covering project {project} via an organization-level aggregated sink." + else: + report.status = "FAIL" + report.status_extended = f"There are no log metric filters or alerts associated in project {project}." findings.append(report) return findings diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.py b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.py index c8b15ce1ee..1330ad7a9a 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.py +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.py @@ -1,5 +1,8 @@ from prowler.lib.check.models import Check, Check_Report_GCP from prowler.providers.gcp.services.logging.logging_client import logging_client +from prowler.providers.gcp.services.logging.logging_service import ( + get_projects_covered_by_aggregated_metric, +) from prowler.providers.gcp.services.monitoring.monitoring_client import ( monitoring_client, ) @@ -8,12 +11,10 @@ from prowler.providers.gcp.services.monitoring.monitoring_client import ( class logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled(Check): def execute(self) -> Check_Report_GCP: findings = [] + metric_filter = 'resource.type="gce_network" AND (protoPayload.methodName:"compute.networks.insert" OR protoPayload.methodName:"compute.networks.patch" OR protoPayload.methodName:"compute.networks.delete" OR protoPayload.methodName:"compute.networks.removePeering" OR protoPayload.methodName:"compute.networks.addPeering")' projects_with_metric = set() for metric in logging_client.metrics: - if ( - 'resource.type="gce_network" AND (protoPayload.methodName:"compute.networks.insert" OR protoPayload.methodName:"compute.networks.patch" OR protoPayload.methodName:"compute.networks.delete" OR protoPayload.methodName:"compute.networks.removePeering" OR protoPayload.methodName:"compute.networks.addPeering")' - in metric.filter - ): + if metric_filter in metric.filter: report = Check_Report_GCP( metadata=self.metadata(), resource=metric, @@ -31,6 +32,9 @@ class logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled(Check) break findings.append(report) + centrally_covered = get_projects_covered_by_aggregated_metric( + logging_client, monitoring_client, metric_filter + ) for project in logging_client.project_ids: if project not in projects_with_metric: report = Check_Report_GCP( @@ -44,8 +48,12 @@ class logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled(Check) else "GCP Project" ), ) - report.status = "FAIL" - report.status_extended = f"There are no log metric filters or alerts associated in project {project}." + if project in centrally_covered: + report.status = "PASS" + report.status_extended = f"Log metric filter {centrally_covered[project]} found with an alert, covering project {project} via an organization-level aggregated sink." + else: + report.status = "FAIL" + report.status_extended = f"There are no log metric filters or alerts associated in project {project}." findings.append(report) return findings diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.py b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.py index f840d75852..27f25879e8 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.py +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.py @@ -1,5 +1,8 @@ from prowler.lib.check.models import Check, Check_Report_GCP from prowler.providers.gcp.services.logging.logging_client import logging_client +from prowler.providers.gcp.services.logging.logging_service import ( + get_projects_covered_by_aggregated_metric, +) from prowler.providers.gcp.services.monitoring.monitoring_client import ( monitoring_client, ) @@ -8,12 +11,10 @@ from prowler.providers.gcp.services.monitoring.monitoring_client import ( class logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled(Check): def execute(self) -> Check_Report_GCP: findings = [] + metric_filter = 'resource.type="gce_route" AND (protoPayload.methodName:"compute.routes.delete" OR protoPayload.methodName:"compute.routes.insert")' projects_with_metric = set() for metric in logging_client.metrics: - if ( - 'resource.type="gce_route" AND (protoPayload.methodName:"compute.routes.delete" OR protoPayload.methodName:"compute.routes.insert")' - in metric.filter - ): + if metric_filter in metric.filter: report = Check_Report_GCP( metadata=self.metadata(), resource=metric, @@ -31,6 +32,9 @@ class logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled( break findings.append(report) + centrally_covered = get_projects_covered_by_aggregated_metric( + logging_client, monitoring_client, metric_filter + ) for project in logging_client.project_ids: if project not in projects_with_metric: report = Check_Report_GCP( @@ -44,8 +48,12 @@ class logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled( else "GCP Project" ), ) - report.status = "FAIL" - report.status_extended = f"There are no log metric filters or alerts associated in project {project}." + if project in centrally_covered: + report.status = "PASS" + report.status_extended = f"Log metric filter {centrally_covered[project]} found with an alert, covering project {project} via an organization-level aggregated sink." + else: + report.status = "FAIL" + report.status_extended = f"There are no log metric filters or alerts associated in project {project}." findings.append(report) return findings diff --git a/prowler/providers/gcp/services/logging/logging_service.py b/prowler/providers/gcp/services/logging/logging_service.py index 2459895c4c..3d5b0d1e79 100644 --- a/prowler/providers/gcp/services/logging/logging_service.py +++ b/prowler/providers/gcp/services/logging/logging_service.py @@ -90,6 +90,7 @@ class Logging(GCPService): type=metric["metricDescriptor"]["type"], filter=metric["filter"], project_id=project_id, + bucket_name=metric.get("bucketName", ""), ) ) @@ -117,3 +118,59 @@ class Metric(BaseModel): type: str filter: str project_id: str + bucket_name: str = "" + + +def get_projects_covered_by_aggregated_metric( + logging_client, monitoring_client, metric_filter +): + """Return {project_id: metric_name} for scanned projects whose logs are routed, + via an organization-level sink with includeChildren=True, to a bucket that holds + a bucket-scoped log metric matching ``metric_filter`` that has an alert policy. + + The CIS GCP logging-metric checks are written per-project, but a common (and + recommended) topology centralizes monitoring: an org-level aggregated sink ships + every child project's logs into one bucket, where a single bucket-scoped metric + + alert covers them all. Without crediting that, those child projects are falsely + failed. Mirrors the org-sink handling already in ``logging_sink_created`` (#11355). + """ + # Buckets that hold a matching, alerted, bucket-scoped metric -> metric name. + bucket_to_metric = {} + for metric in logging_client.metrics: + if not getattr(metric, "bucket_name", ""): + continue + if metric_filter not in metric.filter: + continue + if any( + metric.name in policy_filter + for alert_policy in monitoring_client.alert_policies + for policy_filter in alert_policy.filters + ): + bucket_to_metric[metric.bucket_name] = metric.name + if not bucket_to_metric: + return {} + + # Org resources whose includeChildren sink targets one of those buckets. + org_to_metric = {} + for sink in logging_client.sinks: + if not getattr(sink, "include_children", False): + continue + if getattr(sink, "filter", "all") != "all": + continue + for bucket, metric_name in bucket_to_metric.items(): + # sink.destination e.g. "logging.googleapis.com/projects/.../buckets/X"; + # metric.bucket_name e.g. "projects/.../buckets/X". + if sink.destination.endswith(bucket): + org_to_metric[sink.project_id] = metric_name + break + if not org_to_metric: + return {} + + # Scanned projects sitting under a covering organization. + covered = {} + for project_id in logging_client.project_ids: + project = logging_client.projects.get(project_id) + organization = getattr(project, "organization", None) if project else None + if organization and f"organizations/{organization.id}" in org_to_metric: + covered[project_id] = org_to_metric[f"organizations/{organization.id}"] + return covered diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled_test.py index a38f97d81c..c0dc6b0a06 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled_test.py @@ -259,3 +259,176 @@ class Test_logging_log_metric_filter_and_alert_for_audit_configuration_changes_e assert result[0].resource_name == "metric_name" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION + + def test_project_centrally_covered_via_org_aggregated_sink(self): + """A child project with NO local metric, but whose org has an aggregated + sink (includeChildren=True) routing its logs to a central bucket that has + a bucket-scoped metric + alert, should PASS (covered centrally) instead of + being falsely failed.""" + logging_client = MagicMock() + monitoring_client = MagicMock() + org_id = "111222333" + central_bucket = ( + "projects/central-logging-project/locations/eu/buckets/central-bucket" + ) + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.models import GCPOrganization + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled import ( + logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled, + ) + from prowler.providers.gcp.services.logging.logging_service import ( + Metric, + Sink, + ) + from prowler.providers.gcp.services.monitoring.monitoring_service import ( + AlertPolicy, + ) + + logging_client.region = GCP_EU1_LOCATION + logging_client.project_ids = [GCP_PROJECT_ID, "central-logging-project"] + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="child", + labels={}, + lifecycle_state="ACTIVE", + organization=GCPOrganization( + id=org_id, name=f"organizations/{org_id}" + ), + ) + } + # Bucket-scoped central metric, in the scanned logging project. + logging_client.metrics = [ + Metric( + name="central-audit-config-metric", + type="logging.googleapis.com/user/central-audit-config-metric", + filter='protoPayload.methodName="SetIamPolicy" AND protoPayload.serviceData.policyDelta.auditConfigDeltas:*', + project_id="central-logging-project", + bucket_name=central_bucket, + ) + ] + # Org-level aggregated sink routing the child's logs to that bucket. + logging_client.sinks = [ + Sink( + name="org-aggregated-sink", + destination=f"logging.googleapis.com/{central_bucket}", + filter="all", + project_id=f"organizations/{org_id}", + include_children=True, + ) + ] + monitoring_client.alert_policies = [ + AlertPolicy( + name="projects/central-logging-project/alertPolicies/ap", + display_name="central-alert", + enabled=True, + filters=[ + 'metric.type = "logging.googleapis.com/user/central-audit-config-metric"' + ], + project_id="central-logging-project", + ) + ] + + check = ( + logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled() + ) + result = check.execute() + + assert any( + r.project_id == GCP_PROJECT_ID + and r.status == "PASS" + and "aggregated sink" in r.status_extended + for r in result + ), [(r.project_id, r.status, r.status_extended) for r in result] + + def test_aggregated_sink_metric_without_alert_still_fails(self): + """Guard: an org aggregated sink + a bucket-scoped metric matching the filter + but with NO alert must NOT credit the child project — it should still FAIL.""" + logging_client = MagicMock() + monitoring_client = MagicMock() + org_id = "111222333" + central_bucket = ( + "projects/central-logging-project/locations/eu/buckets/central-bucket" + ) + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.models import GCPOrganization + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled import ( + logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled, + ) + from prowler.providers.gcp.services.logging.logging_service import ( + Metric, + Sink, + ) + + logging_client.region = GCP_EU1_LOCATION + logging_client.project_ids = [GCP_PROJECT_ID, "central-logging-project"] + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="child", + labels={}, + lifecycle_state="ACTIVE", + organization=GCPOrganization( + id=org_id, name=f"organizations/{org_id}" + ), + ) + } + logging_client.metrics = [ + Metric( + name="central-metric", + type="logging.googleapis.com/user/central-metric", + filter='protoPayload.methodName="SetIamPolicy" AND protoPayload.serviceData.policyDelta.auditConfigDeltas:*', + project_id="central-logging-project", + bucket_name=central_bucket, + ) + ] + logging_client.sinks = [ + Sink( + name="org-aggregated-sink", + destination=f"logging.googleapis.com/{central_bucket}", + filter="all", + project_id=f"organizations/{org_id}", + include_children=True, + ) + ] + monitoring_client.alert_policies = [] # no alert -> must NOT credit + + check = ( + logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled() + ) + result = check.execute() + + child = [r for r in result if r.project_id == GCP_PROJECT_ID] + assert child and all(r.status == "FAIL" for r in child), [ + (r.project_id, r.status) for r in result + ] diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled_test.py index e2b7b2d068..e9eeabf430 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled_test.py @@ -397,3 +397,173 @@ class Test_logging_log_metric_filter_and_alert_for_bucket_permission_changes_ena assert result[0].resource_name == "GCP Project" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION + + def test_project_centrally_covered_via_org_aggregated_sink(self): + """A child project with NO local metric, but whose org has an aggregated + sink (includeChildren=True) routing its logs to a central bucket that has + a bucket-scoped metric + alert, should PASS (covered centrally).""" + logging_client = MagicMock() + monitoring_client = MagicMock() + org_id = "111222333" + central_bucket = ( + "projects/central-logging-project/locations/eu/buckets/central-bucket" + ) + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.models import GCPOrganization, GCPProject + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled import ( + logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled, + ) + from prowler.providers.gcp.services.logging.logging_service import ( + Metric, + Sink, + ) + from prowler.providers.gcp.services.monitoring.monitoring_service import ( + AlertPolicy, + ) + + logging_client.region = GCP_EU1_LOCATION + logging_client.project_ids = [GCP_PROJECT_ID, "central-logging-project"] + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="child", + labels={}, + lifecycle_state="ACTIVE", + organization=GCPOrganization( + id=org_id, name=f"organizations/{org_id}" + ), + ) + } + logging_client.metrics = [ + Metric( + name="central-metric", + type="logging.googleapis.com/user/central-metric", + filter='resource.type="gcs_bucket" AND protoPayload.methodName="storage.setIamPermissions"', + project_id="central-logging-project", + bucket_name=central_bucket, + ) + ] + logging_client.sinks = [ + Sink( + name="org-aggregated-sink", + destination=f"logging.googleapis.com/{central_bucket}", + filter="all", + project_id=f"organizations/{org_id}", + include_children=True, + ) + ] + monitoring_client.alert_policies = [ + AlertPolicy( + name="projects/central-logging-project/alertPolicies/ap", + display_name="central-alert", + enabled=True, + filters=[ + 'metric.type = "logging.googleapis.com/user/central-metric"' + ], + project_id="central-logging-project", + ) + ] + + check = ( + logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled() + ) + result = check.execute() + + assert any( + r.project_id == GCP_PROJECT_ID + and r.status == "PASS" + and "aggregated sink" in r.status_extended + for r in result + ), [(r.project_id, r.status, r.status_extended) for r in result] + + def test_aggregated_sink_metric_without_alert_still_fails(self): + """Guard: an org aggregated sink + a bucket-scoped metric matching the filter + but with NO alert must NOT credit the child project — it should still FAIL.""" + logging_client = MagicMock() + monitoring_client = MagicMock() + org_id = "111222333" + central_bucket = ( + "projects/central-logging-project/locations/eu/buckets/central-bucket" + ) + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.models import GCPOrganization, GCPProject + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled import ( + logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled, + ) + from prowler.providers.gcp.services.logging.logging_service import ( + Metric, + Sink, + ) + + logging_client.region = GCP_EU1_LOCATION + logging_client.project_ids = [GCP_PROJECT_ID, "central-logging-project"] + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="child", + labels={}, + lifecycle_state="ACTIVE", + organization=GCPOrganization( + id=org_id, name=f"organizations/{org_id}" + ), + ) + } + logging_client.metrics = [ + Metric( + name="central-metric", + type="logging.googleapis.com/user/central-metric", + filter='resource.type="gcs_bucket" AND protoPayload.methodName="storage.setIamPermissions"', + project_id="central-logging-project", + bucket_name=central_bucket, + ) + ] + logging_client.sinks = [ + Sink( + name="org-aggregated-sink", + destination=f"logging.googleapis.com/{central_bucket}", + filter="all", + project_id=f"organizations/{org_id}", + include_children=True, + ) + ] + monitoring_client.alert_policies = [] # no alert -> must NOT credit + + check = ( + logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled() + ) + result = check.execute() + + child = [r for r in result if r.project_id == GCP_PROJECT_ID] + assert child and all(r.status == "FAIL" for r in child), [ + (r.project_id, r.status) for r in result + ] diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled_test.py index 563b4e49ac..5347e3e42e 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled_test.py @@ -346,3 +346,173 @@ class Test_logging_log_metric_filter_and_alert_for_compute_configuration_changes fail_result = [r for r in result if r.status == "FAIL"][0] assert fail_result.project_id == project_id_2 assert "no log metric filters" in fail_result.status_extended + + def test_project_centrally_covered_via_org_aggregated_sink(self): + """A child project with NO local metric, but whose org has an aggregated + sink (includeChildren=True) routing its logs to a central bucket that has + a bucket-scoped metric + alert, should PASS (covered centrally).""" + logging_client = MagicMock() + monitoring_client = MagicMock() + org_id = "111222333" + central_bucket = ( + "projects/central-logging-project/locations/eu/buckets/central-bucket" + ) + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.models import GCPOrganization, GCPProject + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled import ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled, + ) + from prowler.providers.gcp.services.logging.logging_service import ( + Metric, + Sink, + ) + from prowler.providers.gcp.services.monitoring.monitoring_service import ( + AlertPolicy, + ) + + logging_client.region = GCP_EU1_LOCATION + logging_client.project_ids = [GCP_PROJECT_ID, "central-logging-project"] + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="child", + labels={}, + lifecycle_state="ACTIVE", + organization=GCPOrganization( + id=org_id, name=f"organizations/{org_id}" + ), + ) + } + logging_client.metrics = [ + Metric( + name="central-metric", + type="logging.googleapis.com/user/central-metric", + filter='protoPayload.serviceName="compute.googleapis.com"', + project_id="central-logging-project", + bucket_name=central_bucket, + ) + ] + logging_client.sinks = [ + Sink( + name="org-aggregated-sink", + destination=f"logging.googleapis.com/{central_bucket}", + filter="all", + project_id=f"organizations/{org_id}", + include_children=True, + ) + ] + monitoring_client.alert_policies = [ + AlertPolicy( + name="projects/central-logging-project/alertPolicies/ap", + display_name="central-alert", + enabled=True, + filters=[ + 'metric.type = "logging.googleapis.com/user/central-metric"' + ], + project_id="central-logging-project", + ) + ] + + check = ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled() + ) + result = check.execute() + + assert any( + r.project_id == GCP_PROJECT_ID + and r.status == "PASS" + and "aggregated sink" in r.status_extended + for r in result + ), [(r.project_id, r.status, r.status_extended) for r in result] + + def test_aggregated_sink_metric_without_alert_still_fails(self): + """Guard: an org aggregated sink + a bucket-scoped metric matching the filter + but with NO alert must NOT credit the child project — it should still FAIL.""" + logging_client = MagicMock() + monitoring_client = MagicMock() + org_id = "111222333" + central_bucket = ( + "projects/central-logging-project/locations/eu/buckets/central-bucket" + ) + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.models import GCPOrganization, GCPProject + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled import ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled, + ) + from prowler.providers.gcp.services.logging.logging_service import ( + Metric, + Sink, + ) + + logging_client.region = GCP_EU1_LOCATION + logging_client.project_ids = [GCP_PROJECT_ID, "central-logging-project"] + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="child", + labels={}, + lifecycle_state="ACTIVE", + organization=GCPOrganization( + id=org_id, name=f"organizations/{org_id}" + ), + ) + } + logging_client.metrics = [ + Metric( + name="central-metric", + type="logging.googleapis.com/user/central-metric", + filter='protoPayload.serviceName="compute.googleapis.com"', + project_id="central-logging-project", + bucket_name=central_bucket, + ) + ] + logging_client.sinks = [ + Sink( + name="org-aggregated-sink", + destination=f"logging.googleapis.com/{central_bucket}", + filter="all", + project_id=f"organizations/{org_id}", + include_children=True, + ) + ] + monitoring_client.alert_policies = [] # no alert -> must NOT credit + + check = ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled() + ) + result = check.execute() + + child = [r for r in result if r.project_id == GCP_PROJECT_ID] + assert child and all(r.status == "FAIL" for r in child), [ + (r.project_id, r.status) for r in result + ] diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled_test.py index 4ec94be657..18d1807255 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled_test.py @@ -259,3 +259,173 @@ class Test_logging_log_metric_filter_and_alert_for_custom_role_changes_enabled: assert result[0].resource_name == "metric_name" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION + + def test_project_centrally_covered_via_org_aggregated_sink(self): + """A child project with NO local metric, but whose org has an aggregated + sink (includeChildren=True) routing its logs to a central bucket that has + a bucket-scoped metric + alert, should PASS (covered centrally).""" + logging_client = MagicMock() + monitoring_client = MagicMock() + org_id = "111222333" + central_bucket = ( + "projects/central-logging-project/locations/eu/buckets/central-bucket" + ) + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.models import GCPOrganization, GCPProject + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.logging_log_metric_filter_and_alert_for_custom_role_changes_enabled import ( + logging_log_metric_filter_and_alert_for_custom_role_changes_enabled, + ) + from prowler.providers.gcp.services.logging.logging_service import ( + Metric, + Sink, + ) + from prowler.providers.gcp.services.monitoring.monitoring_service import ( + AlertPolicy, + ) + + logging_client.region = GCP_EU1_LOCATION + logging_client.project_ids = [GCP_PROJECT_ID, "central-logging-project"] + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="child", + labels={}, + lifecycle_state="ACTIVE", + organization=GCPOrganization( + id=org_id, name=f"organizations/{org_id}" + ), + ) + } + logging_client.metrics = [ + Metric( + name="central-metric", + type="logging.googleapis.com/user/central-metric", + filter='resource.type="iam_role" AND (protoPayload.methodName="google.iam.admin.v1.CreateRole" OR protoPayload.methodName="google.iam.admin.v1.DeleteRole" OR protoPayload.methodName="google.iam.admin.v1.UpdateRole")', + project_id="central-logging-project", + bucket_name=central_bucket, + ) + ] + logging_client.sinks = [ + Sink( + name="org-aggregated-sink", + destination=f"logging.googleapis.com/{central_bucket}", + filter="all", + project_id=f"organizations/{org_id}", + include_children=True, + ) + ] + monitoring_client.alert_policies = [ + AlertPolicy( + name="projects/central-logging-project/alertPolicies/ap", + display_name="central-alert", + enabled=True, + filters=[ + 'metric.type = "logging.googleapis.com/user/central-metric"' + ], + project_id="central-logging-project", + ) + ] + + check = ( + logging_log_metric_filter_and_alert_for_custom_role_changes_enabled() + ) + result = check.execute() + + assert any( + r.project_id == GCP_PROJECT_ID + and r.status == "PASS" + and "aggregated sink" in r.status_extended + for r in result + ), [(r.project_id, r.status, r.status_extended) for r in result] + + def test_aggregated_sink_metric_without_alert_still_fails(self): + """Guard: an org aggregated sink + a bucket-scoped metric matching the filter + but with NO alert must NOT credit the child project — it should still FAIL.""" + logging_client = MagicMock() + monitoring_client = MagicMock() + org_id = "111222333" + central_bucket = ( + "projects/central-logging-project/locations/eu/buckets/central-bucket" + ) + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.models import GCPOrganization, GCPProject + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.logging_log_metric_filter_and_alert_for_custom_role_changes_enabled import ( + logging_log_metric_filter_and_alert_for_custom_role_changes_enabled, + ) + from prowler.providers.gcp.services.logging.logging_service import ( + Metric, + Sink, + ) + + logging_client.region = GCP_EU1_LOCATION + logging_client.project_ids = [GCP_PROJECT_ID, "central-logging-project"] + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="child", + labels={}, + lifecycle_state="ACTIVE", + organization=GCPOrganization( + id=org_id, name=f"organizations/{org_id}" + ), + ) + } + logging_client.metrics = [ + Metric( + name="central-metric", + type="logging.googleapis.com/user/central-metric", + filter='resource.type="iam_role" AND (protoPayload.methodName="google.iam.admin.v1.CreateRole" OR protoPayload.methodName="google.iam.admin.v1.DeleteRole" OR protoPayload.methodName="google.iam.admin.v1.UpdateRole")', + project_id="central-logging-project", + bucket_name=central_bucket, + ) + ] + logging_client.sinks = [ + Sink( + name="org-aggregated-sink", + destination=f"logging.googleapis.com/{central_bucket}", + filter="all", + project_id=f"organizations/{org_id}", + include_children=True, + ) + ] + monitoring_client.alert_policies = [] # no alert -> must NOT credit + + check = ( + logging_log_metric_filter_and_alert_for_custom_role_changes_enabled() + ) + result = check.execute() + + child = [r for r in result if r.project_id == GCP_PROJECT_ID] + assert child and all(r.status == "FAIL" for r in child), [ + (r.project_id, r.status) for r in result + ] diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled_test.py index 0ea0798e03..3adb48e567 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled_test.py @@ -392,3 +392,173 @@ class Test_logging_log_metric_filter_and_alert_for_project_ownership_changes_ena assert result[0].resource_name == "GCP Project" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION + + def test_project_centrally_covered_via_org_aggregated_sink(self): + """A child project with NO local metric, but whose org has an aggregated + sink (includeChildren=True) routing its logs to a central bucket that has + a bucket-scoped metric + alert, should PASS (covered centrally).""" + logging_client = MagicMock() + monitoring_client = MagicMock() + org_id = "111222333" + central_bucket = ( + "projects/central-logging-project/locations/eu/buckets/central-bucket" + ) + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.models import GCPOrganization, GCPProject + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled import ( + logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled, + ) + from prowler.providers.gcp.services.logging.logging_service import ( + Metric, + Sink, + ) + from prowler.providers.gcp.services.monitoring.monitoring_service import ( + AlertPolicy, + ) + + logging_client.region = GCP_EU1_LOCATION + logging_client.project_ids = [GCP_PROJECT_ID, "central-logging-project"] + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="child", + labels={}, + lifecycle_state="ACTIVE", + organization=GCPOrganization( + id=org_id, name=f"organizations/{org_id}" + ), + ) + } + logging_client.metrics = [ + Metric( + name="central-metric", + type="logging.googleapis.com/user/central-metric", + filter='(protoPayload.serviceName="cloudresourcemanager.googleapis.com") AND (ProjectOwnership OR projectOwnerInvitee) OR (protoPayload.serviceData.policyDelta.bindingDeltas.action="REMOVE" AND protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/owner") OR (protoPayload.serviceData.policyDelta.bindingDeltas.action="ADD" AND protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/owner")', + project_id="central-logging-project", + bucket_name=central_bucket, + ) + ] + logging_client.sinks = [ + Sink( + name="org-aggregated-sink", + destination=f"logging.googleapis.com/{central_bucket}", + filter="all", + project_id=f"organizations/{org_id}", + include_children=True, + ) + ] + monitoring_client.alert_policies = [ + AlertPolicy( + name="projects/central-logging-project/alertPolicies/ap", + display_name="central-alert", + enabled=True, + filters=[ + 'metric.type = "logging.googleapis.com/user/central-metric"' + ], + project_id="central-logging-project", + ) + ] + + check = ( + logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled() + ) + result = check.execute() + + assert any( + r.project_id == GCP_PROJECT_ID + and r.status == "PASS" + and "aggregated sink" in r.status_extended + for r in result + ), [(r.project_id, r.status, r.status_extended) for r in result] + + def test_aggregated_sink_metric_without_alert_still_fails(self): + """Guard: an org aggregated sink + a bucket-scoped metric matching the filter + but with NO alert must NOT credit the child project — it should still FAIL.""" + logging_client = MagicMock() + monitoring_client = MagicMock() + org_id = "111222333" + central_bucket = ( + "projects/central-logging-project/locations/eu/buckets/central-bucket" + ) + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.models import GCPOrganization, GCPProject + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled import ( + logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled, + ) + from prowler.providers.gcp.services.logging.logging_service import ( + Metric, + Sink, + ) + + logging_client.region = GCP_EU1_LOCATION + logging_client.project_ids = [GCP_PROJECT_ID, "central-logging-project"] + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="child", + labels={}, + lifecycle_state="ACTIVE", + organization=GCPOrganization( + id=org_id, name=f"organizations/{org_id}" + ), + ) + } + logging_client.metrics = [ + Metric( + name="central-metric", + type="logging.googleapis.com/user/central-metric", + filter='(protoPayload.serviceName="cloudresourcemanager.googleapis.com") AND (ProjectOwnership OR projectOwnerInvitee) OR (protoPayload.serviceData.policyDelta.bindingDeltas.action="REMOVE" AND protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/owner") OR (protoPayload.serviceData.policyDelta.bindingDeltas.action="ADD" AND protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/owner")', + project_id="central-logging-project", + bucket_name=central_bucket, + ) + ] + logging_client.sinks = [ + Sink( + name="org-aggregated-sink", + destination=f"logging.googleapis.com/{central_bucket}", + filter="all", + project_id=f"organizations/{org_id}", + include_children=True, + ) + ] + monitoring_client.alert_policies = [] # no alert -> must NOT credit + + check = ( + logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled() + ) + result = check.execute() + + child = [r for r in result if r.project_id == GCP_PROJECT_ID] + assert child and all(r.status == "FAIL" for r in child), [ + (r.project_id, r.status) for r in result + ] diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled_test.py index 1a8a1d0da3..9444f0cc61 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled_test.py @@ -259,3 +259,173 @@ class Test_logging_log_metric_filter_and_alert_for_sql_instance_configuration_ch assert result[0].resource_name == "metric_name" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION + + def test_project_centrally_covered_via_org_aggregated_sink(self): + """A child project with NO local metric, but whose org has an aggregated + sink (includeChildren=True) routing its logs to a central bucket that has + a bucket-scoped metric + alert, should PASS (covered centrally).""" + logging_client = MagicMock() + monitoring_client = MagicMock() + org_id = "111222333" + central_bucket = ( + "projects/central-logging-project/locations/eu/buckets/central-bucket" + ) + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.models import GCPOrganization, GCPProject + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled import ( + logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled, + ) + from prowler.providers.gcp.services.logging.logging_service import ( + Metric, + Sink, + ) + from prowler.providers.gcp.services.monitoring.monitoring_service import ( + AlertPolicy, + ) + + logging_client.region = GCP_EU1_LOCATION + logging_client.project_ids = [GCP_PROJECT_ID, "central-logging-project"] + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="child", + labels={}, + lifecycle_state="ACTIVE", + organization=GCPOrganization( + id=org_id, name=f"organizations/{org_id}" + ), + ) + } + logging_client.metrics = [ + Metric( + name="central-metric", + type="logging.googleapis.com/user/central-metric", + filter='protoPayload.methodName="cloudsql.instances.update"', + project_id="central-logging-project", + bucket_name=central_bucket, + ) + ] + logging_client.sinks = [ + Sink( + name="org-aggregated-sink", + destination=f"logging.googleapis.com/{central_bucket}", + filter="all", + project_id=f"organizations/{org_id}", + include_children=True, + ) + ] + monitoring_client.alert_policies = [ + AlertPolicy( + name="projects/central-logging-project/alertPolicies/ap", + display_name="central-alert", + enabled=True, + filters=[ + 'metric.type = "logging.googleapis.com/user/central-metric"' + ], + project_id="central-logging-project", + ) + ] + + check = ( + logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled() + ) + result = check.execute() + + assert any( + r.project_id == GCP_PROJECT_ID + and r.status == "PASS" + and "aggregated sink" in r.status_extended + for r in result + ), [(r.project_id, r.status, r.status_extended) for r in result] + + def test_aggregated_sink_metric_without_alert_still_fails(self): + """Guard: an org aggregated sink + a bucket-scoped metric matching the filter + but with NO alert must NOT credit the child project — it should still FAIL.""" + logging_client = MagicMock() + monitoring_client = MagicMock() + org_id = "111222333" + central_bucket = ( + "projects/central-logging-project/locations/eu/buckets/central-bucket" + ) + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.models import GCPOrganization, GCPProject + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled import ( + logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled, + ) + from prowler.providers.gcp.services.logging.logging_service import ( + Metric, + Sink, + ) + + logging_client.region = GCP_EU1_LOCATION + logging_client.project_ids = [GCP_PROJECT_ID, "central-logging-project"] + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="child", + labels={}, + lifecycle_state="ACTIVE", + organization=GCPOrganization( + id=org_id, name=f"organizations/{org_id}" + ), + ) + } + logging_client.metrics = [ + Metric( + name="central-metric", + type="logging.googleapis.com/user/central-metric", + filter='protoPayload.methodName="cloudsql.instances.update"', + project_id="central-logging-project", + bucket_name=central_bucket, + ) + ] + logging_client.sinks = [ + Sink( + name="org-aggregated-sink", + destination=f"logging.googleapis.com/{central_bucket}", + filter="all", + project_id=f"organizations/{org_id}", + include_children=True, + ) + ] + monitoring_client.alert_policies = [] # no alert -> must NOT credit + + check = ( + logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled() + ) + result = check.execute() + + child = [r for r in result if r.project_id == GCP_PROJECT_ID] + assert child and all(r.status == "FAIL" for r in child), [ + (r.project_id, r.status) for r in result + ] diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled_test.py index a9460e6b46..3d34a3c295 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled_test.py @@ -259,3 +259,173 @@ class Test_logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_ena assert result[0].resource_name == "metric_name" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION + + def test_project_centrally_covered_via_org_aggregated_sink(self): + """A child project with NO local metric, but whose org has an aggregated + sink (includeChildren=True) routing its logs to a central bucket that has + a bucket-scoped metric + alert, should PASS (covered centrally).""" + logging_client = MagicMock() + monitoring_client = MagicMock() + org_id = "111222333" + central_bucket = ( + "projects/central-logging-project/locations/eu/buckets/central-bucket" + ) + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.models import GCPOrganization, GCPProject + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled import ( + logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled, + ) + from prowler.providers.gcp.services.logging.logging_service import ( + Metric, + Sink, + ) + from prowler.providers.gcp.services.monitoring.monitoring_service import ( + AlertPolicy, + ) + + logging_client.region = GCP_EU1_LOCATION + logging_client.project_ids = [GCP_PROJECT_ID, "central-logging-project"] + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="child", + labels={}, + lifecycle_state="ACTIVE", + organization=GCPOrganization( + id=org_id, name=f"organizations/{org_id}" + ), + ) + } + logging_client.metrics = [ + Metric( + name="central-metric", + type="logging.googleapis.com/user/central-metric", + filter='resource.type="gce_firewall_rule" AND (protoPayload.methodName:"compute.firewalls.patch" OR protoPayload.methodName:"compute.firewalls.insert" OR protoPayload.methodName:"compute.firewalls.delete")', + project_id="central-logging-project", + bucket_name=central_bucket, + ) + ] + logging_client.sinks = [ + Sink( + name="org-aggregated-sink", + destination=f"logging.googleapis.com/{central_bucket}", + filter="all", + project_id=f"organizations/{org_id}", + include_children=True, + ) + ] + monitoring_client.alert_policies = [ + AlertPolicy( + name="projects/central-logging-project/alertPolicies/ap", + display_name="central-alert", + enabled=True, + filters=[ + 'metric.type = "logging.googleapis.com/user/central-metric"' + ], + project_id="central-logging-project", + ) + ] + + check = ( + logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled() + ) + result = check.execute() + + assert any( + r.project_id == GCP_PROJECT_ID + and r.status == "PASS" + and "aggregated sink" in r.status_extended + for r in result + ), [(r.project_id, r.status, r.status_extended) for r in result] + + def test_aggregated_sink_metric_without_alert_still_fails(self): + """Guard: an org aggregated sink + a bucket-scoped metric matching the filter + but with NO alert must NOT credit the child project — it should still FAIL.""" + logging_client = MagicMock() + monitoring_client = MagicMock() + org_id = "111222333" + central_bucket = ( + "projects/central-logging-project/locations/eu/buckets/central-bucket" + ) + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.models import GCPOrganization, GCPProject + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled import ( + logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled, + ) + from prowler.providers.gcp.services.logging.logging_service import ( + Metric, + Sink, + ) + + logging_client.region = GCP_EU1_LOCATION + logging_client.project_ids = [GCP_PROJECT_ID, "central-logging-project"] + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="child", + labels={}, + lifecycle_state="ACTIVE", + organization=GCPOrganization( + id=org_id, name=f"organizations/{org_id}" + ), + ) + } + logging_client.metrics = [ + Metric( + name="central-metric", + type="logging.googleapis.com/user/central-metric", + filter='resource.type="gce_firewall_rule" AND (protoPayload.methodName:"compute.firewalls.patch" OR protoPayload.methodName:"compute.firewalls.insert" OR protoPayload.methodName:"compute.firewalls.delete")', + project_id="central-logging-project", + bucket_name=central_bucket, + ) + ] + logging_client.sinks = [ + Sink( + name="org-aggregated-sink", + destination=f"logging.googleapis.com/{central_bucket}", + filter="all", + project_id=f"organizations/{org_id}", + include_children=True, + ) + ] + monitoring_client.alert_policies = [] # no alert -> must NOT credit + + check = ( + logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled() + ) + result = check.execute() + + child = [r for r in result if r.project_id == GCP_PROJECT_ID] + assert child and all(r.status == "FAIL" for r in child), [ + (r.project_id, r.status) for r in result + ] diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled_test.py index 9c59d56a81..a71a21ceb6 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled_test.py @@ -259,3 +259,173 @@ class Test_logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled: assert result[0].resource_name == "metric_name" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION + + def test_project_centrally_covered_via_org_aggregated_sink(self): + """A child project with NO local metric, but whose org has an aggregated + sink (includeChildren=True) routing its logs to a central bucket that has + a bucket-scoped metric + alert, should PASS (covered centrally).""" + logging_client = MagicMock() + monitoring_client = MagicMock() + org_id = "111222333" + central_bucket = ( + "projects/central-logging-project/locations/eu/buckets/central-bucket" + ) + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.models import GCPOrganization, GCPProject + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled import ( + logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled, + ) + from prowler.providers.gcp.services.logging.logging_service import ( + Metric, + Sink, + ) + from prowler.providers.gcp.services.monitoring.monitoring_service import ( + AlertPolicy, + ) + + logging_client.region = GCP_EU1_LOCATION + logging_client.project_ids = [GCP_PROJECT_ID, "central-logging-project"] + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="child", + labels={}, + lifecycle_state="ACTIVE", + organization=GCPOrganization( + id=org_id, name=f"organizations/{org_id}" + ), + ) + } + logging_client.metrics = [ + Metric( + name="central-metric", + type="logging.googleapis.com/user/central-metric", + filter='resource.type="gce_network" AND (protoPayload.methodName:"compute.networks.insert" OR protoPayload.methodName:"compute.networks.patch" OR protoPayload.methodName:"compute.networks.delete" OR protoPayload.methodName:"compute.networks.removePeering" OR protoPayload.methodName:"compute.networks.addPeering")', + project_id="central-logging-project", + bucket_name=central_bucket, + ) + ] + logging_client.sinks = [ + Sink( + name="org-aggregated-sink", + destination=f"logging.googleapis.com/{central_bucket}", + filter="all", + project_id=f"organizations/{org_id}", + include_children=True, + ) + ] + monitoring_client.alert_policies = [ + AlertPolicy( + name="projects/central-logging-project/alertPolicies/ap", + display_name="central-alert", + enabled=True, + filters=[ + 'metric.type = "logging.googleapis.com/user/central-metric"' + ], + project_id="central-logging-project", + ) + ] + + check = ( + logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled() + ) + result = check.execute() + + assert any( + r.project_id == GCP_PROJECT_ID + and r.status == "PASS" + and "aggregated sink" in r.status_extended + for r in result + ), [(r.project_id, r.status, r.status_extended) for r in result] + + def test_aggregated_sink_metric_without_alert_still_fails(self): + """Guard: an org aggregated sink + a bucket-scoped metric matching the filter + but with NO alert must NOT credit the child project — it should still FAIL.""" + logging_client = MagicMock() + monitoring_client = MagicMock() + org_id = "111222333" + central_bucket = ( + "projects/central-logging-project/locations/eu/buckets/central-bucket" + ) + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.models import GCPOrganization, GCPProject + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled import ( + logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled, + ) + from prowler.providers.gcp.services.logging.logging_service import ( + Metric, + Sink, + ) + + logging_client.region = GCP_EU1_LOCATION + logging_client.project_ids = [GCP_PROJECT_ID, "central-logging-project"] + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="child", + labels={}, + lifecycle_state="ACTIVE", + organization=GCPOrganization( + id=org_id, name=f"organizations/{org_id}" + ), + ) + } + logging_client.metrics = [ + Metric( + name="central-metric", + type="logging.googleapis.com/user/central-metric", + filter='resource.type="gce_network" AND (protoPayload.methodName:"compute.networks.insert" OR protoPayload.methodName:"compute.networks.patch" OR protoPayload.methodName:"compute.networks.delete" OR protoPayload.methodName:"compute.networks.removePeering" OR protoPayload.methodName:"compute.networks.addPeering")', + project_id="central-logging-project", + bucket_name=central_bucket, + ) + ] + logging_client.sinks = [ + Sink( + name="org-aggregated-sink", + destination=f"logging.googleapis.com/{central_bucket}", + filter="all", + project_id=f"organizations/{org_id}", + include_children=True, + ) + ] + monitoring_client.alert_policies = [] # no alert -> must NOT credit + + check = ( + logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled() + ) + result = check.execute() + + child = [r for r in result if r.project_id == GCP_PROJECT_ID] + assert child and all(r.status == "FAIL" for r in child), [ + (r.project_id, r.status) for r in result + ] diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled_test.py index 254c41bb5f..3a7f41a485 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled_test.py @@ -259,3 +259,173 @@ class Test_logging_log_metric_filter_and_alert_for_vpc_network_route_changes_ena assert result[0].resource_name == "metric_name" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION + + def test_project_centrally_covered_via_org_aggregated_sink(self): + """A child project with NO local metric, but whose org has an aggregated + sink (includeChildren=True) routing its logs to a central bucket that has + a bucket-scoped metric + alert, should PASS (covered centrally).""" + logging_client = MagicMock() + monitoring_client = MagicMock() + org_id = "111222333" + central_bucket = ( + "projects/central-logging-project/locations/eu/buckets/central-bucket" + ) + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.models import GCPOrganization, GCPProject + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled import ( + logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled, + ) + from prowler.providers.gcp.services.logging.logging_service import ( + Metric, + Sink, + ) + from prowler.providers.gcp.services.monitoring.monitoring_service import ( + AlertPolicy, + ) + + logging_client.region = GCP_EU1_LOCATION + logging_client.project_ids = [GCP_PROJECT_ID, "central-logging-project"] + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="child", + labels={}, + lifecycle_state="ACTIVE", + organization=GCPOrganization( + id=org_id, name=f"organizations/{org_id}" + ), + ) + } + logging_client.metrics = [ + Metric( + name="central-metric", + type="logging.googleapis.com/user/central-metric", + filter='resource.type="gce_route" AND (protoPayload.methodName:"compute.routes.delete" OR protoPayload.methodName:"compute.routes.insert")', + project_id="central-logging-project", + bucket_name=central_bucket, + ) + ] + logging_client.sinks = [ + Sink( + name="org-aggregated-sink", + destination=f"logging.googleapis.com/{central_bucket}", + filter="all", + project_id=f"organizations/{org_id}", + include_children=True, + ) + ] + monitoring_client.alert_policies = [ + AlertPolicy( + name="projects/central-logging-project/alertPolicies/ap", + display_name="central-alert", + enabled=True, + filters=[ + 'metric.type = "logging.googleapis.com/user/central-metric"' + ], + project_id="central-logging-project", + ) + ] + + check = ( + logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled() + ) + result = check.execute() + + assert any( + r.project_id == GCP_PROJECT_ID + and r.status == "PASS" + and "aggregated sink" in r.status_extended + for r in result + ), [(r.project_id, r.status, r.status_extended) for r in result] + + def test_aggregated_sink_metric_without_alert_still_fails(self): + """Guard: an org aggregated sink + a bucket-scoped metric matching the filter + but with NO alert must NOT credit the child project — it should still FAIL.""" + logging_client = MagicMock() + monitoring_client = MagicMock() + org_id = "111222333" + central_bucket = ( + "projects/central-logging-project/locations/eu/buckets/central-bucket" + ) + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.models import GCPOrganization, GCPProject + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled import ( + logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled, + ) + from prowler.providers.gcp.services.logging.logging_service import ( + Metric, + Sink, + ) + + logging_client.region = GCP_EU1_LOCATION + logging_client.project_ids = [GCP_PROJECT_ID, "central-logging-project"] + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="child", + labels={}, + lifecycle_state="ACTIVE", + organization=GCPOrganization( + id=org_id, name=f"organizations/{org_id}" + ), + ) + } + logging_client.metrics = [ + Metric( + name="central-metric", + type="logging.googleapis.com/user/central-metric", + filter='resource.type="gce_route" AND (protoPayload.methodName:"compute.routes.delete" OR protoPayload.methodName:"compute.routes.insert")', + project_id="central-logging-project", + bucket_name=central_bucket, + ) + ] + logging_client.sinks = [ + Sink( + name="org-aggregated-sink", + destination=f"logging.googleapis.com/{central_bucket}", + filter="all", + project_id=f"organizations/{org_id}", + include_children=True, + ) + ] + monitoring_client.alert_policies = [] # no alert -> must NOT credit + + check = ( + logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled() + ) + result = check.execute() + + child = [r for r in result if r.project_id == GCP_PROJECT_ID] + assert child and all(r.status == "FAIL" for r in child), [ + (r.project_id, r.status) for r in result + ] diff --git a/tests/providers/gcp/services/logging/logging_service_test.py b/tests/providers/gcp/services/logging/logging_service_test.py index 49368d0289..72e18eddbb 100644 --- a/tests/providers/gcp/services/logging/logging_service_test.py +++ b/tests/providers/gcp/services/logging/logging_service_test.py @@ -137,3 +137,166 @@ class TestLoggingService: s for s in logging_svc.sinks if s.project_id.startswith("organizations/") ] assert org_sinks == [] + + def test_get_metrics_populates_bucket_name(self): + """_get_metrics() captures a metric's bucketName (for aggregated-sink crediting).""" + bucket = "projects/central-logging-project/locations/eu/buckets/central-bucket" + mock_client = MagicMock() + mock_client.sinks().list().execute.return_value = {"sinks": []} + mock_client.sinks().list_next.return_value = None + mock_client.projects().metrics().list().execute.return_value = { + "metrics": [ + { + "name": "central-metric", + "metricDescriptor": { + "type": "logging.googleapis.com/user/central-metric" + }, + "filter": "severity>=ERROR", + "bucketName": bucket, + } + ] + } + mock_client.projects().metrics().list_next.return_value = None + + 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__", + return_value=mock_client, + ), + ): + logging_svc = Logging(set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID])) + + metrics = [m for m in logging_svc.metrics if m.name == "central-metric"] + assert len(metrics) == 1 + assert metrics[0].bucket_name == bucket + + +class TestGetProjectsCoveredByAggregatedMetric: + """Unit tests for the aggregated-sink crediting helper: one positive case and the + guards that must NOT credit a project (so the metric-filter checks never false-pass). + """ + + FILTER = 'protoPayload.methodName="SetIamPolicy"' + ORG = "111222333" + BUCKET = "projects/central-logging-project/locations/eu/buckets/central-bucket" + + def _clients( + self, + *, + include_children=True, + bucket_name=None, + sink_destination=None, + sink_filter="all", + with_alert=True, + project_org_id=None, + ): + from prowler.providers.gcp.models import GCPOrganization, GCPProject + from prowler.providers.gcp.services.logging.logging_service import Metric, Sink + from prowler.providers.gcp.services.monitoring.monitoring_service import ( + AlertPolicy, + ) + + bucket_name = self.BUCKET if bucket_name is None else bucket_name + sink_destination = ( + f"logging.googleapis.com/{self.BUCKET}" + if sink_destination is None + else sink_destination + ) + project_org_id = self.ORG if project_org_id is None else project_org_id + + logging_client = MagicMock() + logging_client.project_ids = [GCP_PROJECT_ID] + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="child", + labels={}, + lifecycle_state="ACTIVE", + organization=GCPOrganization( + id=project_org_id, name=f"organizations/{project_org_id}" + ), + ) + } + logging_client.metrics = [ + Metric( + name="central-metric", + type="logging.googleapis.com/user/central-metric", + filter=self.FILTER, + project_id="central-logging-project", + bucket_name=bucket_name, + ) + ] + logging_client.sinks = [ + Sink( + name="org-sink", + destination=sink_destination, + filter=sink_filter, + project_id=f"organizations/{self.ORG}", + include_children=include_children, + ) + ] + monitoring_client = MagicMock() + monitoring_client.alert_policies = ( + [ + AlertPolicy( + name="projects/central-logging-project/alertPolicies/ap", + display_name="central-alert", + enabled=True, + filters=[ + 'metric.type = "logging.googleapis.com/user/central-metric"' + ], + project_id="central-logging-project", + ) + ] + if with_alert + else [] + ) + return logging_client, monitoring_client + + def _run(self, logging_client, monitoring_client): + from prowler.providers.gcp.services.logging.logging_service import ( + get_projects_covered_by_aggregated_metric, + ) + + return get_projects_covered_by_aggregated_metric( + logging_client, monitoring_client, self.FILTER + ) + + def test_covered_when_all_conditions_met(self): + logging_client, monitoring_client = self._clients() + assert self._run(logging_client, monitoring_client) == { + GCP_PROJECT_ID: "central-metric" + } + + def test_not_covered_without_alert(self): + logging_client, monitoring_client = self._clients(with_alert=False) + assert self._run(logging_client, monitoring_client) == {} + + def test_not_covered_when_metric_not_bucket_scoped(self): + logging_client, monitoring_client = self._clients(bucket_name="") + assert self._run(logging_client, monitoring_client) == {} + + def test_not_covered_when_sink_not_include_children(self): + logging_client, monitoring_client = self._clients(include_children=False) + assert self._run(logging_client, monitoring_client) == {} + + def test_not_covered_when_sink_filter_is_restrictive(self): + logging_client, monitoring_client = self._clients( + sink_filter='resource.type="gce_instance"' + ) + assert self._run(logging_client, monitoring_client) == {} + + def test_not_covered_when_sink_destination_bucket_differs(self): + logging_client, monitoring_client = self._clients( + sink_destination="logging.googleapis.com/projects/x/locations/eu/buckets/other" + ) + assert self._run(logging_client, monitoring_client) == {} + + def test_not_covered_when_project_org_differs(self): + logging_client, monitoring_client = self._clients(project_org_id="999999999") + assert self._run(logging_client, monitoring_client) == {}