Rule body yaml
---
AnalysisType: rule
DedupPeriodMinutes: 60
DisplayName: GCP Logging Sink Modified
Enabled: true
Filename: gcp_logging_sink_modified.py
RuleID: "GCP.Logging.Sink.Modified"
Severity: Info
CreateAlert: false
LogTypes:
- GCP.AuditLog
Tags:
- GCP
- Logging
- Sink
- Infrastructure
Description: >
This rule detects modifications to GCP Log Sinks.
Runbook: >
Ensure that the modification was valid or expected. Adversaries may do this to exfiltrate logs or evade detection.
Reference: https://cloud.google.com/logging/docs
Tests:
- Name: logging-sink.modifed-should-alert
LogType: GCP.AuditLog
ExpectedResult: true
Log:
{
"insertid": "6ns26jclap",
"logname": "projects/test-project-123456/logs/cloudaudit.googleapis.com%2Factivity",
"protoPayload":
{
"at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
"authenticationInfo": { "principalEmail": "user@domain.com" },
"authorizationInfo":
[
{
"granted": true,
"permission": "logging.sinks.update",
"resource": "projects/test-project-123456/sinks/test-1",
"resourceAttributes":
{
"name": "projects/test-project-123456/sinks/test-1",
"service": "logging.googleapis.com",
},
},
],
"methodName": "google.logging.v2.ConfigServiceV2.UpdateSink",
"request":
{
"@type": "type.googleapis.com/google.logging.v2.UpdateSinkRequest",
"sink":
{
"description": "test",
"destination": "logging.googleapis.com/projects/test-project-123456/locations/global/buckets/testloggingbucket",
"exclusions": [{ "filter": "*", "name": "excludeall" }],
"name": "test-1",
},
"sinkName": "projects/test-project-123456/sinks/test-1",
"uniqueWriterIdentity": true,
"updateMask": "exclusions",
},
"requestMetadata":
{
"callerIP": "12.12.12.12",
"callerSuppliedUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36,gzip(gfe),gzip(gfe)",
"destinationAttributes": {},
"requestAttributes":
{ "auth": {}, "time": "2023-05-23T19:39:07.289670886Z" },
},
"resourceName": "projects/test-project-123456/sinks/test-1",
"serviceName": "logging.googleapis.com",
"status": {},
},
"receiveTimestamp": "2023-05-23 19:39:07.924",
"resource":
{
"labels":
{
"destination": "",
"name": "test-1",
"project_id": "test-project-123456",
},
"type": "logging_sink",
},
"severity": "NOTICE",
"timestamp": "2023-05-23 19:39:07.272",
}
- Name: logging-sink.non-modified-should-not-alert
LogType: GCP.AuditLog
ExpectedResult: false
Log:
{
"insertid": "6ns26jclap",
"logname": "projects/test-project-123456/logs/cloudaudit.googleapis.com%2Factivity",
"protoPayload":
{
"at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
"authenticationInfo": { "principalEmail": "user@domain.com" },
"authorizationInfo":
[
{
"granted": true,
"permission": "logging.sinks.list",
"resource": "projects/test-project-123456/sinks/test-1",
"resourceAttributes":
{
"name": "projects/test-project-123456/sinks/test-1",
"service": "logging.googleapis.com",
},
},
],
"methodName": "google.logging.v2.ConfigServiceV2.ListSink",
"request":
{
"@type": "type.googleapis.com/google.logging.v2.ListSinkRequest",
"sink":
{
"description": "test",
"destination": "logging.googleapis.com/projects/test-project-123456/locations/global/buckets/testloggingbucket",
"exclusions": [{ "filter": "*", "name": "excludeall" }],
"name": "test-1",
},
"sinkName": "projects/test-project-123456/sinks/test-1",
"uniqueWriterIdentity": true,
"updateMask": "exclusions",
},
"requestMetadata":
{
"callerIP": "12.12.12.12",
"callerSuppliedUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36,gzip(gfe),gzip(gfe)",
"destinationAttributes": {},
"requestAttributes":
{ "auth": {}, "time": "2023-05-23T19:39:07.289670886Z" },
},
"resourceName": "projects/test-project-123456/sinks/test-1",
"serviceName": "logging.googleapis.com",
"status": {},
},
"receiveTimestamp": "2023-05-23 19:39:07.924",
"resource":
{
"labels":
{
"destination": "",
"name": "test-1",
"project_id": "test-project-123456",
},
"type": "logging_sink",
},
"severity": "NOTICE",
"timestamp": "2023-05-23 19:39:07.272",
}
Detection logic
Rule logic imperative Python
import re
from panther_gcp_helpers import gcp_alert_context
def rule(event):
method_pattern = r"(?:\w+\.)*v\d\.(?:ConfigServiceV\d\.(?:UpdateSink))"
match = re.search(method_pattern, event.deep_get("protoPayload", "methodName", default=""))
return match is not None
def title(event):
actor = event.deep_get(
"protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
)
resource = event.deep_get(
"protoPayload",
"resourceName",
default="<RESOURCE_NOT_FOUND>",
)
return f"[GCP]: [{actor}] updated logging sink [{resource}]"
def alert_context(event):
return gcp_alert_context(event)
The parser cannot express this rule's logic as a field filter; the imperative Python above is the detection.
Output fields
Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |