diff --git a/docs/user-guide/cli/tutorials/configuration_file.mdx b/docs/user-guide/cli/tutorials/configuration_file.mdx index fe8cbf4f39..73f9ff3999 100644 --- a/docs/user-guide/cli/tutorials/configuration_file.mdx +++ b/docs/user-guide/cli/tutorials/configuration_file.mdx @@ -97,6 +97,7 @@ The following list includes all the GCP checks with configurable variables that | Check Name | Value | Type | |---------------------------------------------------------------|--------------------------------------------------|-----------------| +| `compute_configuration_changes` | `compute_audit_log_lookback_days` | Integer | | `compute_instance_group_multiple_zones` | `mig_min_zones` | Integer | ## Kubernetes @@ -553,6 +554,9 @@ gcp: # GCP Compute Configuration # gcp.compute_public_address_shodan shodan_api_key: null + # gcp.compute_configuration_changes + # Number of days to look back for Compute Engine configuration changes in audit logs + compute_audit_log_lookback_days: 1 # gcp.compute_instance_group_multiple_zones # Minimum number of zones a MIG should span for high availability mig_min_zones: 2 diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index dbf0711422..5a3620f32f 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `compute_instance_disk_auto_delete_disabled` check for GCP provider [(#9604)](https://github.com/prowler-cloud/prowler/pull/9604) - Bedrock service pagination [(#9606)](https://github.com/prowler-cloud/prowler/pull/9606) - `ResourceGroup` field to all check metadata for resource classification [(#9656)](https://github.com/prowler-cloud/prowler/pull/9656) +- `compute_configuration_changes` check for GCP provider to detect Compute Engine configuration changes in Cloud Audit Logs [(#9698)](https://github.com/prowler-cloud/prowler/pull/9698) - `compute_instance_group_load_balancer_attached` check for GCP provider [(#9695)](https://github.com/prowler-cloud/prowler/pull/9695) ### Changed diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/__init__.py b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.metadata.json b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.metadata.json new file mode 100644 index 0000000000..9cdbbc51e8 --- /dev/null +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "gcp", + "CheckID": "logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled", + "CheckTitle": "Compute Engine configuration changes are monitored with log metric filters and alerts", + "CheckType": [], + "ServiceName": "logging", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "MetricFilter", + "ResourceGroup": "monitoring", + "Description": "Log metric filters and alerts for **Compute Engine configuration changes** provide visibility into modifications to instances, disks, networks, firewalls, and routes. These monitoring controls enable security teams to detect unauthorized changes and investigate suspicious infrastructure modifications.", + "Risk": "Without monitoring for Compute Engine configuration changes, **unauthorized modifications** to compute resources may go undetected. Attackers can establish **persistence** through instance modifications, escalate privileges via IAM policy changes, disable security controls, or pivot to other resources. This compromises **confidentiality**, **integrity**, and **availability** of workloads and may enable **data exfiltration** or **lateral movement**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/gcp-compute-engine-configuration-changes.html", + "https://cloud.google.com/logging/docs/audit", + "https://cloud.google.com/monitoring/alerts" + ], + "Remediation": { + "Code": { + "CLI": "gcloud logging metrics create compute_config_changes --description=\"Compute Engine configuration changes\" --log-filter='protoPayload.serviceName=\"compute.googleapis.com\"' && gcloud alpha monitoring policies create --notification-channels=CHANNEL_ID --display-name=\"Compute Engine Configuration Changes Alert\" --condition-threshold-value=1 --condition-threshold-duration=0s --condition-filter='metric.type=\"logging.googleapis.com/user/compute_config_changes\"'", + "NativeIaC": "", + "Other": "1. Open the Google Cloud Console\n2. Navigate to Logging > Logs-based Metrics\n3. Click 'Create Metric'\n4. Set Metric Type to 'Counter'\n5. Enter filter: protoPayload.serviceName=\"compute.googleapis.com\"\n6. Click 'Create Metric'\n7. Navigate to Monitoring > Alerting\n8. Click 'Create Policy'\n9. Click 'Add Condition'\n10. Select your log metric in the metric dropdown\n11. Set threshold and conditions\n12. Add notification channels\n13. Click 'Save'", + "Terraform": "```hcl\nresource \"google_logging_metric\" \"compute_config_changes\" {\n name = \"compute_config_changes\"\n filter = \"protoPayload.serviceName=\\\"compute.googleapis.com\\\"\"\n metric_descriptor {\n metric_kind = \"DELTA\"\n value_type = \"INT64\"\n }\n}\n\nresource \"google_monitoring_alert_policy\" \"compute_config_alert\" {\n display_name = \"Compute Engine Configuration Changes\"\n conditions {\n display_name = \"Compute config changes detected\"\n condition_threshold {\n filter = \"metric.type=\\\"logging.googleapis.com/user/compute_config_changes\\\"\"\n duration = \"0s\"\n comparison = \"COMPARISON_GT\"\n threshold_value = 0\n }\n }\n notification_channels = [var.notification_channel_id]\n}\n```" + }, + "Recommendation": { + "Text": "Configure log-based metric filters to detect Compute Engine configuration changes and create alert policies that trigger notifications when these metrics increment. Apply the **principle of least privilege** to limit who can modify compute resources, and establish **change management processes** to review and approve infrastructure modifications.", + "Url": "https://hub.prowler.com/check/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled" + } + }, + "Categories": [ + "logging" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.py b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.py new file mode 100644 index 0000000000..7902f9ed72 --- /dev/null +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.py @@ -0,0 +1,50 @@ +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.monitoring.monitoring_client import ( + monitoring_client, +) + + +class logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled( + Check +): + def execute(self) -> Check_Report_GCP: + findings = [] + projects_with_metric = set() + for metric in logging_client.metrics: + if 'protoPayload.serviceName="compute.googleapis.com"' in metric.filter: + report = Check_Report_GCP( + metadata=self.metadata(), + resource=metric, + location=logging_client.region, + resource_name=metric.name if metric.name else "Log Metric Filter", + ) + projects_with_metric.add(metric.project_id) + report.status = "FAIL" + report.status_extended = f"Log metric filter {metric.name} found but no alerts associated in project {metric.project_id}." + for alert_policy in monitoring_client.alert_policies: + for filter in alert_policy.filters: + if metric.name in filter: + report.status = "PASS" + report.status_extended = f"Log metric filter {metric.name} found with alert policy {alert_policy.display_name} associated in project {metric.project_id}." + break + findings.append(report) + + for project in logging_client.project_ids: + if project not in projects_with_metric: + report = Check_Report_GCP( + metadata=self.metadata(), + resource=logging_client.projects[project], + project_id=project, + location=logging_client.region, + resource_name=( + logging_client.projects[project].name + if logging_client.projects[project].name + 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}." + findings.append(report) + + return findings diff --git a/tests/providers/gcp/gcp_fixtures.py b/tests/providers/gcp/gcp_fixtures.py index c6182bbf0c..cbbfdd1238 100644 --- a/tests/providers/gcp/gcp_fixtures.py +++ b/tests/providers/gcp/gcp_fixtures.py @@ -32,6 +32,11 @@ def set_mocked_gcp_provider( provider.identity = GCPIdentityInfo( profile=profile, ) + provider.audit_config = { + "mig_min_zones": 2, + "max_unused_account_days": 30, + } + provider.fixer_config = {} return provider @@ -1102,6 +1107,34 @@ def mock_api_sink_calls(client: MagicMock): } client.sinks().list_next.return_value = None + client.entries().list().execute.return_value = { + "entries": [ + { + "insertId": "audit-log-entry-1", + "timestamp": "2024-01-15T10:30:00Z", + "receiveTimestamp": "2024-01-15T10:30:01Z", + "resource": { + "type": "gce_instance", + "labels": { + "instance_id": "test-instance-1", + "project_id": GCP_PROJECT_ID, + }, + }, + "protoPayload": { + "serviceName": "compute.googleapis.com", + "methodName": "v1.compute.instances.insert", + "resourceName": "projects/test-project/zones/us-central1-a/instances/test-instance-1", + "authenticationInfo": { + "principalEmail": "user@example.com", + }, + "requestMetadata": { + "callerIp": "192.168.1.1", + }, + }, + }, + ] + } + def mock_api_services_calls(client: MagicMock): client.services().list().execute.return_value = { diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled_test.py new file mode 100644 index 0000000000..563b4e49ac --- /dev/null +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled_test.py @@ -0,0 +1,348 @@ +from unittest.mock import MagicMock, patch + +from prowler.providers.gcp.models import GCPProject +from tests.providers.gcp.gcp_fixtures import ( + GCP_EU1_LOCATION, + GCP_PROJECT_ID, + set_mocked_gcp_provider, +) + + +class Test_logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled: + def test_no_projects(self): + logging_client = MagicMock() + monitoring_client = MagicMock() + + 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.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, + ) + + logging_client.metrics = [] + logging_client.project_ids = [] + monitoring_client.alert_policies = [] + + check = ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled() + ) + result = check.execute() + assert len(result) == 0 + + def test_no_log_metric_filters_no_alerts_one_project(self): + logging_client = MagicMock() + monitoring_client = MagicMock() + + 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.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, + ) + + logging_client.metrics = [] + logging_client.project_ids = [GCP_PROJECT_ID] + logging_client.region = GCP_EU1_LOCATION + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="test", + labels={}, + lifecycle_state="ACTIVE", + ) + } + + monitoring_client.alert_policies = [] + + check = ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled() + ) + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"There are no log metric filters or alerts associated for Compute Engine configuration changes in project {GCP_PROJECT_ID}." + ) + assert result[0].resource_id == GCP_PROJECT_ID + assert result[0].resource_name == "test" + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].location == GCP_EU1_LOCATION + + def test_no_log_metric_filters_no_alerts_one_project_empty_name(self): + logging_client = MagicMock() + monitoring_client = MagicMock() + + 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.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, + ) + + logging_client.metrics = [] + logging_client.project_ids = [GCP_PROJECT_ID] + logging_client.region = GCP_EU1_LOCATION + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="", + labels={}, + lifecycle_state="ACTIVE", + ) + } + + monitoring_client.alert_policies = [] + + check = ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled() + ) + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"There are no log metric filters or alerts associated for Compute Engine configuration changes in project {GCP_PROJECT_ID}." + ) + assert result[0].resource_id == GCP_PROJECT_ID + assert result[0].resource_name == "GCP Project" + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].location == GCP_EU1_LOCATION + + def test_log_metric_filters_no_alerts(self): + logging_client = MagicMock() + monitoring_client = MagicMock() + + 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.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 + + logging_client.metrics = [ + Metric( + name="compute_config_changes", + type="logging.googleapis.com/user/compute_config_changes", + filter='protoPayload.serviceName="compute.googleapis.com"', + project_id=GCP_PROJECT_ID, + ) + ] + logging_client.project_ids = [GCP_PROJECT_ID] + logging_client.region = GCP_EU1_LOCATION + + monitoring_client.alert_policies = [] + + check = ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled() + ) + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Log metric filter compute_config_changes found but no alerts associated in project {GCP_PROJECT_ID}." + ) + assert result[0].resource_id == "compute_config_changes" + assert result[0].resource_name == "compute_config_changes" + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].location == GCP_EU1_LOCATION + + def test_log_metric_filters_with_alerts(self): + logging_client = MagicMock() + monitoring_client = MagicMock() + + 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.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 + from prowler.providers.gcp.services.monitoring.monitoring_service import ( + AlertPolicy, + ) + + logging_client.metrics = [ + Metric( + name="compute_config_changes", + type="logging.googleapis.com/user/compute_config_changes", + filter='protoPayload.serviceName="compute.googleapis.com"', + project_id=GCP_PROJECT_ID, + ) + ] + logging_client.project_ids = [GCP_PROJECT_ID] + logging_client.region = GCP_EU1_LOCATION + + monitoring_client.alert_policies = [ + AlertPolicy( + name=f"projects/{GCP_PROJECT_ID}/alertPolicies/12345", + display_name="Compute Config Alert", + enabled=True, + filters=[ + 'metric.type = "logging.googleapis.com/user/compute_config_changes"', + ], + project_id=GCP_PROJECT_ID, + ) + ] + + check = ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled() + ) + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Log metric filter compute_config_changes found with alert policy Compute Config Alert associated in project {GCP_PROJECT_ID}." + ) + assert result[0].resource_id == "compute_config_changes" + assert result[0].resource_name == "compute_config_changes" + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].location == GCP_EU1_LOCATION + + def test_multiple_projects_mixed_results(self): + logging_client = MagicMock() + monitoring_client = MagicMock() + + project_id_1 = "project-with-monitoring" + project_id_2 = "project-without-monitoring" + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider( + project_ids=[project_id_1, project_id_2] + ), + ), + 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.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 + from prowler.providers.gcp.services.monitoring.monitoring_service import ( + AlertPolicy, + ) + + logging_client.metrics = [ + Metric( + name="compute_config_changes", + type="logging.googleapis.com/user/compute_config_changes", + filter='protoPayload.serviceName="compute.googleapis.com"', + project_id=project_id_1, + ) + ] + logging_client.project_ids = [project_id_1, project_id_2] + logging_client.region = GCP_EU1_LOCATION + logging_client.projects = { + project_id_1: GCPProject( + id=project_id_1, + number="111111111111", + name="test-project-1", + labels={}, + lifecycle_state="ACTIVE", + ), + project_id_2: GCPProject( + id=project_id_2, + number="222222222222", + name="test-project-2", + labels={}, + lifecycle_state="ACTIVE", + ), + } + + monitoring_client.alert_policies = [ + AlertPolicy( + name=f"projects/{project_id_1}/alertPolicies/12345", + display_name="Compute Config Alert", + enabled=True, + filters=[ + 'metric.type = "logging.googleapis.com/user/compute_config_changes"', + ], + project_id=project_id_1, + ) + ] + + check = ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled() + ) + result = check.execute() + assert len(result) == 2 + + # Project 1 should PASS (has metric + alert) + pass_result = [r for r in result if r.status == "PASS"][0] + assert pass_result.project_id == project_id_1 + assert "compute_config_changes" in pass_result.status_extended + assert "Compute Config Alert" in pass_result.status_extended + + # Project 2 should FAIL (no metric) + 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