Detection rules › Panther
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 | T1537 Transfer Data to Cloud Account |
Rule body yaml
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
Tests:
- Name: Cross-Project Object Copy
ExpectedResult: true
Log:
{
"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",
}
- Name: Same-Project Different Bucket Copy
ExpectedResult: false
Log:
{
"protoPayload":
{
"at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
"authenticationInfo":
{ "principalEmail": "denethor@lotr.com" },
"metadata":
{
"destination": "projects/_/buckets/backup-bucket/objects/file.txt",
"requested_bytes": 100,
},
"methodName": "storage.objects.get",
"resourceName": "projects/_/buckets/prod-bucket/objects/file.txt",
"serviceName": "storage.googleapis.com",
"status": {},
},
"resource":
{
"labels":
{
"bucket_name": "prod-bucket",
"project_id": "test-project",
},
"type": "gcs_bucket",
},
"severity": "INFO",
"timestamp": "2025-12-15 15:55:03.915792086",
}
- Name: Object Get Without Copy Operation
ExpectedResult: false
Log:
{
"protoPayload":
{
"at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
"authenticationInfo":
{ "principalEmail": "user@example.com" },
"methodName": "storage.objects.get",
"resourceName": "projects/_/buckets/test-bucket/objects/file.txt",
"serviceName": "storage.googleapis.com",
"status": {},
},
"resource":
{
"labels":
{
"bucket_name": "test-bucket",
"project_id": "test-project",
},
"type": "gcs_bucket",
},
"severity": "INFO",
"timestamp": "2025-12-15 15:55:03.915792086",
}
Detection logic
Condition
not (protoPayload.methodName ne "storage.objects.get" or protoPayload.serviceName ne "storage.googleapis.com" or severity eq "ERROR" or protoPayload.metadata.destination is_null)
resource.labels.bucket_name is_not_null
resource.labels.project_id is_not_null
This rule also runs imperative logic the parser cannot express as a filter; the conditions above are the structured part it could extract.
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
protoPayload.metadata.destination | is_null | |
protoPayload.methodName | ne | storage.objects.get |
protoPayload.serviceName | ne | storage.googleapis.com |
severity | eq | ERROR |
Indicators
Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.
| Field | Kind | Values |
|---|---|---|
resource.labels.bucket_name | is_not_null | |
resource.labels.project_id | is_not_null |
Output fields
Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.
| Field | Source |
|---|---|
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 |