From de2ad9f90572b397cb39ba90b966a8fd699a4ebe Mon Sep 17 00:00:00 2001 From: Ricardo Valdes Date: Wed, 16 Sep 2026 20:34:30 -0400 Subject: [PATCH] fix: align Google Cloud filter and rule contracts --- filters/audits/gcp.md | 40 +++ filters/google/gcp.yml | 113 ++++++-- .../alerts/testdata/filter-contracts/gcp.json | 245 ++++++++++++++++++ rules/cloud/google/gcp_iam_policy_changed.yml | 4 +- .../google/gcp_logging_sink_modified.yml | 2 +- 5 files changed, 384 insertions(+), 20 deletions(-) create mode 100644 filters/audits/gcp.md create mode 100644 plugins/alerts/testdata/filter-contracts/gcp.json diff --git a/filters/audits/gcp.md b/filters/audits/gcp.md new file mode 100644 index 000000000..5efdfec96 --- /dev/null +++ b/filters/audits/gcp.md @@ -0,0 +1,40 @@ +# Google Cloud normalization and rule review + +Normalize outcome/severity/address values and repair IAM/sink rule CEL functions and grouping. + +This draft targets UTMStack `v11`. It contains 1 filter changes +and 2 rule changes for this technology only. Review covered +1 filter configurations and 43 matching shipped rule files. +Unchanged rules are listed in the regression manifest; they are not duplicated in the diff. + +## Contract and validation + +- Compared exact standard names/types with go-sdk v1.1.31 and the supplied UTMStack dictionaries. +- Checked documented pipeline ordering, rename/move behavior, open vendor log fields, + event-side versus alert-side fields, and surviving fields used by affected rule predicates/history/grouping. +- Strict SDK configuration decoding and actual CEL compilation pass for this scope. +- 13 synthetic normalization cases pass, including SDK Event conversion and any + trigger predicate assertions recorded in the manifest. +- The scoped alerts module tests and `git diff --check` pass with the shared contract runner applied. + +The shared alert-contract PR supplies the reusable Go runner for the manifest in +`plugins/alerts/testdata/filter-contracts/gcp.json`. Apply that support before running `go test ./...` in `plugins/alerts`. + +The model starts from synthetic extraction results. It does not run complex grok, +JSON/KV/XML/CSV extraction, time conversion, dynamic plugins, historical OpenSearch +queries, or the closed EventProcessor. Raw vendor logs and resulting alerts must +still be checked in staging before rollout. No customer false-positive reduction +has been measured and no production rollout is included. + + + +## References + +- [SDK schema](https://github.com/threatwinds/go-sdk/blob/v1.1.31/plugins/plugins.proto) +- [Filter steps](https://github.com/threatwinds/go-sdk/wiki/Filter-Steps-Reference) +- [Standard event schema](https://github.com/threatwinds/go-sdk/wiki/Standard-Event-Schema) +- [Rule implementation](https://github.com/threatwinds/go-sdk/wiki/Implementing-Rules) + +`afterEvents`, empty noncapturing grok names, supported numeric strings, and custom +`log.*` fields are accepted. Existing textual protocol casing and vendor action names +are preserved unless a concrete consumer mismatch requires correction. diff --git a/filters/google/gcp.yml b/filters/google/gcp.yml index 46b69fb9b..222ac9ac0 100644 --- a/filters/google/gcp.yml +++ b/filters/google/gcp.yml @@ -380,24 +380,24 @@ pipeline: # Adding severity field based on log.severity - add: - function: "string" + function: string params: key: severity - value: "low" + value: info where: oneOf("log.severity", ["INFO", "NOTICE"]) - add: - function: "string" + function: string params: key: severity - value: "medium" + value: warning where: equals("log.severity", "WARNING") - add: - function: "string" + function: string params: key: severity - value: "high" + value: error where: equals("log.severity", "ERROR") # Adding action field based on log.httpRequestMethod @@ -445,10 +445,10 @@ pipeline: # Adding actionResult field based on log.jsonPayloadEnforcedEdgeSecurityPolicyOutcome - add: - function: "string" + function: string params: key: actionResult - value: "accepted" + value: success where: equals("log.jsonPayloadEnforcedEdgeSecurityPolicyOutcome", "ACCEPT") - add: @@ -480,11 +480,11 @@ pipeline: where: 'exists("log.protoPayloadMethodName") && equals("log.protoPayloadStatusCode", 0)' - add: - function: "string" + function: string params: key: actionResult - value: "failed" - where: 'exists("log.protoPayloadMethodName") && greaterThan("log.protoPayloadStatusCode", 0)' + value: failure + where: exists("log.protoPayloadMethodName") && greaterThan("log.protoPayloadStatusCode", 0) # Adding actionResult for Cloud DNS query logs (dns.googleapis.com). - add: @@ -502,10 +502,10 @@ pipeline: where: '!exists("actionResult") && equals("log.jsonPayloadResponseCode", "REFUSED")' - add: - function: "string" + function: string params: key: actionResult - value: "failed" + value: failure where: '!exists("actionResult") && exists("log.jsonPayloadResponseCode")' # Adding actionResult for plain HTTP request logs (Cloud Run, Load @@ -525,11 +525,11 @@ pipeline: where: 'exists("statusCode") && !exists("actionResult") && oneOf("statusCode", [401, 403])' - add: - function: "string" + function: string params: key: actionResult - value: "failed" - where: 'exists("statusCode") && !exists("actionResult") && greaterOrEqual("statusCode", 400)' + value: failure + where: exists("statusCode") && !exists("actionResult") && greaterOrEqual("statusCode", 400) # Adding geolocation to origin.ip - dynamic: @@ -547,6 +547,20 @@ pipeline: destination: target.geolocation where: exists("target.ip") + # Normalize the source event severity. + - add: + function: string + params: + key: severity + value: critical + where: oneOf("log.severity",["EMERGENCY","ALERT","CRITICAL"]) + - add: + function: string + params: + key: severity + value: debug + where: equals("log.severity","DEBUG") + # Removing unused fields - delete: fields: @@ -556,4 +570,69 @@ pipeline: - log.httpRequestMethod - log.jsonPayloadEnforcedEdgeSecurityPolicyOutcome - log.severity - - log.jsonPayloadStructuredRdata \ No newline at end of file + - log.jsonPayloadStructuredRdata + + # Keep addresses in IP fields and retain other source values under log. + - rename: + from: + - origin.ip + to: log.unparsedOriginIp + where: exists("origin.ip") && (!(inCIDR("origin.ip","0.0.0.0/0") || inCIDR("origin.ip","::/0")) || oneOf("origin.ip",["0.0.0.0","::"])) + - rename: + from: + - target.ip + to: log.unparsedTargetIp + where: exists("target.ip") && (!(inCIDR("target.ip","0.0.0.0/0") || inCIDR("target.ip","::/0")) || oneOf("target.ip",["0.0.0.0","::"])) + - add: + function: string + params: + key: protocol + value: ICMP + where: equals("protocol",1) + - add: + function: string + params: + key: protocol + value: TCP + where: equals("protocol",6) + - add: + function: string + params: + key: protocol + value: UDP + where: equals("protocol",17) + - add: + function: string + params: + key: protocol + value: GRE + where: equals("protocol",47) + - add: + function: string + params: + key: protocol + value: ESP + where: equals("protocol",50) + - add: + function: string + params: + key: protocol + value: AH + where: equals("protocol",51) + - add: + function: string + params: + key: protocol + value: ICMPV6 + where: equals("protocol",58) + - add: + function: string + params: + key: protocol + value: SCTP + where: equals("protocol",132) + - rename: + from: + - protocol + to: log.ipProtocolNumber + where: exists("protocol") && greaterOrEqual("protocol",0) diff --git a/plugins/alerts/testdata/filter-contracts/gcp.json b/plugins/alerts/testdata/filter-contracts/gcp.json new file mode 100644 index 000000000..372feeb3c --- /dev/null +++ b/plugins/alerts/testdata/filter-contracts/gcp.json @@ -0,0 +1,245 @@ +{ + "technology": "Google Cloud", + "filters": [ + "filters/google/gcp.yml" + ], + "rules": [ + "rules/cloud/google/anthos_security_events.yml", + "rules/cloud/google/binary_authorization_bypasses.yml", + "rules/cloud/google/cloud_identity_suspicious_signins.yml", + "rules/cloud/google/gcp_account_disabled_hijacked.yml", + "rules/cloud/google/gcp_account_disabled_password_leak.yml", + "rules/cloud/google/gcp_account_disabled_spamming.yml", + "rules/cloud/google/gcp_audit_config_changed.yml", + "rules/cloud/google/gcp_audit_log_disabling.yml", + "rules/cloud/google/gcp_bigquery_exfiltration.yml", + "rules/cloud/google/gcp_breakglass_container_deploy.yml", + "rules/cloud/google/gcp_cloud_function_abuse.yml", + "rules/cloud/google/gcp_cryptomining_detection.yml", + "rules/cloud/google/gcp_custom_role_creation.yml", + "rules/cloud/google/gcp_defense_evasion_logging_sink_deletion.yml", + "rules/cloud/google/gcp_dlp_reidentification.yml", + "rules/cloud/google/gcp_domain_api_access_granted.yml", + "rules/cloud/google/gcp_exfiltration_logging_sink_modification.yml", + "rules/cloud/google/gcp_firewall_open_ingress.yml", + "rules/cloud/google/gcp_firewall_rule_deleted.yml", + "rules/cloud/google/gcp_gcs_bucket_created.yml", + "rules/cloud/google/gcp_gcs_bucket_deleted.yml", + "rules/cloud/google/gcp_gcs_data_access.yml", + "rules/cloud/google/gcp_gov_attack.yml", + "rules/cloud/google/gcp_iam_policy_changed.yml", + "rules/cloud/google/gcp_impact_storage_bucket_deleted.yml", + "rules/cloud/google/gcp_kms_key_modifications.yml", + "rules/cloud/google/gcp_kubernetes_admission_controller.yml", + "rules/cloud/google/gcp_logging_sink_modified.yml", + "rules/cloud/google/gcp_packet_capture_abuse.yml", + "rules/cloud/google/gcp_privilege_escalation_kubernetes_rolebindings_created_or_patched.yml", + "rules/cloud/google/gcp_privileged_role_granted.yml", + "rules/cloud/google/gcp_probable_password_guess.yml", + "rules/cloud/google/gcp_project_manipulation.yml", + "rules/cloud/google/gcp_secret_manager_access.yml", + "rules/cloud/google/gcp_service_account_impersonation.yml", + "rules/cloud/google/gcp_storage_exfiltration.yml", + "rules/cloud/google/gcp_suspicious_login_blocked.yml", + "rules/cloud/google/gcp_suspicious_login_less_secure_app.yml", + "rules/cloud/google/gcp_suspicious_programmatic_login.yml", + "rules/cloud/google/gcp_two_step_verification_disabled.yml", + "rules/cloud/google/gcp_workload_identity_abuse.yml", + "rules/cloud/google/gcp_workspace_mfa_disabled.yml", + "rules/cloud/google/service_account_key_creation_spikes.yml" + ], + "fixtures": [ + { + "name": "GCP numeric protocol", + "filter": "google/gcp.yml", + "input": { + "log": { + "jsonPayload": { + "protocol": 6 + } + } + }, + "expected": { + "protocol": "TCP" + }, + "absent": [], + "rules": {} + }, + { + "name": "GCP unknown numeric protocol preserved", + "filter": "google/gcp.yml", + "input": { + "log": { + "jsonPayload": { + "protocol": 253 + } + } + }, + "expected": { + "log.ipProtocolNumber": 253 + }, + "absent": [ + "protocol" + ], + "rules": {} + }, + { + "name": "GCP severity EMERGENCY", + "filter": "google/gcp.yml", + "input": { + "log": { + "severity": "EMERGENCY" + } + }, + "expected": { + "severity": "critical" + }, + "absent": [] + }, + { + "name": "GCP severity ALERT", + "filter": "google/gcp.yml", + "input": { + "log": { + "severity": "ALERT" + } + }, + "expected": { + "severity": "critical" + }, + "absent": [] + }, + { + "name": "GCP severity CRITICAL", + "filter": "google/gcp.yml", + "input": { + "log": { + "severity": "CRITICAL" + } + }, + "expected": { + "severity": "critical" + }, + "absent": [] + }, + { + "name": "GCP severity ERROR", + "filter": "google/gcp.yml", + "input": { + "log": { + "severity": "ERROR" + } + }, + "expected": { + "severity": "error" + }, + "absent": [] + }, + { + "name": "GCP severity WARNING", + "filter": "google/gcp.yml", + "input": { + "log": { + "severity": "WARNING" + } + }, + "expected": { + "severity": "warning" + }, + "absent": [] + }, + { + "name": "GCP severity NOTICE", + "filter": "google/gcp.yml", + "input": { + "log": { + "severity": "NOTICE" + } + }, + "expected": { + "severity": "info" + }, + "absent": [] + }, + { + "name": "GCP severity INFO", + "filter": "google/gcp.yml", + "input": { + "log": { + "severity": "INFO" + } + }, + "expected": { + "severity": "info" + }, + "absent": [] + }, + { + "name": "GCP severity DEBUG", + "filter": "google/gcp.yml", + "input": { + "log": { + "severity": "DEBUG" + } + }, + "expected": { + "severity": "debug" + }, + "absent": [] + }, + { + "name": "GCP audit status 0", + "filter": "google/gcp.yml", + "input": { + "log": { + "protoPayload": { + "methodName": "DeleteBucket", + "status": { + "code": 0 + } + } + } + }, + "expected": { + "actionResult": "success" + }, + "absent": [] + }, + { + "name": "GCP audit status 5", + "filter": "google/gcp.yml", + "input": { + "log": { + "protoPayload": { + "methodName": "DeleteBucket", + "status": { + "code": 5 + } + } + } + }, + "expected": { + "actionResult": "failure" + }, + "absent": [] + }, + { + "name": "GCP audit status 7", + "filter": "google/gcp.yml", + "input": { + "log": { + "protoPayload": { + "methodName": "DeleteBucket", + "status": { + "code": 7 + } + } + } + }, + "expected": { + "actionResult": "failure" + }, + "absent": [] + } + ] +} diff --git a/rules/cloud/google/gcp_iam_policy_changed.yml b/rules/cloud/google/gcp_iam_policy_changed.yml index 4916be7de..172b5e091 100644 --- a/rules/cloud/google/gcp_iam_policy_changed.yml +++ b/rules/cloud/google/gcp_iam_policy_changed.yml @@ -25,6 +25,6 @@ description: | 5. Review the actor's session for other privilege escalation attempts 6. Check if the service account's workload identity was compromised where: | - oneof("log.protoPayloadServiceName", ["cloudresourcemanager.googleapis.com", "pubsub.googleapis.com"]) && - oneof("log.protoPayloadMethodName", ["SetIamPolicy", "google.iam.v1.IAMPolicy.SetIamPolicy"]) && + oneOf("log.protoPayloadServiceName", ["cloudresourcemanager.googleapis.com", "pubsub.googleapis.com"]) && + oneOf("log.protoPayloadMethodName", ["SetIamPolicy", "google.iam.v1.IAMPolicy.SetIamPolicy"]) && exists("log.protoPayload.request.policy.bindings") && contains("log.logName", "activity") && exists("origin.user") diff --git a/rules/cloud/google/gcp_logging_sink_modified.yml b/rules/cloud/google/gcp_logging_sink_modified.yml index 5c797bd1b..cdcbbb1cc 100644 --- a/rules/cloud/google/gcp_logging_sink_modified.yml +++ b/rules/cloud/google/gcp_logging_sink_modified.yml @@ -26,5 +26,5 @@ description: | 6. Review Cloud Audit logs for other logging configuration changes where: | equals("log.protoPayloadServiceName", "logging.googleapis.com") && - oneof("log.protoPayloadMethodName", ["google.logging.v2.ConfigServiceV2.CreateSink", "google.logging.v2.ConfigServiceV2.DeleteSink", "google.logging.v2.ConfigServiceV2.UpdateSink"]) && + oneOf("log.protoPayloadMethodName", ["google.logging.v2.ConfigServiceV2.CreateSink", "google.logging.v2.ConfigServiceV2.DeleteSink", "google.logging.v2.ConfigServiceV2.UpdateSink"]) && exists("origin.user")