mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-06-17 04:52:05 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 19a1ac2744 | |||
| 859fe1a29f | |||
| a0ee9107db | |||
| d49ec58c02 | |||
| 736f3f6f02 | |||
| 7b8acf974a | |||
| c6d8aa78dc | |||
| 7c9f8971a5 |
@@ -145,7 +145,7 @@ SENTRY_RELEASE=local
|
||||
NEXT_PUBLIC_SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT}
|
||||
|
||||
#### Prowler release version ####
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.29.2
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.29.3
|
||||
|
||||
# Social login credentials
|
||||
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
## [1.30.3] (Prowler v5.29.3)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- API startup no longer crashes when Neo4j is unreachable, as the Neo4j driver now connects lazily on first use rather than during app initialization [(#11491)](https://github.com/prowler-cloud/prowler/pull/11491)
|
||||
|
||||
---
|
||||
|
||||
## [1.30.1] (Prowler v5.29.1)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ name = "prowler-api"
|
||||
package-mode = false
|
||||
# Needed for the SDK compatibility
|
||||
requires-python = ">=3.11,<3.13"
|
||||
version = "1.30.2"
|
||||
version = "1.30.3"
|
||||
|
||||
[tool.uv]
|
||||
# Transitive pins matching master to avoid silent drift; bump deliberately.
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
|
||||
from config.custom_logging import BackendLogger
|
||||
from config.env import env
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(BackendLogger.API)
|
||||
|
||||
@@ -30,7 +32,6 @@ class ApiConfig(AppConfig):
|
||||
def ready(self):
|
||||
from api import schema_extensions # noqa: F401
|
||||
from api import signals # noqa: F401
|
||||
from api.attack_paths import database as graph_database
|
||||
|
||||
# Generate required cryptographic keys if not present, but only if:
|
||||
# `"manage.py" not in sys.argv[0]`: If an external server (e.g., Gunicorn) is running the app
|
||||
@@ -41,37 +42,8 @@ class ApiConfig(AppConfig):
|
||||
):
|
||||
self._ensure_crypto_keys()
|
||||
|
||||
# Commands that don't need Neo4j
|
||||
SKIP_NEO4J_DJANGO_COMMANDS = [
|
||||
"makemigrations",
|
||||
"migrate",
|
||||
"pgpartition",
|
||||
"check",
|
||||
"help",
|
||||
"showmigrations",
|
||||
"check_and_fix_socialaccount_sites_migration",
|
||||
]
|
||||
|
||||
# Skip eager Neo4j init for tests, some Django commands, and Celery (prefork pool: driver must stay lazy, no post_fork hook)
|
||||
if getattr(settings, "TESTING", False) or (
|
||||
len(sys.argv) > 1
|
||||
and (
|
||||
(
|
||||
"manage.py" in sys.argv[0]
|
||||
and sys.argv[1] in SKIP_NEO4J_DJANGO_COMMANDS
|
||||
)
|
||||
or "celery" in sys.argv[0]
|
||||
)
|
||||
):
|
||||
logger.info(
|
||||
"Skipping eager Neo4j init: tests, some Django commands, or Celery prefork pool (driver stays lazy)"
|
||||
)
|
||||
|
||||
else:
|
||||
graph_database.init_driver()
|
||||
|
||||
# Neo4j driver is initialized at API startup (see api.attack_paths.database)
|
||||
# It remains lazy for Celery workers and selected Django commands
|
||||
# Neo4j driver is created lazily on first use (see api.attack_paths.database).
|
||||
# App init never contacts Neo4j, so a Neo4j outage cannot block API startup.
|
||||
|
||||
def _ensure_crypto_keys(self):
|
||||
"""
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import atexit
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Iterator
|
||||
from uuid import UUID
|
||||
|
||||
import neo4j
|
||||
import neo4j.exceptions
|
||||
|
||||
from config.env import env
|
||||
from django.conf import settings
|
||||
|
||||
from api.attack_paths.retryable_session import RetryableSession
|
||||
from tasks.jobs.attack_paths.config import (
|
||||
BATCH_SIZE,
|
||||
PROVIDER_RESOURCE_LABEL,
|
||||
get_provider_label,
|
||||
)
|
||||
|
||||
from api.attack_paths.retryable_session import RetryableSession
|
||||
|
||||
# Without this Celery goes crazy with Neo4j logging
|
||||
logging.getLogger("neo4j").setLevel(logging.ERROR)
|
||||
logging.getLogger("neo4j").propagate = False
|
||||
@@ -28,6 +30,9 @@ READ_QUERY_TIMEOUT_SECONDS = env.int(
|
||||
"ATTACK_PATHS_READ_QUERY_TIMEOUT_SECONDS", default=30
|
||||
)
|
||||
MAX_CUSTOM_QUERY_NODES = env.int("ATTACK_PATHS_MAX_CUSTOM_QUERY_NODES", default=250)
|
||||
# Shorter than CONN_ACQUISITION_TIMEOUT — the driver requires acquisition to be
|
||||
# the longer of the two (it may include opening a new connection).
|
||||
CONNECTION_TIMEOUT = env.int("NEO4J_CONNECTION_TIMEOUT", default=5)
|
||||
CONN_ACQUISITION_TIMEOUT = env.int("NEO4J_CONN_ACQUISITION_TIMEOUT", default=15)
|
||||
READ_EXCEPTION_CODES = [
|
||||
"Neo.ClientError.Statement.AccessMode",
|
||||
@@ -58,15 +63,24 @@ def init_driver() -> neo4j.Driver:
|
||||
uri = get_uri()
|
||||
config = settings.DATABASES["neo4j"]
|
||||
|
||||
_driver = neo4j.GraphDatabase.driver(
|
||||
driver = neo4j.GraphDatabase.driver(
|
||||
uri,
|
||||
auth=(config["USER"], config["PASSWORD"]),
|
||||
keep_alive=True,
|
||||
max_connection_lifetime=7200,
|
||||
connection_timeout=CONNECTION_TIMEOUT,
|
||||
connection_acquisition_timeout=CONN_ACQUISITION_TIMEOUT,
|
||||
max_connection_pool_size=50,
|
||||
)
|
||||
_driver.verify_connectivity()
|
||||
# Publish the singleton only after connectivity is verified so a
|
||||
# failed probe does not leave an unverified driver behind. Close the
|
||||
# driver on failure so a repeatedly-probed outage cannot leak pools.
|
||||
try:
|
||||
driver.verify_connectivity()
|
||||
except Exception:
|
||||
driver.close()
|
||||
raise
|
||||
_driver = driver
|
||||
|
||||
# Register cleanup handler (only runs once since we're inside the _driver is None block)
|
||||
atexit.register(close_driver)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Prowler API
|
||||
version: 1.30.2
|
||||
version: 1.30.3
|
||||
description: |-
|
||||
Prowler API specification.
|
||||
|
||||
|
||||
@@ -182,23 +182,19 @@ def _make_app():
|
||||
return ApiConfig("api", api)
|
||||
|
||||
|
||||
def test_ready_initializes_driver_for_api_process(monkeypatch):
|
||||
@pytest.mark.parametrize(
|
||||
"argv",
|
||||
[
|
||||
["gunicorn"],
|
||||
["celery", "-A", "api"],
|
||||
["manage.py", "migrate"],
|
||||
],
|
||||
ids=["api", "celery", "manage_py"],
|
||||
)
|
||||
def test_ready_never_eagerly_initializes_neo4j_driver(monkeypatch, argv):
|
||||
"""ready() must never contact Neo4j; the driver is created lazily on first use."""
|
||||
config = _make_app()
|
||||
_set_argv(monkeypatch, ["gunicorn"])
|
||||
_set_testing(monkeypatch, False)
|
||||
|
||||
with (
|
||||
patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None),
|
||||
patch("api.attack_paths.database.init_driver") as init_driver,
|
||||
):
|
||||
config.ready()
|
||||
|
||||
init_driver.assert_called_once()
|
||||
|
||||
|
||||
def test_ready_skips_driver_for_celery(monkeypatch):
|
||||
config = _make_app()
|
||||
_set_argv(monkeypatch, ["celery", "-A", "api"])
|
||||
_set_argv(monkeypatch, argv)
|
||||
_set_testing(monkeypatch, False)
|
||||
|
||||
with (
|
||||
@@ -208,31 +204,3 @@ def test_ready_skips_driver_for_celery(monkeypatch):
|
||||
config.ready()
|
||||
|
||||
init_driver.assert_not_called()
|
||||
|
||||
|
||||
def test_ready_skips_driver_for_manage_py_skip_command(monkeypatch):
|
||||
config = _make_app()
|
||||
_set_argv(monkeypatch, ["manage.py", "migrate"])
|
||||
_set_testing(monkeypatch, False)
|
||||
|
||||
with (
|
||||
patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None),
|
||||
patch("api.attack_paths.database.init_driver") as init_driver,
|
||||
):
|
||||
config.ready()
|
||||
|
||||
init_driver.assert_not_called()
|
||||
|
||||
|
||||
def test_ready_skips_driver_when_testing(monkeypatch):
|
||||
config = _make_app()
|
||||
_set_argv(monkeypatch, ["gunicorn"])
|
||||
_set_testing(monkeypatch, True)
|
||||
|
||||
with (
|
||||
patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None),
|
||||
patch("api.attack_paths.database.init_driver") as init_driver,
|
||||
):
|
||||
config.ready()
|
||||
|
||||
init_driver.assert_not_called()
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
"""
|
||||
Tests for Neo4j database lazy initialization.
|
||||
|
||||
The Neo4j driver connects on first use by default. API processes may
|
||||
eagerly initialize the driver during app startup, while Celery workers
|
||||
remain lazy. These tests validate the database module behavior itself.
|
||||
The Neo4j driver is created on first use for every process type; app startup
|
||||
never contacts Neo4j. These tests validate the database module behavior itself.
|
||||
"""
|
||||
|
||||
import threading
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import neo4j
|
||||
import neo4j.exceptions
|
||||
import pytest
|
||||
|
||||
import api.attack_paths.database as db_module
|
||||
@@ -59,6 +60,32 @@ class TestLazyInitialization:
|
||||
assert result is mock_driver
|
||||
assert db_module._driver is mock_driver
|
||||
|
||||
@patch("api.attack_paths.database.settings")
|
||||
@patch("api.attack_paths.database.neo4j.GraphDatabase.driver")
|
||||
def test_init_driver_leaves_driver_none_when_verify_fails(
|
||||
self, mock_driver_factory, mock_settings
|
||||
):
|
||||
"""A failed verify_connectivity() must not publish or leak the driver."""
|
||||
mock_driver = MagicMock()
|
||||
mock_driver.verify_connectivity.side_effect = (
|
||||
neo4j.exceptions.ServiceUnavailable("down")
|
||||
)
|
||||
mock_driver_factory.return_value = mock_driver
|
||||
mock_settings.DATABASES = {
|
||||
"neo4j": {
|
||||
"HOST": "localhost",
|
||||
"PORT": 7687,
|
||||
"USER": "neo4j",
|
||||
"PASSWORD": "password",
|
||||
}
|
||||
}
|
||||
|
||||
with pytest.raises(neo4j.exceptions.ServiceUnavailable):
|
||||
db_module.init_driver()
|
||||
|
||||
assert db_module._driver is None
|
||||
mock_driver.close.assert_called_once()
|
||||
|
||||
@patch("api.attack_paths.database.settings")
|
||||
@patch("api.attack_paths.database.neo4j.GraphDatabase.driver")
|
||||
def test_init_driver_returns_cached_driver_on_subsequent_calls(
|
||||
@@ -116,21 +143,23 @@ class TestConnectionAcquisitionTimeout:
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_module_state(self):
|
||||
original_driver = db_module._driver
|
||||
original_timeout = db_module.CONN_ACQUISITION_TIMEOUT
|
||||
original_acq_timeout = db_module.CONN_ACQUISITION_TIMEOUT
|
||||
original_conn_timeout = db_module.CONNECTION_TIMEOUT
|
||||
|
||||
db_module._driver = None
|
||||
|
||||
yield
|
||||
|
||||
db_module._driver = original_driver
|
||||
db_module.CONN_ACQUISITION_TIMEOUT = original_timeout
|
||||
db_module.CONN_ACQUISITION_TIMEOUT = original_acq_timeout
|
||||
db_module.CONNECTION_TIMEOUT = original_conn_timeout
|
||||
|
||||
@patch("api.attack_paths.database.settings")
|
||||
@patch("api.attack_paths.database.neo4j.GraphDatabase.driver")
|
||||
def test_driver_receives_configured_timeout(
|
||||
self, mock_driver_factory, mock_settings
|
||||
):
|
||||
"""init_driver() should pass CONN_ACQUISITION_TIMEOUT to the neo4j driver."""
|
||||
"""init_driver() should pass the configured timeouts to the neo4j driver."""
|
||||
mock_driver_factory.return_value = MagicMock()
|
||||
mock_settings.DATABASES = {
|
||||
"neo4j": {
|
||||
@@ -141,11 +170,13 @@ class TestConnectionAcquisitionTimeout:
|
||||
}
|
||||
}
|
||||
db_module.CONN_ACQUISITION_TIMEOUT = 42
|
||||
db_module.CONNECTION_TIMEOUT = 7
|
||||
|
||||
db_module.init_driver()
|
||||
|
||||
_, kwargs = mock_driver_factory.call_args
|
||||
assert kwargs["connection_acquisition_timeout"] == 42
|
||||
assert kwargs["connection_timeout"] == 7
|
||||
|
||||
|
||||
class TestAtexitRegistration:
|
||||
|
||||
Generated
+1
-1
@@ -4498,7 +4498,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prowler-api"
|
||||
version = "1.30.2"
|
||||
version = "1.30.3"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "cartography" },
|
||||
|
||||
@@ -2,6 +2,17 @@
|
||||
|
||||
All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
## [5.29.3] (Prowler v5.29.3)
|
||||
|
||||
### 🐞 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)
|
||||
|
||||
---
|
||||
|
||||
## [5.29.1] (Prowler v5.29.1)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
@@ -48,7 +48,7 @@ class _MutableTimestamp:
|
||||
|
||||
timestamp = _MutableTimestamp(datetime.today())
|
||||
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
|
||||
prowler_version = "5.29.2"
|
||||
prowler_version = "5.29.3"
|
||||
html_logo_url = "https://github.com/prowler-cloud/prowler/"
|
||||
square_logo_img = "https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png"
|
||||
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
|
||||
|
||||
@@ -229,7 +229,9 @@ class MarkdownToADFConverter:
|
||||
return node
|
||||
|
||||
def _paragraph_with_text(self, text: str) -> Dict:
|
||||
return {"type": "paragraph", "content": [self._create_text_node(text, None)]}
|
||||
# ADF forbids empty text nodes; emit an empty paragraph instead.
|
||||
content = [self._create_text_node(text, None)] if text else []
|
||||
return {"type": "paragraph", "content": content}
|
||||
|
||||
@staticmethod
|
||||
def _pop_mark(marks_stack: List[Dict], mark_type: str) -> None:
|
||||
@@ -1118,6 +1120,18 @@ class Jira:
|
||||
tenant_info: str = "",
|
||||
) -> dict:
|
||||
|
||||
# ADF forbids empty text nodes, so Jira rejects them with 400 INVALID_INPUT.
|
||||
def _safe(value: str) -> str:
|
||||
return value if (value and value.strip()) else "-"
|
||||
|
||||
check_id = _safe(check_id)
|
||||
check_title = _safe(check_title)
|
||||
status_extended = _safe(status_extended)
|
||||
provider = _safe(provider)
|
||||
region = _safe(region)
|
||||
resource_uid = _safe(resource_uid)
|
||||
resource_name = _safe(resource_name)
|
||||
|
||||
table_rows = [
|
||||
{
|
||||
"type": "tableRow",
|
||||
|
||||
@@ -37,6 +37,7 @@ class IAM(GCPService):
|
||||
display_name=account.get("displayName", ""),
|
||||
project_id=project_id,
|
||||
uniqueId=account.get("uniqueId", ""),
|
||||
disabled=account.get("disabled", False),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -102,6 +103,7 @@ class ServiceAccount(BaseModel):
|
||||
keys: list[Key] = []
|
||||
project_id: str
|
||||
uniqueId: str
|
||||
disabled: bool = False
|
||||
|
||||
|
||||
class AccessApproval(GCPService):
|
||||
|
||||
+6
-1
@@ -19,7 +19,12 @@ class iam_service_account_unused(Check):
|
||||
resource_id=account.email,
|
||||
location=iam_client.region,
|
||||
)
|
||||
if account.uniqueId in sa_ids_used:
|
||||
if account.disabled:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Service Account {account.email} is disabled and cannot be used."
|
||||
)
|
||||
elif account.uniqueId in sa_ids_used:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Service Account {account.email} was used over the last {max_unused_days} days."
|
||||
else:
|
||||
|
||||
+16
-6
@@ -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
|
||||
|
||||
+14
-6
@@ -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
|
||||
|
||||
+14
-3
@@ -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
|
||||
|
||||
+14
-6
@@ -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
|
||||
|
||||
+14
-6
@@ -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
|
||||
|
||||
+14
-3
@@ -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
|
||||
|
||||
+14
-6
@@ -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
|
||||
|
||||
+14
-6
@@ -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
|
||||
|
||||
+14
-6
@@ -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
|
||||
|
||||
@@ -12,6 +12,7 @@ class Logging(GCPService):
|
||||
self.sinks = []
|
||||
self.metrics = []
|
||||
self._get_sinks()
|
||||
self._get_org_sinks()
|
||||
self._get_metrics()
|
||||
|
||||
def _get_sinks(self):
|
||||
@@ -39,6 +40,38 @@ class Logging(GCPService):
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_org_sinks(self):
|
||||
"""Fetch org-level sinks with includeChildren so child projects are not falsely failed."""
|
||||
org_ids = set()
|
||||
for project in self.projects.values():
|
||||
if project.organization:
|
||||
org_ids.add(project.organization.id)
|
||||
|
||||
for org_id in org_ids:
|
||||
try:
|
||||
request = self.client.sinks().list(parent=f"organizations/{org_id}")
|
||||
while request is not None:
|
||||
response = request.execute(num_retries=DEFAULT_RETRY_ATTEMPTS)
|
||||
|
||||
for sink in response.get("sinks", []):
|
||||
self.sinks.append(
|
||||
Sink(
|
||||
name=sink["name"],
|
||||
destination=sink["destination"],
|
||||
filter=sink.get("filter", "all"),
|
||||
project_id=f"organizations/{org_id}",
|
||||
include_children=sink.get("includeChildren", False),
|
||||
)
|
||||
)
|
||||
|
||||
request = self.client.sinks().list_next(
|
||||
previous_request=request, previous_response=response
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_metrics(self):
|
||||
for project_id in self.project_ids:
|
||||
try:
|
||||
@@ -57,6 +90,7 @@ class Logging(GCPService):
|
||||
type=metric["metricDescriptor"]["type"],
|
||||
filter=metric["filter"],
|
||||
project_id=project_id,
|
||||
bucket_name=metric.get("bucketName", ""),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -76,6 +110,7 @@ class Sink(BaseModel):
|
||||
destination: str
|
||||
filter: str
|
||||
project_id: str
|
||||
include_children: bool = False
|
||||
|
||||
|
||||
class Metric(BaseModel):
|
||||
@@ -83,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
|
||||
|
||||
+46
-15
@@ -5,26 +5,30 @@ from prowler.providers.gcp.services.logging.logging_client import logging_client
|
||||
class logging_sink_created(Check):
|
||||
def execute(self) -> Check_Report_GCP:
|
||||
findings = []
|
||||
|
||||
# Map project_id -> sink for direct project-level sinks
|
||||
projects_with_logging_sink = {}
|
||||
for sink in logging_client.sinks:
|
||||
if sink.filter == "all":
|
||||
if sink.filter == "all" and not sink.include_children:
|
||||
projects_with_logging_sink[sink.project_id] = sink
|
||||
|
||||
# Collect org resource names that have a covering sink (includeChildren=True)
|
||||
covering_org_sinks = {}
|
||||
for sink in logging_client.sinks:
|
||||
if sink.filter == "all" and sink.include_children:
|
||||
covering_org_sinks[sink.project_id] = sink
|
||||
|
||||
for project in logging_client.project_ids:
|
||||
if project not in projects_with_logging_sink.keys():
|
||||
project_obj = logging_client.projects.get(project)
|
||||
report = Check_Report_GCP(
|
||||
metadata=self.metadata(),
|
||||
resource=project_obj,
|
||||
resource_id=project,
|
||||
project_id=project,
|
||||
location=logging_client.region,
|
||||
resource_name=(getattr(project_obj, "name", None) or "GCP Project"),
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"There are no logging sinks to export copies of all the log entries in project {project}."
|
||||
findings.append(report)
|
||||
else:
|
||||
project_obj = logging_client.projects.get(project)
|
||||
|
||||
# Determine whether this project is covered by an org-level sink
|
||||
org = getattr(project_obj, "organization", None) if project_obj else None
|
||||
org_resource = f"organizations/{org.id}" if org else None
|
||||
covering_sink = (
|
||||
covering_org_sinks.get(org_resource) if org_resource else None
|
||||
)
|
||||
|
||||
if project in projects_with_logging_sink:
|
||||
sink = projects_with_logging_sink[project]
|
||||
sink_name = getattr(sink, "name", None) or "unknown"
|
||||
report = Check_Report_GCP(
|
||||
@@ -40,4 +44,31 @@ class logging_sink_created(Check):
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Sink {sink_name} is enabled exporting copies of all the log entries in project {project}."
|
||||
findings.append(report)
|
||||
elif covering_sink:
|
||||
sink_name = getattr(covering_sink, "name", None) or "unknown"
|
||||
report = Check_Report_GCP(
|
||||
metadata=self.metadata(),
|
||||
resource=covering_sink,
|
||||
resource_id=sink_name,
|
||||
project_id=project,
|
||||
location=logging_client.region,
|
||||
resource_name=(
|
||||
sink_name if sink_name != "unknown" else "Logging Sink"
|
||||
),
|
||||
)
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Sink {sink_name} at organization level is exporting copies of all the log entries in project {project}."
|
||||
findings.append(report)
|
||||
else:
|
||||
report = Check_Report_GCP(
|
||||
metadata=self.metadata(),
|
||||
resource=project_obj,
|
||||
resource_id=project,
|
||||
project_id=project,
|
||||
location=logging_client.region,
|
||||
resource_name=(getattr(project_obj, "name", None) or "GCP Project"),
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"There are no logging sinks to export copies of all the log entries in project {project}."
|
||||
findings.append(report)
|
||||
return findings
|
||||
|
||||
+1
-1
@@ -123,7 +123,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"}
|
||||
name = "prowler"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.13"
|
||||
version = "5.29.2"
|
||||
version = "5.29.3"
|
||||
|
||||
[project.scripts]
|
||||
prowler = "prowler.__main__:prowler"
|
||||
|
||||
@@ -1004,6 +1004,89 @@ class TestJiraIntegration:
|
||||
for mark in node.get("marks", [])
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _find_empty_text_nodes(node) -> List[str]:
|
||||
# ADF forbids empty text nodes; collect any to assert the document is valid.
|
||||
empties: List[str] = []
|
||||
|
||||
def walk(current) -> None:
|
||||
if isinstance(current, dict):
|
||||
if current.get("type") == "text" and current.get("text", "") == "":
|
||||
empties.append(current.get("text", ""))
|
||||
for value in current.values():
|
||||
walk(value)
|
||||
elif isinstance(current, list):
|
||||
for item in current:
|
||||
walk(item)
|
||||
|
||||
walk(node)
|
||||
return empties
|
||||
|
||||
def test_get_adf_description_empty_resource_name_has_no_empty_text_nodes(self):
|
||||
# A resource without a name (e.g. an AWS-managed IAM policy) used to emit an
|
||||
# empty ADF text node, making Jira reject the issue with 400 INVALID_INPUT.
|
||||
adf_description = self.jira_integration.get_adf_description(
|
||||
check_id="CHECK-1",
|
||||
check_title="Sample check",
|
||||
severity="CRITICAL",
|
||||
severity_color="#FF0000",
|
||||
status="FAIL",
|
||||
status_color="#FF0000",
|
||||
status_extended="Some status",
|
||||
provider="aws",
|
||||
region="eu-west-1",
|
||||
resource_uid="arn:aws:iam::aws:policy/AdministratorAccess",
|
||||
resource_name="",
|
||||
recommendation_text="",
|
||||
)
|
||||
|
||||
assert self._find_empty_text_nodes(adf_description) == []
|
||||
|
||||
table = adf_description["content"][1]
|
||||
resource_name_row = self._find_table_row(table["content"], "Resource Name")
|
||||
value_cell = resource_name_row["content"][1]
|
||||
assert self._collect_text_from_cell(value_cell) == "-"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field, header",
|
||||
[
|
||||
("check_id", "Check Id"),
|
||||
("check_title", "Check Title"),
|
||||
("status_extended", "Status Extended"),
|
||||
("provider", "Provider"),
|
||||
("region", "Region"),
|
||||
("resource_uid", "Resource UID"),
|
||||
("resource_name", "Resource Name"),
|
||||
],
|
||||
)
|
||||
def test_get_adf_description_empty_plain_text_fields_render_placeholder(
|
||||
self, field, header
|
||||
):
|
||||
base_kwargs = dict(
|
||||
check_id="CHECK-1",
|
||||
check_title="Sample check",
|
||||
severity="HIGH",
|
||||
severity_color="#FF0000",
|
||||
status="FAIL",
|
||||
status_color="#00FF00",
|
||||
status_extended="Some status",
|
||||
provider="aws",
|
||||
region="us-east-1",
|
||||
resource_uid="resource-1",
|
||||
resource_name="resource-name",
|
||||
recommendation_text="",
|
||||
)
|
||||
base_kwargs[field] = ""
|
||||
|
||||
adf_description = self.jira_integration.get_adf_description(**base_kwargs)
|
||||
|
||||
assert self._find_empty_text_nodes(adf_description) == []
|
||||
|
||||
table = adf_description["content"][1]
|
||||
row = self._find_table_row(table["content"], header)
|
||||
value_cell = row["content"][1]
|
||||
assert self._collect_text_from_cell(value_cell) == "-"
|
||||
|
||||
@patch.object(Jira, "get_access_token", return_value="valid_access_token")
|
||||
@patch.object(
|
||||
Jira, "get_available_issue_types", return_value=["Bug", "Task", "Story"]
|
||||
|
||||
+57
@@ -179,3 +179,60 @@ class Test_iam_service_account_unused:
|
||||
assert result[1].project_id == GCP_PROJECT_ID
|
||||
assert result[1].location == GCP_US_CENTER1_LOCATION
|
||||
assert result[1].resource == iam_client.service_accounts[1]
|
||||
|
||||
def test_iam_service_account_disabled(self):
|
||||
iam_client = mock.MagicMock()
|
||||
monitoring_client = mock.MagicMock()
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.gcp.services.iam.iam_service_account_unused.iam_service_account_unused.iam_client",
|
||||
new=iam_client,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.gcp.services.iam.iam_service_account_unused.iam_service_account_unused.monitoring_client",
|
||||
new=monitoring_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.iam.iam_service import ServiceAccount
|
||||
from prowler.providers.gcp.services.iam.iam_service_account_unused.iam_service_account_unused import (
|
||||
iam_service_account_unused,
|
||||
)
|
||||
|
||||
iam_client.project_ids = [GCP_PROJECT_ID]
|
||||
iam_client.region = GCP_US_CENTER1_LOCATION
|
||||
|
||||
iam_client.service_accounts = [
|
||||
ServiceAccount(
|
||||
name="projects/my-project/serviceAccounts/disabled-sa@my-project.iam.gserviceaccount.com",
|
||||
email="disabled-sa@my-project.iam.gserviceaccount.com",
|
||||
display_name="Disabled service account",
|
||||
keys=[],
|
||||
project_id=GCP_PROJECT_ID,
|
||||
uniqueId="999888877776666",
|
||||
disabled=True,
|
||||
)
|
||||
]
|
||||
|
||||
# The account is absent from the usage metrics, so a non-disabled
|
||||
# account here would FAIL. Being disabled must take precedence and
|
||||
# PASS, since a disabled account cannot authenticate or be used.
|
||||
monitoring_client.sa_api_metrics = set()
|
||||
monitoring_client.audit_config = {"max_unused_account_days": 30}
|
||||
|
||||
check = iam_service_account_unused()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Service Account {iam_client.service_accounts[0].email} is disabled and cannot be used."
|
||||
)
|
||||
assert result[0].resource_id == iam_client.service_accounts[0].email
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
assert result[0].location == GCP_US_CENTER1_LOCATION
|
||||
assert result[0].resource == iam_client.service_accounts[0]
|
||||
|
||||
+173
@@ -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
|
||||
]
|
||||
|
||||
+170
@@ -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
|
||||
]
|
||||
|
||||
+170
@@ -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
|
||||
]
|
||||
|
||||
+170
@@ -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
|
||||
]
|
||||
|
||||
+170
@@ -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
|
||||
]
|
||||
|
||||
+170
@@ -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
|
||||
]
|
||||
|
||||
+170
@@ -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
|
||||
]
|
||||
|
||||
+170
@@ -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
|
||||
]
|
||||
|
||||
+170
@@ -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
|
||||
]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from prowler.providers.gcp.services.logging.logging_service import Logging
|
||||
from tests.providers.gcp.gcp_fixtures import (
|
||||
@@ -66,3 +66,237 @@ class TestLoggingService:
|
||||
== "resource.type=gae_app AND severity>=ERROR"
|
||||
)
|
||||
assert logging_client.metrics[1].project_id == GCP_PROJECT_ID
|
||||
|
||||
def test_org_sinks_fetched_when_project_has_organization(self):
|
||||
"""_get_org_sinks() appends org-level sinks when projects have an org."""
|
||||
from prowler.providers.gcp.models import GCPOrganization, GCPProject
|
||||
|
||||
org_id = "999888777"
|
||||
provider = set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID])
|
||||
provider.projects = {
|
||||
GCP_PROJECT_ID: GCPProject(
|
||||
id=GCP_PROJECT_ID,
|
||||
number="123456789012",
|
||||
name="test",
|
||||
labels={},
|
||||
lifecycle_state="ACTIVE",
|
||||
organization=GCPOrganization(id=org_id, name=f"organizations/{org_id}"),
|
||||
)
|
||||
}
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client.sinks().list().execute.return_value = {
|
||||
"sinks": [
|
||||
{
|
||||
"name": "org-sink",
|
||||
"destination": "storage.googleapis.com/org-bucket",
|
||||
"filter": "all",
|
||||
"includeChildren": True,
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_client.sinks().list_next.return_value = None
|
||||
mock_client.projects().metrics().list().execute.return_value = {"metrics": []}
|
||||
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(provider)
|
||||
|
||||
org_sinks = [
|
||||
s for s in logging_svc.sinks if s.project_id == f"organizations/{org_id}"
|
||||
]
|
||||
assert len(org_sinks) == 1
|
||||
assert org_sinks[0].name == "org-sink"
|
||||
assert org_sinks[0].include_children is True
|
||||
assert org_sinks[0].filter == "all"
|
||||
|
||||
def test_org_sinks_skipped_when_no_organization(self):
|
||||
"""_get_org_sinks() adds nothing when projects have no organization."""
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.gcp.lib.service.service.GCPService.__is_api_active__",
|
||||
new=mock_is_api_active,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.gcp.lib.service.service.GCPService.__generate_client__",
|
||||
new=mock_api_client,
|
||||
),
|
||||
):
|
||||
logging_svc = Logging(set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID]))
|
||||
|
||||
org_sinks = [
|
||||
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) == {}
|
||||
|
||||
+176
-2
@@ -1,6 +1,6 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from prowler.providers.gcp.models import GCPProject
|
||||
from prowler.providers.gcp.models import GCPOrganization, GCPProject
|
||||
from tests.providers.gcp.gcp_fixtures import (
|
||||
GCP_EU1_LOCATION,
|
||||
GCP_PROJECT_ID,
|
||||
@@ -268,6 +268,7 @@ class Test_logging_sink_created:
|
||||
sink.name = None
|
||||
sink.filter = "all"
|
||||
sink.project_id = GCP_PROJECT_ID
|
||||
sink.include_children = False
|
||||
|
||||
logging_client.project_ids = [GCP_PROJECT_ID]
|
||||
logging_client.region = GCP_EU1_LOCATION
|
||||
@@ -311,9 +312,10 @@ class Test_logging_sink_created:
|
||||
)
|
||||
|
||||
# Create a MagicMock sink object without name attribute
|
||||
sink = MagicMock(spec=["filter", "project_id"])
|
||||
sink = MagicMock(spec=["filter", "project_id", "include_children"])
|
||||
sink.filter = "all"
|
||||
sink.project_id = GCP_PROJECT_ID
|
||||
sink.include_children = False
|
||||
|
||||
logging_client.project_ids = [GCP_PROJECT_ID]
|
||||
logging_client.region = GCP_EU1_LOCATION
|
||||
@@ -336,3 +338,175 @@ class Test_logging_sink_created:
|
||||
assert result[0].resource_id == "unknown"
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
assert result[0].location == GCP_EU1_LOCATION
|
||||
|
||||
def test_org_level_sink_with_include_children_passes(self):
|
||||
"""Projects covered by an org-level sink with includeChildren=True should PASS."""
|
||||
logging_client = MagicMock()
|
||||
org_id = "111222333"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.gcp.services.logging.logging_sink_created.logging_sink_created.logging_client",
|
||||
new=logging_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.logging.logging_service import Sink
|
||||
from prowler.providers.gcp.services.logging.logging_sink_created.logging_sink_created import (
|
||||
logging_sink_created,
|
||||
)
|
||||
|
||||
logging_client.project_ids = [GCP_PROJECT_ID]
|
||||
logging_client.region = GCP_EU1_LOCATION
|
||||
logging_client.sinks = [
|
||||
Sink(
|
||||
name="org-sink",
|
||||
destination="storage.googleapis.com/org-bucket",
|
||||
filter="all",
|
||||
project_id=f"organizations/{org_id}",
|
||||
include_children=True,
|
||||
)
|
||||
]
|
||||
logging_client.projects = {
|
||||
GCP_PROJECT_ID: GCPProject(
|
||||
id=GCP_PROJECT_ID,
|
||||
number="123456789012",
|
||||
name="test",
|
||||
labels={},
|
||||
lifecycle_state="ACTIVE",
|
||||
organization=GCPOrganization(
|
||||
id=org_id, name=f"organizations/{org_id}"
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
check = logging_sink_created()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Sink org-sink at organization level is exporting copies of all the log entries in project {GCP_PROJECT_ID}."
|
||||
)
|
||||
assert result[0].resource_id == "org-sink"
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
assert result[0].location == GCP_EU1_LOCATION
|
||||
|
||||
def test_org_level_sink_without_include_children_fails(self):
|
||||
"""Projects NOT covered by includeChildren should still FAIL if no direct project sink."""
|
||||
logging_client = MagicMock()
|
||||
org_id = "111222333"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.gcp.services.logging.logging_sink_created.logging_sink_created.logging_client",
|
||||
new=logging_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.logging.logging_service import Sink
|
||||
from prowler.providers.gcp.services.logging.logging_sink_created.logging_sink_created import (
|
||||
logging_sink_created,
|
||||
)
|
||||
|
||||
logging_client.project_ids = [GCP_PROJECT_ID]
|
||||
logging_client.region = GCP_EU1_LOCATION
|
||||
logging_client.sinks = [
|
||||
Sink(
|
||||
name="org-sink-no-children",
|
||||
destination="storage.googleapis.com/org-bucket",
|
||||
filter="all",
|
||||
project_id=f"organizations/{org_id}",
|
||||
include_children=False,
|
||||
)
|
||||
]
|
||||
logging_client.projects = {
|
||||
GCP_PROJECT_ID: GCPProject(
|
||||
id=GCP_PROJECT_ID,
|
||||
number="123456789012",
|
||||
name="test",
|
||||
labels={},
|
||||
lifecycle_state="ACTIVE",
|
||||
organization=GCPOrganization(
|
||||
id=org_id, name=f"organizations/{org_id}"
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
check = logging_sink_created()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"There are no logging sinks to export copies of all the log entries in project {GCP_PROJECT_ID}."
|
||||
)
|
||||
assert result[0].resource_id == GCP_PROJECT_ID
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
|
||||
def test_project_sink_takes_precedence_over_org_sink(self):
|
||||
"""A direct project sink should be reported even when an org-level sink also covers the project."""
|
||||
logging_client = MagicMock()
|
||||
org_id = "111222333"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.gcp.services.logging.logging_sink_created.logging_sink_created.logging_client",
|
||||
new=logging_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.logging.logging_service import Sink
|
||||
from prowler.providers.gcp.services.logging.logging_sink_created.logging_sink_created import (
|
||||
logging_sink_created,
|
||||
)
|
||||
|
||||
logging_client.project_ids = [GCP_PROJECT_ID]
|
||||
logging_client.region = GCP_EU1_LOCATION
|
||||
logging_client.sinks = [
|
||||
Sink(
|
||||
name="project-sink",
|
||||
destination="storage.googleapis.com/project-bucket",
|
||||
filter="all",
|
||||
project_id=GCP_PROJECT_ID,
|
||||
),
|
||||
Sink(
|
||||
name="org-sink",
|
||||
destination="storage.googleapis.com/org-bucket",
|
||||
filter="all",
|
||||
project_id=f"organizations/{org_id}",
|
||||
include_children=True,
|
||||
),
|
||||
]
|
||||
logging_client.projects = {
|
||||
GCP_PROJECT_ID: GCPProject(
|
||||
id=GCP_PROJECT_ID,
|
||||
number="123456789012",
|
||||
name="test",
|
||||
labels={},
|
||||
lifecycle_state="ACTIVE",
|
||||
organization=GCPOrganization(
|
||||
id=org_id, name=f"organizations/{org_id}"
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
check = logging_sink_created()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Sink project-sink is enabled exporting copies of all the log entries in project {GCP_PROJECT_ID}."
|
||||
)
|
||||
assert result[0].resource_id == "project-sink"
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
## [1.29.3] (Prowler v5.29.3)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Finding drawer tabs now keep the active tab text and underline styling when tooltip state changes [(#11493)](https://github.com/prowler-cloud/prowler/pull/11493)
|
||||
|
||||
---
|
||||
|
||||
## [1.29.2] (Prowler v5.29.2)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { Tabs, TabsList, TabsTrigger } from "./tabs";
|
||||
|
||||
describe("TabsTrigger", () => {
|
||||
it("keeps active styling available when rendered with a tooltip", () => {
|
||||
render(
|
||||
<Tabs defaultValue="overview">
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview" tooltip="Overview">
|
||||
Overview
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="remediation" tooltip="Remediation">
|
||||
Remediation
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>,
|
||||
);
|
||||
|
||||
const activeTrigger = screen.getByRole("tab", { name: "Overview" });
|
||||
|
||||
expect(activeTrigger).toHaveAttribute("aria-selected", "true");
|
||||
expect(activeTrigger).toHaveClass("aria-selected:text-slate-900");
|
||||
expect(activeTrigger).toHaveClass("aria-selected:after:scale-x-100");
|
||||
});
|
||||
});
|
||||
@@ -18,9 +18,9 @@ const TRIGGER_STYLES = {
|
||||
border: "border-r border-[#E9E9F0] last:border-r-0 dark:border-[#171D30]",
|
||||
text: "text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white",
|
||||
active:
|
||||
"data-[state=active]:text-slate-900 dark:data-[state=active]:text-white",
|
||||
"data-[state=active]:text-slate-900 aria-selected:text-slate-900 dark:data-[state=active]:text-white dark:aria-selected:text-white",
|
||||
underline:
|
||||
"after:absolute after:bottom-0 after:left-0 after:right-4 after:h-0.5 after:scale-x-0 after:bg-emerald-400 after:transition-transform data-[state=active]:after:scale-x-100 [&:not(:first-child)]:after:left-4 [&:last-child]:after:right-0",
|
||||
"after:absolute after:bottom-0 after:left-0 after:right-4 after:h-0.5 after:scale-x-0 after:bg-emerald-400 after:transition-transform data-[state=active]:after:scale-x-100 aria-selected:after:scale-x-100 [&:not(:first-child)]:after:left-4 [&:last-child]:after:right-0",
|
||||
focus:
|
||||
"focus-visible:ring-2 focus-visible:ring-emerald-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white focus-visible:outline-none dark:focus-visible:ring-offset-slate-950",
|
||||
icon: "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
|
||||
Reference in New Issue
Block a user