feat(gcp): add check to detect Compute Engine configuration changes (#9698)

Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
This commit is contained in:
lydiavilchez
2026-01-12 12:22:15 +01:00
committed by GitHub
parent 9ee77c2b97
commit 62a8540169
7 changed files with 474 additions and 0 deletions

View File

@@ -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

View File

@@ -13,6 +13,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)
- `compute_instance_single_network_interface` check for GCP provider [(#9702)](https://github.com/prowler-cloud/prowler/pull/9702)
- `compute_image_not_publicly_shared` check for GCP provider [(#9718)](https://github.com/prowler-cloud/prowler/pull/9718)

View File

@@ -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": ""
}

View File

@@ -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

View File

@@ -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
@@ -1103,6 +1108,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 = {

View File

@@ -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