Detection rules › Panther
Panther rules: gsuite
Gmail Malicious SMTP Response
#Detects when Gmail blocks or rejects emails due to malicious SMTP response reasons including malware detection, spam/phishing links, low sender reputation, RBL listings, or denial of service attempts. This rule monitors inbound SMTP connections for security threats that Gmail's filters identify.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | any: Gmail (any event) |
Rules detecting the same action
These rules filter on the same operation.
- Gmail Potential Spoofed Email Delivered (Panther)
- gmail spike in undeliverables (YARA-L)
- Gsuite Attachments Downloaded from Spam Email (Panther)
- Gsuite Email Bypassed Spam Filter (Panther)
- Gsuite Link Clicked in Spam Email (Panther)
- Malware Detected in Email (Panther)
- Spam Email Surge (Panther)
Detection logic
from panther_gsuite_helpers import gsuite_activityevent_alert_context
# SMTP response reasons that indicate security threats
MALICIOUS_SMTP_RESPONSES = {
3: "Malware",
13: "Blatant Spam",
14: "Denial of Service",
15: "Malicious or Spam Links",
16: "Low IP Reputation",
17: "Low Domain Reputation",
18: "IP address listed in public real-time block list",
}
def rule(event):
if event.deep_get("id", "applicationName", default="<UNKNOWN_APPLICATION>") != "gmail":
return False
smtp_response_reason = event.deep_get(
"parameters", "message_info", "connection_info", "smtp_response_reason", default=0
)
return smtp_response_reason in MALICIOUS_SMTP_RESPONSES
def title(event):
user = event.deep_get("actor", "email", default="<UNKNOWN_USER>")
smtp_response_reason = event.deep_get(
"parameters", "message_info", "connection_info", "smtp_response_reason", default=0
)
reason_description = MALICIOUS_SMTP_RESPONSES.get(
smtp_response_reason, f"Unknown ({smtp_response_reason})"
)
sender = event.deep_get(
"parameters", "message_info", "source", "address", default="<UNKNOWN_SENDER>"
)
return f"Gmail blocked email to [{user}] from [{sender}] due to: {reason_description}"
def alert_context(event):
context = gsuite_activityevent_alert_context(event)
# Add specific SMTP response information
smtp_response_reason = event.deep_get(
"parameters", "message_info", "connection_info", "smtp_response_reason", default=0
)
context.update(
{
"smtp_response_reason_code": smtp_response_reason,
"smtp_response_reason": MALICIOUS_SMTP_RESPONSES.get(
smtp_response_reason, f"Unknown ({smtp_response_reason})"
),
"smtp_reply_code": event.deep_get(
"parameters", "message_info", "connection_info", "smtp_reply_code", default=0
),
}
)
return context
Rule specification
AnalysisType: rule
Filename: gsuite_malicious_smtp_response.py
RuleID: "GSuite.Gmail.Malicious.SMTP.Response"
DisplayName: "Gmail Malicious SMTP Response"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
- Gmail
- Email Security
- Malware
- Spam
Reports:
MITRE ATT&CK:
- TA0001:T1566 # Initial Access: Phishing
Severity: High
Description: >
Detects when Gmail blocks or rejects emails due to malicious SMTP response reasons including
malware detection, spam/phishing links, low sender reputation, RBL listings, or denial of service attempts.
This rule monitors inbound SMTP connections for security threats that Gmail's filters identify.
Reference: https://support.google.com/a/answer/12384955
Runbook: |
1. Review the sender's email address and domain
2. Check the SMTP response reason and reply code for details
3. Investigate the sender's IP address for additional context (geolocation, reputation)
4. Review authentication status (SPF, DKIM, DMARC)
5. If malware was detected, check if similar messages were received by other users
6. Consider adding sender to blocklist if pattern of malicious activity is confirmed
7. For DoS attempts, review firewall and rate limiting configurations
DedupPeriodMinutes: 60
SummaryAttributes:
- user_email
- sender_address
- smtp_response_reason
Stages and Predicates
Fires on GSuite.ActivityEvent events when the condition below holds.
Condition
id.applicationNameisgmail
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 |
|---|---|---|---|
id.applicationName | eq |
| field:"id.applicationName" kind:eq value:"gmail" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
actor | actor.email |
applicationName | id.applicationName |
name | |
type | |
parameters | |
address | parameters.message_info.source.address |
Response runbook
1. Review the sender's email address and domain
2. Check the SMTP response reason and reply code for details
3. Investigate the sender's IP address for additional context (geolocation, reputation)
4. Review authentication status (SPF, DKIM, DMARC)
5. If malware was detected, check if similar messages were received by other users
6. Consider adding sender to blocklist if pattern of malicious activity is confirmed
7. For DoS attempts, review firewall and rate limiting configurations
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"callerType": "USER",
"email": "frodo@lotr.com",
"profileId": "123456789"
},
"id": {
"applicationName": "gmail",
"customerId": "C01abc123",
"time": "2025-11-04 20:44:43.248000000",
"uniqueQualifier": "-123456789"
},
"ipAddress": "1.1.1.1",
"kind": "admin#reports#activity",
"name": "delivery",
"p_any_ip_addresses": [
"1.1.1.1"
],
"p_event_time": "2025-11-04 20:44:43.248000000",
"p_log_type": "GSuite.ActivityEvent",
"parameters": {
"event_info": {
"elapsed_time_usec": 368746,
"success": false,
"timestamp_usec": 1730751883248347
},
"message_info": {
"action_type": 2,
"connection_info": {
"client_ip": "1.1.1.1",
"dkim_pass": false,
"dmarc_pass": false,
"ip_geo_country": "XX",
"is_internal": false,
"smtp_reply_code": 550,
"smtp_response_reason": 3,
"spf_pass": false
},
"destination": [
{
"address": "frodo@lotr.com",
"service": "gmail-ui"
}
],
"source": {
"address": "eve@lexcorp.com",
"from_header_address": "eve@lexcorp.com"
},
"subject": "Invoice Attached - Please Review"
}
},
"type": "message_delivery"
}
Gmail Potential Spoofed Email Delivered
#Detects when a potentially spoofed email was successfully delivered to a user's inbox despite failing email authentication checks. This rule triggers when: 1. DMARC authentication fails, OR 2. Both SPF and DKIM authentication fail simultaneously These authentication failures indicate the sender may be impersonating a legitimate domain, which is a common tactic in phishing and business email compromise (BEC) attacks.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | any: Gmail (any event) |
Rules detecting the same action
These rules filter on the same operation.
- Gmail Malicious SMTP Response (Panther)
- gmail spike in undeliverables (YARA-L)
- Gsuite Attachments Downloaded from Spam Email (Panther)
- Gsuite Email Bypassed Spam Filter (Panther)
- Gsuite Link Clicked in Spam Email (Panther)
- Malware Detected in Email (Panther)
- Spam Email Surge (Panther)
Detection logic
from panther_gsuite_helpers import gsuite_activityevent_alert_context
def rule(event):
if event.deep_get("id", "applicationName", default="<UNKNOWN_APPLICATION>") != "gmail":
return False
dmarc_passed = event.deep_get(
"parameters",
"message_info",
"connection_info",
"dmarc_pass",
default="<UNKNOWN_DMARC_PASS>",
)
spf_passed = event.deep_get(
"parameters", "message_info", "connection_info", "spf_pass", default="<UNKNOWN_SPF_PASS>"
)
dkim_passed = event.deep_get(
"parameters",
"message_info",
"connection_info",
"dkim_pass",
default="<UNKNOWN_DKIM_PASS>",
)
event_success = event.deep_get("parameters", "event_info", "success", default=False)
if event_success is True: # Message was delivered despite failures
if dmarc_passed is False:
return True
if spf_passed is False and dkim_passed is False:
return True
return False
def title(event):
user = event.deep_get("actor", "email", default="<UNKNOWN_USER>")
return f"[{user}] received a potentially spoofed email"
def alert_context(event):
context = gsuite_activityevent_alert_context(event)
return context
Rule specification
AnalysisType: rule
Filename: gsuite_potential_spoofed_email.py
RuleID: "GSuite.Gmail.Potential.Spoofed.Email"
DisplayName: "Gmail Potential Spoofed Email Delivered"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
- Gmail
- Email Security
- Spoofing
- Phishing
Reports:
MITRE ATT&CK:
- TA0001:T1566.001 # Initial Access: Phishing - Spearphishing Attachment
- TA0001:T1566.002 # Initial Access: Phishing - Spearphishing Link
Severity: High
Description: >
Detects when a potentially spoofed email was successfully delivered to a user's inbox despite
failing email authentication checks. This rule triggers when:
1. DMARC authentication fails, OR
2. Both SPF and DKIM authentication fail simultaneously
These authentication failures indicate the sender may be impersonating a legitimate domain,
which is a common tactic in phishing and business email compromise (BEC) attacks.
Reference: https://support.google.com/a/answer/12384955
Runbook: |
1. Review the sender's email address and compare with the From: header display name
2. Check if the sender domain is impersonating an internal or partner domain
3. Verify the authentication status details (SPF, DKIM, DMARC)
4. Review the message subject and content if available
5. Check the sender's IP geolocation and reputation
6. Search for similar messages from the same sender to other users
7. If confirmed as spoofing:
- Add sender domain/IP to blocklist
- Remove the message from user's inbox
- Notify affected users not to interact with the email
8. Consider strengthening DMARC policy (quarantine/reject) if not already enforced
DedupPeriodMinutes: 60
SummaryAttributes:
- user_email
- sender_address
- dmarc_pass
- spf_pass
- dkim_pass
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameisgmailparameters.event_info.successistrueany of:
parameters.message_info.connection_info.dmarc_passisfalseall of:
parameters.message_info.connection_info.spf_passisfalseparameters.message_info.connection_info.dkim_passisfalse
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 |
|---|---|
actor | actor.email |
applicationName | id.applicationName |
name | |
type | |
parameters |
Response runbook
1. Review the sender's email address and compare with the From: header display name
2. Check if the sender domain is impersonating an internal or partner domain
3. Verify the authentication status details (SPF, DKIM, DMARC)
4. Review the message subject and content if available
5. Check the sender's IP geolocation and reputation
6. Search for similar messages from the same sender to other users
7. If confirmed as spoofing:
- Add sender domain/IP to blocklist
- Remove the message from user's inbox
- Notify affected users not to interact with the email
8. Consider strengthening DMARC policy (quarantine/reject) if not already enforced
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"callerType": "USER",
"email": "frodo@lotr.com",
"profileId": "123456789"
},
"id": {
"applicationName": "gmail",
"customerId": "C01abc123",
"time": "2025-11-05 10:15:30.000000000",
"uniqueQualifier": "-123456789"
},
"ipAddress": "8.8.8.8",
"kind": "admin#reports#activity",
"name": "delivery",
"p_any_ip_addresses": [
"8.8.8.8"
],
"p_event_time": "2025-11-05 10:15:30.000000000",
"p_log_type": "GSuite.ActivityEvent",
"parameters": {
"event_info": {
"elapsed_time_usec": 250000,
"success": true,
"timestamp_usec": 1730800530000000
},
"message_info": {
"action_type": 2,
"connection_info": {
"client_ip": "8.8.8.8",
"dkim_pass": true,
"dmarc_pass": false,
"dmarc_published_domain": "fake-company.com",
"ip_geo_country": "RU",
"is_internal": false,
"spf_pass": true
},
"destination": [
{
"address": "frodo@lotr.com",
"service": "gmail-ui"
}
],
"rfc2822_message_id": "<denethor@lotr.com>",
"source": {
"address": "john@justice.org",
"from_header_address": "john@justice.org",
"from_header_displayname": "CEO John Smith"
},
"subject": "Urgent: Wire Transfer Request"
}
},
"type": "message_delivery"
}
Google Accessed a GSuite Resource
#Google accessed one of your GSuite resources directly, most likely in response to a support incident.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | any: Access Transparency (any event) |
Detection logic
def rule(event):
if event.deep_get("id", "applicationName") != "access_transparency":
return False
return bool(event.get("type") == "GSUITE_RESOURCE")
Rule specification
AnalysisType: rule
Filename: gsuite_google_access.py
RuleID: "GSuite.GoogleAccess"
DisplayName: "Google Accessed a GSuite Resource"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
Severity: Low
Description: >
Google accessed one of your GSuite resources directly, most likely in response to a support incident.
Reference: https://support.google.com/a/answer/9230474?hl=en
Runbook: >
Your GSuite Super Admin can visit the Access Transparency report in the GSuite Admin Dashboard to see more details about the access.
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameisaccess_transparencytypeisGSUITE_RESOURCE
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
id.applicationName | eq |
| field:"id.applicationName" kind:eq value:"access_transparency" |
type | eq |
| field:"type" kind:eq value:"GSUITE_RESOURCE" |
Response runbook
Your GSuite Super Admin can visit the Access Transparency report in the GSuite Admin Dashboard to see more details about the access.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"id": {
"applicationName": "access_transparency"
},
"type": "GSUITE_RESOURCE"
}
Google Drive High Download Count
#Scheduled rule for the High Google Drive Download Count query which looks for incidents of more than 10 (tunable) downloads by a user in the past day.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | download: Download |
Rules detecting the same action
These rules filter on the same operation.
- GSuite Many Docs Downloaded Query (Panther)
Detection logic
def rule(_):
return True
def title(event):
return (
f"GSuite: [{event.get('user', '<user_not_found>')}] "
f"downloaded [{event.get('download_count', '<count_not_found>')}] "
"files from Google Drive."
)
def alert_context(event):
return event.to_dict()
Rule specification
AnalysisType: scheduled_rule
Description: Scheduled rule for the High Google Drive Download Count query which looks for incidents of more than 10 (tunable) downloads by a user in the past day.
DisplayName: "Google Drive High Download Count"
Status: Deprecated
Enabled: false
Filename: gsuite_drive_many_docs_downloaded.py
Reference: https://support.google.com/drive/answer/2423534?hl=en&co=GENIE.Platform%3DDesktop
Severity: Medium
Tags:
- Deprecated
DedupPeriodMinutes: 60
RuleID: "Google.Drive.High.Download.Count"
Threshold: 1
ScheduledQueries:
- GSuite Many Docs Downloaded Query
Stages and Predicates
Rule logic
This rule alerts on rows returned by its scheduled query GSuite Many Docs Downloaded Query; its Python module (Detection logic above) shapes the alert rather than filtering.
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
user |
download_count |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"download_count": 23,
"downloaded_files": [
"all_hands01.mov",
"all_hands02.mov",
"all_hands03.mov",
"all_hands23.mov"
],
"user": "homer.simpson@simpsons.com"
}
Google Workspace Login Type Anomaly
#Detects users authenticating with login types they haven't used in the past 30 days. May indicate GAIA credential theft where attackers use stolen tokens with different authentication methods than the victim's normal pattern (e.g., google_password instead of SAML).
Rule specification
AnalysisType: scheduled_query
QueryName: "Google Workspace Login Type Anomaly"
Description: |
Detects users authenticating with login types they haven't used in the past 30 days.
May indicate GAIA credential theft where attackers use stolen tokens with different
authentication methods than the victim's normal pattern (e.g., google_password instead of SAML).
Enabled: false
Query: |
-- pragma: template
{% import 'anomalies' new_unique_values %}
WITH subquery AS (
SELECT
actor:email AS email,
parameters:login_type AS login_type,
ipAddress,
p_event_time
FROM panther_logs.public.gsuite_activityevent
WHERE p_occurs_since('30 days')
AND id:applicationName = 'login'
AND name = 'login_success'
AND parameters:login_type IS NOT NULL
AND parameters:login_type != 'reauth'
),
{{ new_unique_values('subquery', 'email', 'login_type', '1 day') }}
Schedule:
RateMinutes: 360
TimeoutMinutes: 3
Google Workspace Login Type Anomaly
#Alerts when users authenticate with login types they haven't used in the past 30 days. This may indicate GAIA credential theft where attackers use stolen OAuth tokens with different authentication methods (e.g., google_password instead of SAML). Particularly suspicious when a user who normally uses SSO/SAML suddenly authenticates via password.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence | |
| Privilege Escalation | |
| Stealth | |
| Lateral Movement |
Detection logic
import re
def normalize_username(email):
if not email:
return None
# Extract username before @ symbol
username = email.split("@")[0] if "@" in email else email
# Remove all non-alphanumeric characters and convert to lowercase
return re.sub(r"[^a-z0-9]", "", username.lower())
def rule(_):
return True
def title(event):
user = event.get("email", "<UNKNOWN_USER>")
login_type = event.get("login_type", "<UNKNOWN_TYPE>")
return f"Google Workspace: User [{user}] used anomalous login type [{login_type}]"
def severity(event):
login_type = event.get("login_type", "")
# Higher severity for password-based auth
if login_type == "google_password":
return "HIGH"
return "MEDIUM"
def alert_context(event):
email = event.get("email")
return {
"user_email": email,
"username_normalized": normalize_username(email),
"anomalous_login_type": event.get("login_type"),
"ip_address": event.get("ipAddress"),
"description": ("User authenticated with a login type not seen in the previous 30 days"),
}
Rule specification
AnalysisType: scheduled_rule
DisplayName: "Google Workspace Login Type Anomaly"
DedupPeriodMinutes: 360
RuleID: "Google.Workspace.Login.Type.Anomaly"
Description: >
Alerts when users authenticate with login types they haven't used in the past 30 days.
This may indicate GAIA credential theft where attackers use stolen OAuth tokens with
different authentication methods (e.g., google_password instead of SAML). Particularly
suspicious when a user who normally uses SSO/SAML suddenly authenticates via password.
ScheduledQueries:
- Google Workspace Login Type Anomaly
Enabled: false
Filename: gsuite_login_type_anomaly_rule.py
Reference: https://businessinsights.bitdefender.com/the-chain-reaction-new-methods-for-extending-local-breaches-in-google-workspace
Runbook: |
1. Query GSuite.ActivityEvent for all login events by the user email in the 24 hours before and after the alert to identify login patterns, IP addresses used, and the context around the anomalous login_type
2. Check if the source IP addresses from recent logins are associated with known VPN services, cloud providers, or corporate network ranges, and compare to the user's typical login locations in the past 30 days
3. Search for other authentication anomalies or alerts for this user in the past 7 days, including failed logins, OAuth token authorizations, password changes, or suspicious activity warnings
Severity: Medium
Tags:
- GSuite
- Lateral Movement
- Valid Accounts
- GAIA
Reports:
MITRE ATT&CK:
- TA0008:T1078.004
- TA0006:T1550
SummaryAttributes:
- email
Stages and Predicates
Rule logic
This rule alerts on rows returned by its scheduled query Google Workspace Login Type Anomaly; its Python module (Detection logic above) shapes the alert rather than filtering.
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
user_email | email |
anomalous_login_type | login_type |
ip_address | ipAddress |
Response runbook
1. Query GSuite.ActivityEvent for all login events by the user email in the 24 hours before and after the alert to identify login patterns, IP addresses used, and the context around the anomalous login_type
2. Check if the source IP addresses from recent logins are associated with known VPN services, cloud providers, or corporate network ranges, and compare to the user's typical login locations in the past 30 days
3. Search for other authentication anomalies or alerts for this user in the past 7 days, including failed logins, OAuth token authorizations, password changes, or suspicious activity warnings
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"email": "user@example.com",
"ipAddress": "1.1.1.1",
"login_type": "google_password"
}
Google Workspace OAuth Anomalous Privileged Request
#Detects new OAuth applications authorized with privileged scopes in Google Workspace. Uses anomaly detection to identify users authorizing OAuth apps they haven't used in the past 7 days.
Rule specification
AnalysisType: scheduled_query
QueryName: "Google Workspace OAuth Anomalous Privileged Request"
Description: |
Detects new OAuth applications authorized with privileged scopes in Google Workspace.
Uses anomaly detection to identify users authorizing OAuth apps they haven't used in
the past 7 days.
Enabled: false
Query: |
-- pragma: template
{% import 'anomalies' new_unique_values %}
with subquery as (
SELECT
p_event_time,
actor:email AS actor_email,
parameters:client_id AS client_id,
parameters:app_name AS app_name,
parameters:scope AS scopes,
ipAddress,
id:applicationName AS application_name,
name AS event_name
FROM panther_logs.public.gsuite_activityevent
WHERE p_occurs_since('7 day')
AND id:applicationName = 'token'
AND name = 'authorize'
AND (
LOWER(ARRAY_TO_STRING(parameters:scope, ' ')) LIKE '%admin.directory.user%'
OR LOWER(ARRAY_TO_STRING(parameters:scope, ' ')) LIKE '%ediscovery%'
OR LOWER(ARRAY_TO_STRING(parameters:scope, ' ')) LIKE '%drive%'
OR LOWER(ARRAY_TO_STRING(parameters:scope, ' ')) LIKE '%cloud_search.query%'
)
LIMIT 10000
),
{{ new_unique_values('subquery', 'actor_email', 'client_id', '1d') }}
Schedule:
RateMinutes: 360
TimeoutMinutes: 3
Google Workspace OAuth Application Authorized with Privileged Scopes
#Detects when a user authorizes an OAuth application with privileged scopes in Google Workspace. Privileged scopes grant broad access to sensitive data and administrative functions.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence |
Detection logic
from panther_gsuite_helpers import gsuite_parameter_lookup
PRIVILEGED_SCOPES = [
"admin.directory.user",
"admin.directory.group",
"admin.directory.domain",
"ediscovery",
"vault",
"cloud_search.query",
]
def rule(event):
scopes = event.deep_get("parameters", "scope", default=[])
app_name = event.deep_get("id", "applicationName", default="")
event_name = event.get("name")
if app_name != "token" or event_name != "authorize":
return False
# Handle both list and string formats
if scopes and isinstance(scopes, str):
scopes = [scopes]
# Check if any scope matches privileged scopes
privileged_scopes_lower = [ps.lower() for ps in PRIVILEGED_SCOPES]
for scope_url in scopes:
# Extract the last part of the scope URL (e.g., "admin.directory.user" from full URL)
scope_name = scope_url.split("/")[-1].lower()
if scope_name in privileged_scopes_lower:
return True
return False
def title(event):
actor = event.deep_get("actor", "email", default="<UNKNOWN_USER>")
app_name = event.deep_get("parameters", "app_name", default="<UNKNOWN_APP>")
return (
f"Google Workspace: User [{actor}] authorized OAuth app [{app_name}] with privileged scopes"
)
def alert_context(event):
parameters = event.get("parameters", {})
return {
"actor": event.deep_get("actor", "email", default=""),
"app_name": gsuite_parameter_lookup(parameters, "app_name"),
"client_id": gsuite_parameter_lookup(parameters, "client_id"),
"client_type": gsuite_parameter_lookup(parameters, "client_type"),
"scopes": gsuite_parameter_lookup(parameters, "scope"),
"scope_data": gsuite_parameter_lookup(parameters, "scope_data"),
"event_type": event.get("name"),
"ip_address": event.get("ipAddress"),
}
Rule specification
AnalysisType: rule
RuleID: "Google.Workspace.OAuth.Privileged.Scopes"
DisplayName: "Google Workspace OAuth Application Authorized with Privileged Scopes"
Filename: gsuite_oauth_privileged_scopes.py
LogTypes:
- GSuite.ActivityEvent
Enabled: true
Severity: Info
DedupPeriodMinutes: 60
Status: Experimental
Reference: https://businessinsights.bitdefender.com/the-chain-reaction-new-methods-for-extending-local-breaches-in-google-workspace
Description: >
Detects when a user authorizes an OAuth application with privileged scopes in Google Workspace.
Privileged scopes grant broad access to sensitive data and administrative functions.
Runbook: |
1. Query GSuite.ActivityEvent logs for all OAuth token authorize events by actor:email in the 7 days before and after this alert to identify if this is part of a pattern of suspicious OAuth grants
2. Search for the parameters:client_id across all users in the organization to determine if other users also authorized the same application
3. Review audit logs for any actions taken using the OAuth token between the authorization timestamp and now, filtering by parameters:app_name and the authorized scopes
Tags:
- GSuite
- Initial Access
- Persistence
- Account Manipulation
Reports:
MITRE ATT&CK:
- TA0001:T1078.004
- TA0003:T1098
SummaryAttributes:
- actor:email
- p_any_ip_addresses
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameistokennameisauthorize
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 |
|---|---|---|---|
id.applicationName | ne | token | excludes:id.applicationName field:"id.applicationName" value:"token" |
name | ne | authorize | excludes:name field:"name" value:"authorize" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
id.applicationName | eq |
| field:"id.applicationName" kind:eq value:"token" |
name | eq |
| field:"name" kind:eq value:"authorize" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
actor | actor.email |
event_type | name |
ip_address | ipAddress |
app_name | parameters.app_name |
Response runbook
1. Query GSuite.ActivityEvent logs for all OAuth token authorize events by actor:email in the 7 days before and after this alert to identify if this is part of a pattern of suspicious OAuth grants
2. Search for the parameters:client_id across all users in the organization to determine if other users also authorized the same application
3. Review audit logs for any actions taken using the OAuth token between the authorization timestamp and now, filtering by parameters:app_name and the authorized scopes
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"email": "user@example.com",
"profileId": "123456789012345678901"
},
"id": {
"applicationName": "token",
"customerId": "C01234abc",
"time": "2024-01-15 10:30:00.000000000",
"uniqueQualifier": "987654321098765432"
},
"ipAddress": "192.0.2.1",
"kind": "admin#reports#activity",
"name": "authorize",
"parameters": {
"app_name": "SuspiciousThirdPartyApp",
"client_id": "123456789012-abcdefghijklmnopqrstuvwxyz123456.apps.googleusercontent.com",
"client_type": "WEB",
"scope": [
"https://www.googleapis.com/auth/admin.directory.user",
"https://www.googleapis.com/auth/ediscovery",
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/cloud_search.query"
]
},
"type": "auth"
}
Google Workspace OAuth Token Requests from New IP
#Alerts when users request OAuth tokens from IP addresses they haven't used in the past 30 days, with 3+ requests indicating active usage. This may indicate GAIA credential theft where attackers use stolen refresh tokens to request access tokens from their infrastructure.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Lateral Movement |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | OAuth Token audit event authorize: Application access authorized |
Rules detecting the same action
These rules filter on the same operation.
- First Time Seen Google Workspace OAuth Login from Third-Party Application (Elastic)
- Google Workspace Device Registration After OAuth from Suspicious ASN (Elastic)
- Google Workspace OAuth Token Requests from New IPs (Panther)
- Google Workspace Object Copied from External Drive with App Consent (Elastic)
- Google Workspace User Login with Unusual ASN (Elastic)
Detection logic
import re
def normalize_username(email):
if not email:
return None
# Extract username before @ symbol
username = email.split("@")[0] if "@" in email else email
# Remove all non-alphanumeric characters and convert to lowercase
return re.sub(r"[^a-z0-9]", "", username.lower())
def rule(_):
return True
def title(event):
user = event.get("user", "<UNKNOWN_USER>")
new_ip = event.get("new_ip", "<UNKNOWN_IP>")
request_count = event.get("request_count", 0)
return (
f"Google Workspace: User [{user}] made {request_count} OAuth token requests "
f"from new IP [{new_ip}]"
)
def alert_context(event):
user = event.get("user")
return {
"user": user,
"username_normalized": normalize_username(user),
"new_ip": event.get("new_ip"),
"request_count": event.get("request_count"),
"app_names": event.get("app_names"),
"client_ids": event.get("client_ids"),
"first_seen": event.get("first_seen"),
"last_seen": event.get("last_seen"),
"description": (
"User requested OAuth tokens from an IP address not seen in the past 30 days, "
"with multiple requests indicating active usage"
),
}
Rule specification
AnalysisType: scheduled_rule
DisplayName: "Google Workspace OAuth Token Requests from New IP"
DedupPeriodMinutes: 1440
RuleID: "Google.Workspace.OAuth.Token.New.IP"
Description: |
Alerts when users request OAuth tokens from IP addresses they haven't used in the past 30 days,
with 3+ requests indicating active usage. This may indicate GAIA credential theft where attackers
use stolen refresh tokens to request access tokens from their infrastructure.
ScheduledQueries:
- Google Workspace OAuth Token Requests from New IPs
Enabled: false
Filename: gsuite_oauth_token_new_ip_rule.py
Reference: https://businessinsights.bitdefender.com/the-chain-reaction-new-methods-for-extending-local-breaches-in-google-workspace
Runbook: |
1. Query GSuite.ActivityEvent for all OAuth token requests (applicationName: "token") by the user in the 24 hours before and after the alert to identify the full scope of token activity from the new IP address
2. Check if the new IP address is associated with cloud providers, VPN services, proxy networks, or residential ISPs, and compare its geographic location to the user's typical login locations in the past 30 days
3. Search for other authentication anomalies for this user in the past 7 days, including login type changes, rapid multi-IP authentication, password changes, or device compromise warnings
Severity: Medium
Tags:
- GSuite
- Initial Access
- Valid Accounts
- GAIA
- Credential Theft
- OAuth
Reports:
MITRE ATT&CK:
- TA0001:T1078.004
- TA0006:T1550
SummaryAttributes:
- user
- new_ip
- app_names
Stages and Predicates
Rule logic
This rule alerts on rows returned by its scheduled query Google Workspace OAuth Token Requests from New IPs; its Python module (Detection logic above) shapes the alert rather than filtering.
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
user |
new_ip |
request_count |
app_names |
client_ids |
first_seen |
last_seen |
Response runbook
1. Query GSuite.ActivityEvent for all OAuth token requests (applicationName: "token") by the user in the 24 hours before and after the alert to identify the full scope of token activity from the new IP address
2. Check if the new IP address is associated with cloud providers, VPN services, proxy networks, or residential ISPs, and compare its geographic location to the user's typical login locations in the past 30 days
3. Search for other authentication anomalies for this user in the past 7 days, including login type changes, rapid multi-IP authentication, password changes, or device compromise warnings
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"app_names": [
"Google Chrome"
],
"client_ids": [
"12345.apps.googleusercontent.com"
],
"first_seen": "2024-01-15 10:00:00.000",
"last_seen": "2024-01-15 10:05:00.000",
"new_ip": "1.2.3.4",
"request_count": 5,
"user": "user@example.com"
}
Google Workspace OAuth Token Requests from New IPs
#Detects users requesting OAuth tokens from IPv4 addresses they haven't used in the past 30 days, with 3+ requests indicating active usage. May indicate GAIA credential theft where attackers use stolen refresh tokens from their infrastructure.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | OAuth Token audit event authorize: Application access authorized |
Rules detecting the same action
These rules filter on the same operation.
- First Time Seen Google Workspace OAuth Login from Third-Party Application (Elastic)
- Google Workspace Device Registration After OAuth from Suspicious ASN (Elastic)
- Google Workspace OAuth Token Requests from New IP (Panther)
- Google Workspace Object Copied from External Drive with App Consent (Elastic)
- Google Workspace User Login with Unusual ASN (Elastic)
Rule specification
AnalysisType: scheduled_query
QueryName: "Google Workspace OAuth Token Requests from New IPs"
Description: |
Detects users requesting OAuth tokens from IPv4 addresses they haven't used in the past 30 days,
with 3+ requests indicating active usage. May indicate GAIA credential theft where attackers use
stolen refresh tokens from their infrastructure.
Enabled: false
SnowflakeQuery: |
WITH baseline_ips AS (
SELECT
actor:email AS user,
ARRAY_AGG(DISTINCT ipAddress) AS normal_ips
FROM panther_logs.public.gsuite_activityevent
WHERE p_event_time >= DATEADD(day, -30, CURRENT_TIMESTAMP)
AND p_event_time < DATEADD(day, -1, CURRENT_TIMESTAMP)
AND id:applicationName = 'token'
AND name = 'authorize'
AND ipAddress NOT LIKE '%:%'
GROUP BY actor:email
),
recent_tokens AS (
SELECT
actor:email AS user,
ipAddress,
parameters:app_name AS app_name,
parameters:client_id AS client_id,
p_event_time
FROM panther_logs.public.gsuite_activityevent
WHERE p_occurs_since('1 day')
AND id:applicationName = 'token'
AND name = 'authorize'
AND ipAddress NOT LIKE '%:%'
)
SELECT
r.user,
r.ipAddress AS new_ip,
COUNT(*) AS request_count,
ARRAY_UNIQUE_AGG(r.app_name) AS app_names,
ARRAY_UNIQUE_AGG(r.client_id) AS client_ids,
MIN(r.p_event_time) AS first_seen,
MAX(r.p_event_time) AS last_seen
FROM recent_tokens r
LEFT JOIN baseline_ips b ON r.user = b.user
WHERE NOT ARRAY_CONTAINS(r.ipAddress::VARIANT, COALESCE(b.normal_ips, ARRAY_CONSTRUCT()))
GROUP BY r.user, r.ipAddress
HAVING COUNT(*) >= 3
ORDER BY request_count DESC
DatabricksQuery: |
WITH baseline_ips AS (
SELECT
actor:email AS user,
COLLECT_SET(ipAddress) AS normal_ips
FROM panther_logs.gsuite_activityevent
WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL 30 DAYS
AND p_event_time < CURRENT_TIMESTAMP - INTERVAL 1 DAY
AND id:applicationName = 'token'
AND name = 'authorize'
AND ipAddress NOT LIKE '%:%'
GROUP BY actor:email
),
recent_tokens AS (
SELECT
actor:email AS user,
ipAddress,
parameters:app_name AS app_name,
parameters:client_id AS client_id,
p_event_time
FROM panther_logs.gsuite_activityevent
WHERE p_occurs_since('1 day')
AND id:applicationName = 'token'
AND name = 'authorize'
AND ipAddress NOT LIKE '%:%'
)
SELECT
r.user,
r.ipAddress AS new_ip,
COUNT(*) AS request_count,
COLLECT_SET(r.app_name) AS app_names,
COLLECT_SET(r.client_id) AS client_ids,
MIN(r.p_event_time) AS first_seen,
MAX(r.p_event_time) AS last_seen
FROM recent_tokens r
LEFT JOIN baseline_ips b ON r.user = b.user
WHERE NOT ARRAY_CONTAINS(COALESCE(b.normal_ips, ARRAY()), r.ipAddress)
GROUP BY r.user, r.ipAddress
HAVING COUNT(*) >= 3
ORDER BY request_count DESC
Schedule:
RateMinutes: 1440
TimeoutMinutes: 5
Stages and Predicates
Stage 1: source
Stage 2: filter
Stage 3: having
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
r.user | |
new_ip | r.ipAddress |
request_count | COUNT ( * ) |
app_names | ARRAY_UNIQUE_AGG ( r.app_name ) |
client_ids | ARRAY_UNIQUE_AGG ( r.client_id ) |
first_seen | MIN ( r.p_event_time ) |
last_seen | MAX ( r.p_event_time ) |
Google Workspace OAuthLogin Scope Anomalous Application Access
#Detects apps requesting OAuth tokens with the OAuthLogin scope when they haven't requested this scope in the previous 14 days. This scope can be used to access Google's device password escrow endpoint for GAIA credential theft.
Rule specification
AnalysisType: scheduled_query
QueryName: "Google Workspace OAuthLogin Scope Anomalous Application Access"
Description: |
Detects apps requesting OAuth tokens with the OAuthLogin scope when they haven't requested
this scope in the previous 14 days. This scope can be used to access Google's device password
escrow endpoint for GAIA credential theft.
Enabled: false
Query: |
-- pragma: template
{% import 'anomalies' new_unique_values %}
WITH subquery AS (
SELECT
parameters:app_name AS app_name,
parameters:client_id AS client_id,
actor:email AS user_email,
ipAddress,
p_event_time
FROM panther_logs.public.gsuite_activityevent
WHERE p_occurs_since('14 days')
AND id:applicationName = 'token'
AND name = 'authorize'
AND ARRAY_TO_STRING(parameters:scope, ',') ILIKE '%accounts/OAuthLogin%'
),
{{ new_unique_values('subquery', 'app_name', 'user_email', '1 day') }}
Schedule:
RateMinutes: 360
TimeoutMinutes: 3
Google Workspace Rapid Multi-IP Authentication
#Detects users authenticating from 3+ distinct IPv4 addresses within 6 hours. May indicate GAIA credential theft where stolen OAuth tokens are used across multiple compromised machines simultaneously. IPv6 addresses are excluded to avoid false positives from dual-stack networking.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | login_success: Login Success |
Rules detecting the same action
These rules filter on the same operation.
- GCP Successful Single-Factor Authentication (Splunk)
- Google Workspace Rapid Multi-IP Authentication (Panther)
- Google Workspace Suspicious Login and Google Drive File Download (YARA-L)
- Google Workspace Suspicious Login and Google Drive File Share (YARA-L)
- Google Workspace User Login with Unusual ASN (Elastic)
Rule specification
AnalysisType: scheduled_query
QueryName: "Google Workspace Rapid Multi-IP Authentication"
Description: |
Detects users authenticating from 3+ distinct IPv4 addresses within 6 hours.
May indicate GAIA credential theft where stolen OAuth tokens are used across
multiple compromised machines simultaneously. IPv6 addresses are excluded to
avoid false positives from dual-stack networking.
Enabled: false
SnowflakeQuery: |
SELECT
actor:email AS user,
COUNT(DISTINCT
CASE
WHEN ipAddress LIKE '%:%' THEN NULL
ELSE ipAddress
END
) AS unique_ip_count,
COUNT(*) AS total_logins,
ARRAY_UNIQUE_AGG(
CASE
WHEN ipAddress LIKE '%:%' THEN NULL
ELSE ipAddress
END
) AS ip_addresses,
ARRAY_UNIQUE_AGG(parameters:login_type) AS login_types,
MIN(p_event_time) AS first_login,
MAX(p_event_time) AS last_login,
DATEDIFF('minute', MIN(p_event_time), MAX(p_event_time)) AS time_span_minutes
FROM panther_logs.public.gsuite_activityevent
WHERE p_occurs_since('6 hours')
AND id:applicationName = 'login'
AND name = 'login_success'
GROUP BY actor:email
HAVING COUNT(DISTINCT
CASE
WHEN ipAddress LIKE '%:%' THEN NULL
ELSE ipAddress
END
) >= 3
ORDER BY unique_ip_count DESC
LIMIT 10000
DatabricksQuery: |
SELECT
actor:email AS user,
COUNT(DISTINCT
CASE
WHEN ipAddress LIKE '%:%' THEN NULL
ELSE ipAddress
END
) AS unique_ip_count,
COUNT(*) AS total_logins,
COLLECT_SET(
CASE
WHEN ipAddress LIKE '%:%' THEN NULL
ELSE ipAddress
END
) AS ip_addresses,
COLLECT_SET(parameters:login_type) AS login_types,
MIN(p_event_time) AS first_login,
MAX(p_event_time) AS last_login,
TIMESTAMPDIFF(MINUTE, MIN(p_event_time), MAX(p_event_time)) AS time_span_minutes
FROM panther_logs.gsuite_activityevent
WHERE p_occurs_since('6 hours')
AND id:applicationName = 'login'
AND name = 'login_success'
GROUP BY actor:email
HAVING COUNT(DISTINCT
CASE
WHEN ipAddress LIKE '%:%' THEN NULL
ELSE ipAddress
END
) >= 3
ORDER BY unique_ip_count DESC
LIMIT 10000
Schedule:
RateMinutes: 360
TimeoutMinutes: 3
Stages and Predicates
Stage 1: source
Stage 2: filter
id:applicationNameisloginnameislogin_success
Stage 3: having
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
id:applicationName | eq |
| field:"id:applicationName" kind:eq value:"login" |
name | eq |
| field:"name" kind:eq value:"login_success" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
user | actor:email |
unique_ip_count | COUNT ( DISTINCT CASE WHEN ipAddress LIKE '%:%' THEN NULL ELSE ipAddress END ) |
total_logins | COUNT ( * ) |
ip_addresses | ARRAY_UNIQUE_AGG ( CASE WHEN ipAddress LIKE '%:%' THEN NULL ELSE ipAddress END ) |
login_types | ARRAY_UNIQUE_AGG ( parameters:login_type ) |
first_login | MIN ( p_event_time ) |
last_login | MAX ( p_event_time ) |
time_span_minutes | DATEDIFF ( 'minute' , MIN ( p_event_time ) , MAX ( p_event_time ) ) |
Google Workspace Rapid Multi-IP Authentication
#Alerts when users authenticate from 3+ distinct IPv4 addresses within 6 hours. This pattern may indicate GAIA credential theft where attackers use stolen OAuth tokens across multiple compromised machines simultaneously. IPv6 addresses are excluded to avoid false positives from dual-stack networking environments.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence | |
| Privilege Escalation | |
| Stealth | |
| Lateral Movement |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | login_success: Login Success |
Rules detecting the same action
These rules filter on the same operation.
- GCP Successful Single-Factor Authentication (Splunk)
- Google Workspace Rapid Multi-IP Authentication (Panther)
- Google Workspace Suspicious Login and Google Drive File Download (YARA-L)
- Google Workspace Suspicious Login and Google Drive File Share (YARA-L)
- Google Workspace User Login with Unusual ASN (Elastic)
Detection logic
def rule(_):
return True
def title(event):
user = event.get("user", "<UNKNOWN_USER>")
ip_count = event.get("unique_ip_count", 0)
return f"Google Workspace: User [{user}] authenticated from {ip_count} distinct IPs in 6 hours"
def severity(event):
ip_count = event.get("unique_ip_count", 0)
# Very high IP count suggests active credential spread
if ip_count >= 4:
return "HIGH"
return "MEDIUM"
def alert_context(event):
return {
"user": event.get("user"),
"unique_ip_count": event.get("unique_ip_count"),
"ip_addresses": event.get("ip_addresses"),
"login_types": event.get("login_types"),
"first_login": event.get("first_login"),
"last_login": event.get("last_login"),
"time_span_minutes": event.get("time_span_minutes"),
"total_logins": event.get("total_logins"),
"description": (
"User authenticated from multiple distinct IPv4 addresses in a short time window"
),
}
Rule specification
AnalysisType: scheduled_rule
DisplayName: "Google Workspace Rapid Multi-IP Authentication"
DedupPeriodMinutes: 360
RuleID: "Google.Workspace.Rapid.Multi.IP.Authentication"
Description: |
Alerts when users authenticate from 3+ distinct IPv4 addresses within 6 hours.
This pattern may indicate GAIA credential theft where attackers use stolen OAuth
tokens across multiple compromised machines simultaneously. IPv6 addresses are
excluded to avoid false positives from dual-stack networking environments.
ScheduledQueries:
- Google Workspace Rapid Multi-IP Authentication
Enabled: false
Filename: gsuite_rapid_multi_ip_authentication_rule.py
Reference: https://businessinsights.bitdefender.com/the-chain-reaction-new-methods-for-extending-local-breaches-in-google-workspace
Runbook: |
1. Query GSuite.ActivityEvent for all login events by the user in the 12 hours before and after the alert to establish the full timeline of authentication activity and identify all source IP addresses used
2. Check if the source IP addresses are associated with cloud providers, VPN services, proxy networks, or known corporate infrastructure, and compare the geographic locations of the IPs to identify impossible travel patterns
3. Search for other authentication anomalies for this user in the past 7 days, including login type changes, OAuth token authorizations with privileged scopes, failed authentication attempts, or device compromise warnings
Severity: Medium
Tags:
- GSuite
- Lateral Movement
- Valid Accounts
- GAIA
- Credential Theft
Reports:
MITRE ATT&CK:
- TA0008:T1078.004
- TA0006:T1550
SummaryAttributes:
- user
- ip_addresses
Stages and Predicates
Rule logic
This rule alerts on rows returned by its scheduled query Google Workspace Rapid Multi-IP Authentication; its Python module (Detection logic above) shapes the alert rather than filtering.
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
user |
unique_ip_count |
ip_addresses |
login_types |
first_login |
last_login |
time_span_minutes |
total_logins |
Response runbook
1. Query GSuite.ActivityEvent for all login events by the user in the 12 hours before and after the alert to establish the full timeline of authentication activity and identify all source IP addresses used
2. Check if the source IP addresses are associated with cloud providers, VPN services, proxy networks, or known corporate infrastructure, and compare the geographic locations of the IPs to identify impossible travel patterns
3. Search for other authentication anomalies for this user in the past 7 days, including login type changes, OAuth token authorizations with privileged scopes, failed authentication attempts, or device compromise warnings
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"first_login": "2024-01-15 10:00:00.000",
"ip_addresses": [
"1.2.3.4",
"5.6.7.8",
"9.10.11.12"
],
"last_login": "2024-01-15 15:30:00.000",
"login_types": [
"saml"
],
"time_span_minutes": 330,
"total_logins": 5,
"unique_ip_count": 3,
"user": "user@example.com"
}
Gsuite Attachments Downloaded from Spam Email
#Detects when a user downloads or saves to Google Drive one or more attachments that are classified as spam.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | any: Gmail (any event) |
Rules detecting the same action
These rules filter on the same operation.
- Gmail Malicious SMTP Response (Panther)
- Gmail Potential Spoofed Email Delivered (Panther)
- gmail spike in undeliverables (YARA-L)
- Gsuite Email Bypassed Spam Filter (Panther)
- Gsuite Link Clicked in Spam Email (Panther)
- Malware Detected in Email (Panther)
- Spam Email Surge (Panther)
Detection logic
from panther_gsuite_helpers import gsuite_activityevent_alert_context
def rule(event):
if event.deep_get("id", "applicationName", default="<UNKNOWN_APPLICATION>") != "gmail":
return False
return event.deep_get(
"parameters", "message_info", "is_spam", default=False
) is True and event.deep_get("parameters", "event_info", "mail_event_type", default=0) in (
17,
18,
19,
)
def title(event):
user = event.deep_get("actor", "email", default="<UNKNOWN_USER>")
return f"[{user}] has downloaded potentially malicious attachments from a spam email"
def alert_context(event):
return gsuite_activityevent_alert_context(event)
Rule specification
AnalysisType: rule
Filename: gsuite_attachments_downloaded_from_spam_email.py
RuleID: "GSuite.Gmail.SpamEmail.AttachmentDownload"
DisplayName: "Gsuite Attachments Downloaded from Spam Email"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
Reports:
MITRE ATT&CK:
- TA0001:T1566.001 # Initial Access: Phishing - Spearphishing Attachment
- TA0011:T1204.002 # Execution: User Execution - Malicious File
Severity: High
Description: Detects when a user downloads or saves to Google Drive one or more attachments that are classified as spam.
Threshold: 1
DedupPeriodMinutes: 60
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameisgmailparameters.message_info.is_spamistrueparameters.event_info.mail_event_typeis one of17,18,19
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 |
|---|---|
actor | actor.email |
applicationName | id.applicationName |
name | |
type | |
parameters |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"callerType": "USER",
"email": "denethor@lotr.com",
"profileId": "123456789"
},
"id": {
"applicationName": "gmail",
"customerId": "1A2B3C",
"time": "2025-11-04 20:44:43.248000000",
"uniqueQualifier": "-123456789"
},
"ipAddress": "1.1.1.1",
"kind": "admin#reports#activity",
"name": "delivery",
"p_any_actor_ids": [
"1234567891234"
],
"p_any_domain_names": [
"evil.com"
],
"p_any_ip_addresses": [
"1.1.1.1"
],
"p_event_time": "2025-11-04 20:44:43.248000000",
"p_log_type": "GSuite.ActivityEvent",
"p_parse_time": "2025-11-04 20:49:46.688935963",
"p_row_id": "0000000000de09c1dc6f0828cbad2ca5",
"p_schema_version": 0,
"p_source_id": "7ee69d4d-df1b-40b3-b5e8-6826dee34b1c",
"p_source_label": "Google Workspace",
"p_udm": {
"source": {
"address": "1.1.1.1",
"ip": "1.1.1.1"
},
"user": {
"provider_id": "123456789"
}
},
"parameters": {
"event_info": {
"elapsed_time_usec": 368746,
"mail_event_type": 17,
"timestamp_usec": 1762289083248347
},
"message_info": {
"action_type": 19,
"flattened_destinations": "gmail-for-work-catchall::denethor@lotr.com",
"is_spam": true,
"link_domain": [
"evil.com"
],
"message_set": {
"type": 46
},
"payload_size": 12345,
"subject": "You won 1 Million Dollar"
}
},
"type": "delivery_type"
}
GSuite Calendar Has Been Made Public
#A User or Admin Has Modified A Calendar To Be Public
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Discovery |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | change_calendar_acls: Change Calendar ACLs |
Detection logic
def rule(event):
return (
event.get("name") == "change_calendar_acls"
and event.get("parameters", {}).get("grantee_email")
== "__public_principal__@public.calendar.google.com"
)
def title(event):
return (
f"GSuite calendar "
f"[{event.deep_get('parameters', 'calendar_id', default='<NO_CALENDAR_ID>')}] made "
f"{public_or_private(event)} by "
f"[{event.deep_get('actor', 'email', default='<NO_ACTOR_FOUND>')}]"
)
def severity(event):
return "LOW" if public_or_private(event) == "private" else "MEDIUM"
def public_or_private(event):
return "private" if event.deep_get("parameters", "access_level") == "none" else "public"
Rule specification
AnalysisType: rule
Filename: gsuite_calendar_made_public.py
RuleID: "GSuite.CalendarMadePublic"
DisplayName: "GSuite Calendar Has Been Made Public"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
Reports:
MITRE ATT&CK:
- TA0007:T1087
Severity: Medium
Description: >
A User or Admin Has Modified A Calendar To Be Public
Reference: https://support.google.com/calendar/answer/37083?hl=en&sjid=864417124752637253-EU
Runbook: >
Follow up with user about this calendar share.
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
nameischange_calendar_aclsparameters.grantee_emailis__public_principal__@public.calendar.google.com
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
name | eq |
| field:"name" kind:eq value:"change_calendar_acls" |
parameters.grantee_email | eq |
| field:"parameters.grantee_email" kind:eq value:"__public_principal__@public.calendar.google.com" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
calendar_id | parameters.calendar_id |
email | actor.email |
Response runbook
Follow up with user about this calendar share.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"email": "user@example.io",
"profileId": "110111111111111111111"
},
"id": {
"applicationName": "calendar",
"customerId": "D12345",
"time": "2022-12-10 22:33:31.852000000",
"uniqueQualifier": "-2888888888888888888"
},
"ipAddress": "1.2.3.4",
"kind": "admin#reports#activity",
"name": "change_calendar_acls",
"ownerDomain": "example.io",
"parameters": {
"access_level": "freebusy",
"api_kind": "web",
"calendar_id": "user@example.io",
"grantee_email": "__public_principal__@public.calendar.google.com",
"user_agent": "Mozilla/5.0"
},
"type": "calendar_change"
}
GSuite Device Suspicious Activity
#GSuite reported a suspicious activity on a user's device.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | SUSPICIOUS_ACTIVITY_EVENT: Suspicious Activity Event |
Detection logic
def rule(event):
if event.deep_get("id", "applicationName") != "mobile":
return False
return bool(event.get("name") == "SUSPICIOUS_ACTIVITY_EVENT")
def title(event):
return (
f"User [{event.deep_get('actor', 'email', default='<UNKNOWN_USER>')}]"
f"'s device was compromised"
)
Rule specification
AnalysisType: rule
Filename: gsuite_mobile_device_suspicious_activity.py
RuleID: "GSuite.DeviceSuspiciousActivity"
DisplayName: "GSuite Device Suspicious Activity"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
Severity: Low
Description: >
GSuite reported a suspicious activity on a user's device.
Reference: https://support.google.com/a/answer/7562460?hl=en&sjid=864417124752637253-EU
Runbook: >
Validate that the suspicious activity was expected by the user.
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameismobilenameisSUSPICIOUS_ACTIVITY_EVENT
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
id.applicationName | eq |
| field:"id.applicationName" kind:eq value:"mobile" |
name | eq |
| field:"name" kind:eq value:"SUSPICIOUS_ACTIVITY_EVENT" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
email | actor.email |
Response runbook
Validate that the suspicious activity was expected by the user.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"callerType": "USER",
"email": "homer.simpson@example.io"
},
"id": {
"applicationName": "mobile"
},
"name": "SUSPICIOUS_ACTIVITY_EVENT",
"parameters": {
"USER_EMAIL": "homer.simpson@example.io"
},
"type": "device_updates"
}
GSuite Document External Ownership Transfer
#A GSuite document's ownership was transferred to an external party.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Collection |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | change_owner: Document Changed Owners |
Detection logic
def rule(event):
if event.get("name") != "change_owner":
return False
if event.deep_get("parameters", "visibility") in (
"shared_internally",
"people_within_domain_with_link",
"private",
):
return False
previous_owner = event.deep_get("parameters", "owner", default="<UNKNOWN USER>")
new_owner = event.deep_get("parameters", "new_owner", default="<UNKNOWN USER>")
previous_owner_domain = previous_owner.split("@")[1] if "@" in previous_owner else None
new_owner_domain = new_owner.split("@")[1] if "@" in new_owner else None
if previous_owner_domain is None or new_owner_domain is None:
return False
if previous_owner_domain != new_owner_domain:
return True
return False
def title(event):
actor = event.deep_get("actor", "email", default="<UNKNOWN USER>")
previous_owner = event.deep_get("parameters", "owner", default="<UNKNOWN USER>")
new_owner = event.deep_get("parameters", "new_owner", default="<UNKNOWN USER>")
return f"User [{actor}] transferred document ownership from [{previous_owner}] to [{new_owner}]"
Rule specification
AnalysisType: rule
Filename: gsuite_doc_ownership_transfer.py
RuleID: "GSuite.DocOwnershipTransfer"
DisplayName: "GSuite Document External Ownership Transfer"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
- Collection:Data from Information Repositories
Reports:
MITRE ATT&CK:
- TA0009:T1213
Severity: Low
Description: >
A GSuite document's ownership was transferred to an external party.
Reference: https://support.google.com/drive/answer/2494892?hl=en&co=GENIE.Platform%3DDesktop&sjid=864417124752637253-EU
Runbook: >
Verify that this document did not contain sensitive or private company information.
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
nameischange_ownerparameters.visibilityis not one ofshared_internally,people_within_domain_with_link,private
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 |
|---|---|---|---|
parameters.visibility | in | people_within_domain_with_link, private, shared_internally | excludes:parameters.visibility field:"parameters.visibility" value:"people_within_domain_with_link" field:"parameters.visibility" value:"private" field:"parameters.visibility" value:"shared_internally" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
name | eq |
| field:"name" kind:eq value:"change_owner" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
email | actor.email |
owner | parameters.owner |
new_owner | parameters.new_owner |
Response runbook
Verify that this document did not contain sensitive or private company information.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"email": "alice@panther.com",
"profileId": "1234567890"
},
"id": {
"applicationName": "drive",
"customerId": "C123abcde",
"time": "2025-07-11 19:50:09.324000000",
"uniqueQualifier": "1234567890"
},
"kind": "admin#reports#activity",
"name": "change_owner",
"parameters": {
"billable": true,
"doc_id": "1234567890",
"doc_title": "sensitive_document.xlsx",
"doc_type": "msexcel",
"new_owner": "bob@example.com",
"owner": "alice@panther.com",
"primary_event": true,
"visibility": "unknown"
},
"type": "acl_change"
}
GSuite Drive Many Documents Deleted
#Scheduled rule for the GSuite Drive Many Documents Deleted query. Looks for users who have deleted more than 10 (tunable) documents the past day.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | TRASH: Trash |
| Google Workspace | trash: Trash |
Rules detecting the same action
These rules filter on the same operation.
- GSuite Many Docs Deleted Query (Panther)
Detection logic
def rule(_):
return True
def title(event):
return (
f"GSuite: [{event.get('user', '<user_not_found>')}] "
f"has deleted [{event.get('delete_count', '<count_not_found>')}] "
"documents from Google Drive."
)
def alert_context(event):
return event.to_dict()
Rule specification
AnalysisType: scheduled_rule
Description: Scheduled rule for the GSuite Drive Many Documents Deleted query. Looks for users who have deleted more than 10 (tunable) documents the past day.
DisplayName: "GSuite Drive Many Documents Deleted"
Enabled: false
Status: Deprecated
Filename: gsuite_drive_many_docs_deleted.py
Reference: https://support.google.com/drive/answer/2375102?hl=en&co=GENIE.Platform%3DAndroid#:~:text=To%20delete%20your%20Google%20Drive,them%20to%20empty%20your%20trash.
Severity: Medium
DedupPeriodMinutes: 60
RuleID: "GSuite.Drive.Many.Documents.Deleted"
Threshold: 1
ScheduledQueries:
- GSuite Many Docs Deleted Query
Stages and Predicates
Rule logic
This rule alerts on rows returned by its scheduled query GSuite Many Docs Deleted Query; its Python module (Detection logic above) shapes the alert rather than filtering.
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field |
|---|
user |
delete_count |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"delete_count": 100,
"deleted_files": [
"importantdoc1",
"importantdoc2",
"importantdoc100"
],
"user": "homer.simpson@simpsons.com"
}
GSuite Drive Many Documents Deleted
#Detects when a user moves more than 10 distinct documents to the trash in Google Drive within 60 minutes. This may indicate accidental or malicious bulk deletion of files.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | trash: Trash |
Rules detecting the same action
These rules filter on the same operation.
Detection logic
def rule(event):
if event.deep_get("id", "applicationName") != "drive":
return False
return event.get("name") == "trash"
def title(event):
user = event.deep_get("actor", "email", default="<UNKNOWN_USER>")
return f"Google Workspace: User [{user}] deleted many documents from Google Drive"
def dedup(event):
return event.deep_get("actor", "email", default="")
def unique(event):
return event.deep_get("parameters", "doc_id") or None
def severity(event):
visibility = event.deep_get("parameters", "visibility", default="")
if visibility == "shared_externally":
return "HIGH"
return "DEFAULT"
def alert_context(event):
return {
"user": event.deep_get("actor", "email"),
"doc_title": event.deep_get("parameters", "doc_title"),
"doc_type": event.deep_get("parameters", "doc_type"),
"visibility": event.deep_get("parameters", "visibility"),
}
Rule specification
AnalysisType: rule
Filename: gsuite_drive_many_docs_deleted.py
RuleID: "GSuite.Drive.BulkDocumentDeletion"
DisplayName: "GSuite Drive Many Documents Deleted"
Status: Experimental
Enabled: true
Severity: Medium
DedupPeriodMinutes: 60
Threshold: 11
LogTypes:
- GSuite.ActivityEvent
Description: >
Detects when a user moves more than 10 distinct documents to the trash in Google Drive
within 60 minutes. This may indicate accidental or malicious bulk deletion of files.
Reference: https://support.google.com/drive/answer/2375102
Reports:
MITRE ATT&CK:
- TA0040:T1485
Tags:
- GSuite
- Impact
- Data Destruction
Runbook: |
1. Query GSuite.ActivityEvent for all trash events by actor:email in the 2 hours around this alert to identify the full list of parameters:doc_title values deleted and whether they belong to shared drives
2. Check parameters:visibility on the deleted documents to determine if externally or internally shared files were affected, and assess the business impact of the deletions
3. Search for other suspicious drive activity by this user in the past 24 hours, including bulk downloads, sharing changes, or access to sensitive documents prior to deletion
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameisdrivenameistrash
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
id.applicationName | eq |
| field:"id.applicationName" kind:eq value:"drive" |
name | eq |
| field:"name" kind:eq value:"trash" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
user | actor.email |
doc_title | parameters.doc_title |
doc_type | parameters.doc_type |
visibility | parameters.visibility |
Response runbook
1. Query GSuite.ActivityEvent for all trash events by actor:email in the 2 hours around this alert to identify the full list of parameters:doc_title values deleted and whether they belong to shared drives
2. Check parameters:visibility on the deleted documents to determine if externally or internally shared files were affected, and assess the business impact of the deletions
3. Search for other suspicious drive activity by this user in the past 24 hours, including bulk downloads, sharing changes, or access to sensitive documents prior to deletion
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"email": "user@example.com"
},
"id": {
"applicationName": "drive"
},
"name": "trash",
"parameters": {
"doc_id": "1ABC123def456GHI789jkl",
"doc_title": "Q4 Financial Report",
"doc_type": "spreadsheet",
"visibility": "shared_internally"
}
}
Gsuite Email Bypassed Spam Filter
#Detects if an email received by a user has bypassed the organization's spam filter.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | any: Gmail (any event) |
Rules detecting the same action
These rules filter on the same operation.
- Gmail Malicious SMTP Response (Panther)
- Gmail Potential Spoofed Email Delivered (Panther)
- gmail spike in undeliverables (YARA-L)
- Gsuite Attachments Downloaded from Spam Email (Panther)
- Gsuite Link Clicked in Spam Email (Panther)
- Malware Detected in Email (Panther)
- Spam Email Surge (Panther)
Detection logic
from panther_gsuite_helpers import gsuite_activityevent_alert_context
def rule(event):
if event.deep_get("id", "applicationName", default="<UNKNOWN_APPLICATION>") != "gmail":
return False
return event.deep_get("parameters", "message_info", "message_set", "type", default=0) == 46
def title(event):
user = event.deep_get("actor", "email", default="<UNKNOWN_USER>")
subject = event.deep_get("parameters", "message_info", "subject", default="<UNKNOWN_SUBJECT>")
return (
f"Message [{subject}] received by user [{user}] "
f"has bypassed your organization's spam filter"
)
def alert_context(event):
return gsuite_activityevent_alert_context(event)
Rule specification
AnalysisType: rule
Filename: gsuite_bypass_spam_filter_email.py
RuleID: "GSuite.Gmail.Email.SpamFilter.Bypass"
DisplayName: "Gsuite Email Bypassed Spam Filter"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
Reports:
MITRE ATT&CK:
- TA0001:T1566 # Initial Access: Phishing
Severity: Medium
Description: >
Detects if an email received by a user has bypassed the organization's spam filter.
Threshold: 1
DedupPeriodMinutes: 60
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameisgmailparameters.message_info.message_set.typeis46
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 |
|---|---|
actor | actor.email |
applicationName | id.applicationName |
name | |
type | |
parameters | |
subject | parameters.message_info.subject |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"callerType": "USER",
"email": "denethor@lotr.com",
"profileId": "123456789"
},
"id": {
"applicationName": "gmail",
"customerId": "1A2B3C",
"time": "2025-11-04 20:44:43.248000000",
"uniqueQualifier": "-123456789"
},
"ipAddress": "1.1.1.1",
"kind": "admin#reports#activity",
"name": "delivery",
"p_any_actor_ids": [
"1234567891234"
],
"p_any_domain_names": [
"evil.com"
],
"p_any_ip_addresses": [
"1.1.1.1"
],
"p_event_time": "2025-11-04 20:44:43.248000000",
"p_log_type": "GSuite.ActivityEvent",
"p_parse_time": "2025-11-04 20:49:46.688935963",
"p_row_id": "0000000000de09c1dc6f0828cbad2ca5",
"p_schema_version": 0,
"p_source_id": "7ee69d4d-df1b-40b3-b5e8-6826dee34b1c",
"p_source_label": "Google Workspace",
"p_udm": {
"source": {
"address": "1.1.1.1",
"ip": "1.1.1.1"
},
"user": {
"provider_id": "123456789"
}
},
"parameters": {
"event_info": {
"elapsed_time_usec": 368746,
"timestamp_usec": 1762289083248347
},
"message_info": {
"action_type": 19,
"flattened_destinations": "gmail-for-work-catchall::denethor@lotr.com",
"link_domain": [
"evil.com"
],
"message_set": {
"type": 46
},
"payload_size": 12345,
"subject": "You won 1 Million Dollar"
}
},
"type": "delivery_type"
}
GSuite External Drive Document
#A Google drive resource became externally accessible.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Collection |
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
Detection logic
import json
from unittest.mock import MagicMock
from panther_gsuite_helpers import gsuite_parameter_lookup as param_lookup
# Add any domain name(s) that you expect to share documents with in the ALLOWED_DOMAINS set
ALLOWED_DOMAINS = set()
PUBLIC_PROVIDERS = {
"gmail.com",
"yahoo.com",
"outlook.com",
"aol.com",
"yandex.com",
"protonmail.com",
"pm.me",
"icloud.com",
"tutamail.com",
"tuta.io",
"keemail.me",
"mail.com",
"zohomail.com",
"hotmail.com",
"msn.com",
}
VISIBILITY = {
"people_with_link",
"people_within_domain_with_link",
"public_on_the_web",
"shared_externally",
"unknown",
}
ALERT_DETAILS = {}
# Events where documents have changed perms due to parent folder change
INHERITANCE_EVENTS = {
"change_user_access_hierarchy_reconciled",
"change_document_access_scope_hierarchy_reconciled",
}
def init_alert_details(log):
global ALERT_DETAILS # pylint: disable=global-statement
ALERT_DETAILS[log] = {
"ACCESS_SCOPE": "<UNKNOWN_ACCESS_SCOPE>",
"DOC_TITLE": "<UNKNOWN_TITLE>",
"NEW_VISIBILITY": "<UNKNOWN_VISIBILITY>",
"TARGET_USER_EMAILS": ["<UNKNOWN_USER>"],
"TARGET_DOMAIN": "<UNKNOWN_DOMAIN>",
}
def user_is_external(target_user):
global ALLOWED_DOMAINS # pylint: disable=global-statement
# We need to type-cast ALLOWED_DOMAINS for unit testing mocks
if isinstance(ALLOWED_DOMAINS, MagicMock):
ALLOWED_DOMAINS = set(json.loads(ALLOWED_DOMAINS())) # pylint: disable=not-callable
for domain in ALLOWED_DOMAINS:
if domain in target_user:
return False
return True
def rule(event):
# pylint: disable=too-complex
global ALLOWED_DOMAINS # pylint: disable=global-statement
if event.deep_get("id", "applicationName") != "drive":
return False
# Events that have the types in INHERITANCE_EVENTS are
# changes to documents and folders that occur due to
# a change in the parent folder's permission. We ignore
# these events to prevent every folder change from
# generating multiple alerts.
if event.get("name") in INHERITANCE_EVENTS:
return False
log = event.get("p_row_id")
init_alert_details(log)
# We need to type-cast ALLOWED_DOMAINS for unit testing mocks
if isinstance(ALLOWED_DOMAINS, MagicMock):
ALLOWED_DOMAINS = set(json.loads(ALLOWED_DOMAINS())) # pylint: disable=not-callable
# For GSuite.ActivityEvent, each log is a single event.
# Check if this event is a visibility change for a domain
if (
event.get("type") == "acl_change"
and event.get("name") == "change_document_visibility"
and param_lookup(event.get("parameters", {}), "new_value") != ["private"]
and not param_lookup(event.get("parameters", {}), "target_domain") in ALLOWED_DOMAINS
and param_lookup(event.get("parameters", {}), "visibility") in VISIBILITY
):
ALERT_DETAILS[log]["TARGET_DOMAIN"] = param_lookup(
event.get("parameters", {}), "target_domain"
)
ALERT_DETAILS[log]["NEW_VISIBILITY"] = param_lookup(
event.get("parameters", {}), "visibility"
)
ALERT_DETAILS[log]["DOC_TITLE"] = param_lookup(event.get("parameters", {}), "doc_title")
if param_lookup(event.get("parameters", {}), "new_value") != ["none"]:
ALERT_DETAILS[log]["ACCESS_SCOPE"] = param_lookup(
event.get("parameters", {}), "new_value"
)
return True
# For visibility changes that apply to a user
if (
event.get("type") == "acl_change"
and event.get("name") == "change_user_access"
and param_lookup(event.get("parameters", {}), "new_value") != ["none"]
and user_is_external(param_lookup(event.get("parameters", {}), "target_user"))
):
if ALERT_DETAILS[log]["TARGET_USER_EMAILS"] != ["<UNKNOWN_USER>"]:
ALERT_DETAILS[log]["TARGET_USER_EMAILS"].append(
param_lookup(event.get("parameters", {}), "target_user")
)
else:
ALERT_DETAILS[log]["TARGET_USER_EMAILS"] = [
param_lookup(event.get("parameters", {}), "target_user")
]
ALERT_DETAILS[log]["DOC_TITLE"] = param_lookup(event.get("parameters", {}), "doc_title")
ALERT_DETAILS[log]["ACCESS_SCOPE"] = param_lookup(
event.get("parameters", {}), "new_value"
)
return True
return False
def alert_context(event):
log = event.get("p_row_id")
if ALERT_DETAILS[log]["TARGET_USER_EMAILS"] != ["<UNKNOWN_USER>"]:
return {"target users": ALERT_DETAILS[log]["TARGET_USER_EMAILS"]}
return {}
def dedup(event):
return event.deep_get("actor", "email", default="<UNKNOWN_USER>")
def title(event):
log = event.get("p_row_id")
if ALERT_DETAILS[log]["TARGET_USER_EMAILS"] != ["<UNKNOWN_USER>"]:
if len(ALERT_DETAILS[log]["TARGET_USER_EMAILS"]) == 1:
sharing_scope = ALERT_DETAILS[log]["TARGET_USER_EMAILS"][0]
else:
sharing_scope = "multiple users"
if ALERT_DETAILS[log]["NEW_VISIBILITY"] == "shared_externally":
sharing_scope += " (outside the document's current domain)"
elif ALERT_DETAILS[log]["TARGET_DOMAIN"] == "all":
sharing_scope = "the entire internet"
if ALERT_DETAILS[log]["NEW_VISIBILITY"] == "people_with_link":
sharing_scope += " (anyone with the link)"
elif ALERT_DETAILS[log]["NEW_VISIBILITY"] == "public_on_the_web":
sharing_scope += " (link not required)"
else:
sharing_scope = f"the {ALERT_DETAILS[log]['TARGET_DOMAIN']} domain"
if ALERT_DETAILS[log]["NEW_VISIBILITY"] == "people_within_domain_with_link":
sharing_scope += f" (anyone in {ALERT_DETAILS[log]['TARGET_DOMAIN']} with the link)"
elif ALERT_DETAILS[log]["NEW_VISIBILITY"] == "public_in_the_domain":
sharing_scope += f" (anyone in {ALERT_DETAILS[log]['TARGET_DOMAIN']})"
# alert_access_scope = ALERT_DETAILS[log]["ACCESS_SCOPE"][0].replace("can_", "")
return (
f"User [{event.deep_get('actor', 'email', default='<UNKNOWN_USER>')}] made documents "
f"externally visible"
)
def severity(event):
log = event.get("p_row_id")
if ALERT_DETAILS[log]["TARGET_USER_EMAILS"] != ["<UNKNOWN_USER>"]:
for address in ALERT_DETAILS[log]["TARGET_USER_EMAILS"]:
domain = address.split("@")[1]
if domain in PUBLIC_PROVIDERS:
return "LOW"
return "INFO"
Rule specification
AnalysisType: rule
Filename: gsuite_drive_visibility_change.py
RuleID: "GSuite.DriveVisibilityChanged"
DisplayName: "GSuite External Drive Document"
Enabled: false
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
- Collection:Data from Information Repositories
- Configuration Required
Reports:
MITRE ATT&CK:
- TA0009:T1213
Severity: Low
Description: >
A Google drive resource became externally accessible.
Reference: https://support.google.com/a/users/answer/12380484?hl=en&sjid=864417124752637253-EU
Runbook: >
Investigate whether the drive document is appropriate to be publicly accessible.
SummaryAttributes:
- actor:email
DedupPeriodMinutes: 360 # 6 hours
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameisdrivenameis not one ofchange_user_access_hierarchy_reconciled,change_document_access_scope_hierarchy_reconciledany of:
all of:
typeisacl_changenameischange_document_visibility
all of:
any of:
typeis notacl_changenameis notchange_document_visibility
typeisacl_changenameischange_user_access
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 |
|---|---|---|---|
name | in | change_document_access_scope_hierarchy_reconciled, change_user_access_hierarchy_reconciled | excludes:name field:"name" value:"change_document_access_scope_hierarchy_reconciled" field:"name" value:"change_user_access_hierarchy_reconciled" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
id.applicationName | eq |
| field:"id.applicationName" kind:eq value:"drive" |
name | eq |
| field:"name" kind:eq |
type | eq |
| field:"type" kind:eq value:"acl_change" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
email | actor.email |
Response runbook
Investigate whether the drive document is appropriate to be publicly accessible.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"email": "bobert@gmail.com"
},
"id": {
"applicationName": "drive"
},
"name": "change_document_visibility",
"p_log_type": "GSuite.ActivityEvent",
"p_row_id": "111222",
"parameters": {
"doc_title": "my shared document",
"new_value": [
"people_with_link"
],
"target_domain": "all",
"visibility": "people_with_link",
"visibility_change": "external"
},
"type": "acl_change"
}
GSuite Government Backed Attack
#Detects Google Workspace warnings of government-backed attacks targeting user accounts, issued only when indicators match nation-state threat actors or APT groups. These sophisticated attacks target high-value individuals using advanced tactics including zero-day exploits, spear-phishing, and social engineering. Successful compromise can lead to persistent access, intellectual property theft, and supply chain attacks.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance | |
| Initial Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | gov_attack_warning: Government-Backed Attack Warning |
Detection logic
def rule(event):
if event.deep_get("id", "applicationName") != "login":
return False
return bool(event.get("name") == "gov_attack_warning")
def title(event):
return (
f"User [{event.deep_get('actor', 'email', default='<UNKNOWN_EMAIL>')}] may have been "
f"targeted by a government attack"
)
Rule specification
AnalysisType: rule
Filename: gsuite_gov_attack.py
RuleID: "GSuite.GovernmentBackedAttack"
DisplayName: "GSuite Government Backed Attack"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
- APT
- Advanced Persistent Threat
- Nation State
- Targeted Attack
- Reconnaissance
- Initial Access
- Government Backed Attack
- High Profile Target
Severity: Critical
Reports:
MITRE ATT&CK:
- TA0043:T1595
- TA0001:T1566
- TA0042:T1589
Description: >
Detects Google Workspace warnings of government-backed attacks targeting user accounts, issued only when indicators match nation-state threat actors or APT groups. These sophisticated attacks target high-value individuals using advanced tactics including zero-day exploits, spear-phishing, and social engineering. Successful compromise can lead to persistent access, intellectual property theft, and supply chain attacks.
Reference: https://support.google.com/a/answer/9007870?hl=en
Runbook: |
1. Query GSuite.ActivityEvent logs for all login events, OAuth app authorizations, email forwarding rules, delegated access grants, and data exports by actor:email in the 90 days before this warning to identify suspicious activity indicating potential compromise
2. Review login locations, IP addresses, and device information from the targeted user's recent authentication events to detect unusual geographic access patterns or impossible travel that may indicate reconnaissance or account takeover attempts
3. Contact Google Workspace enterprise support to obtain additional threat intelligence about the government-backed attack including threat actor attribution, attack vectors observed, and specific indicators of compromise related to this incident
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameisloginnameisgov_attack_warning
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
id.applicationName | eq |
| field:"id.applicationName" kind:eq value:"login" |
name | eq |
| field:"name" kind:eq value:"gov_attack_warning" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
email | actor.email |
Response runbook
1. Query GSuite.ActivityEvent logs for all login events, OAuth app authorizations, email forwarding rules, delegated access grants, and data exports by actor:email in the 90 days before this warning to identify suspicious activity indicating potential compromise
2. Review login locations, IP addresses, and device information from the targeted user's recent authentication events to detect unusual geographic access patterns or impossible travel that may indicate reconnaissance or account takeover attempts
3. Contact Google Workspace enterprise support to obtain additional threat intelligence about the government-backed attack including threat actor attribution, attack vectors observed, and specific indicators of compromise related to this incident
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"email": "homer.simpson@example.com"
},
"id": {
"applicationName": "login"
},
"name": "gov_attack_warning",
"parameters": {
"is_suspicious": null,
"login_challenge_method": [
"none"
]
},
"type": "login"
}
Gsuite Link Clicked in Spam Email
#Detects when a user click links contained in a received email that is classified as spam.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | any: Gmail (any event) |
Rules detecting the same action
These rules filter on the same operation.
- Gmail Malicious SMTP Response (Panther)
- Gmail Potential Spoofed Email Delivered (Panther)
- gmail spike in undeliverables (YARA-L)
- Gsuite Attachments Downloaded from Spam Email (Panther)
- Gsuite Email Bypassed Spam Filter (Panther)
- Malware Detected in Email (Panther)
- Spam Email Surge (Panther)
Detection logic
from panther_gsuite_helpers import gsuite_activityevent_alert_context
def rule(event):
if event.deep_get("id", "applicationName", default="<UNKNOWN_APPLICATION>") != "gmail":
return False
return event.deep_get(
"parameters", "message_info", "is_spam", default=False
) is True and event.deep_get("parameters", "event_info", "mail_event_type", default=0) in (
15,
16,
)
def title(event):
user = event.deep_get("actor", "email", default="<UNKNOWN_USER>")
return f"[{user}] has clicked potentially malicious links contained in a spam email"
def alert_context(event):
return gsuite_activityevent_alert_context(event)
Rule specification
AnalysisType: rule
Filename: gsuite_links_clicked_in_spam_email.py
RuleID: "GSuite.Gmail.SpamEmail.LinkClicked"
DisplayName: "Gsuite Link Clicked in Spam Email"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
Reports:
MITRE ATT&CK:
- TA0001:T1566.002 # Initial Access: Phishing - Spearphishing Link
- TA0011:T1204.001 # Execution: User Execution - Malicious Link
Severity: High
Description: Detects when a user click links contained in a received email that is classified as spam.
Threshold: 1
DedupPeriodMinutes: 60
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameisgmailparameters.message_info.is_spamistrueparameters.event_info.mail_event_typeis one of15,16
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 |
|---|---|
actor | actor.email |
applicationName | id.applicationName |
name | |
type | |
parameters |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"callerType": "USER",
"email": "denethor@lotr.com",
"profileId": "123456789"
},
"id": {
"applicationName": "gmail",
"customerId": "1A2B3C",
"time": "2025-11-04 20:44:43.248000000",
"uniqueQualifier": "-123456789"
},
"ipAddress": "1.1.1.1",
"kind": "admin#reports#activity",
"name": "delivery",
"p_any_actor_ids": [
"1234567891234"
],
"p_any_domain_names": [
"evil.com"
],
"p_any_ip_addresses": [
"1.1.1.1"
],
"p_event_time": "2025-11-04 20:44:43.248000000",
"p_log_type": "GSuite.ActivityEvent",
"p_parse_time": "2025-11-04 20:49:46.688935963",
"p_row_id": "0000000000de09c1dc6f0828cbad2ca5",
"p_schema_version": 0,
"p_source_id": "7ee69d4d-df1b-40b3-b5e8-6826dee34b1c",
"p_source_label": "Google Workspace",
"p_udm": {
"source": {
"address": "1.1.1.1",
"ip": "1.1.1.1"
},
"user": {
"provider_id": "123456789"
}
},
"parameters": {
"event_info": {
"elapsed_time_usec": 368746,
"mail_event_type": 15,
"timestamp_usec": 1762289083248347
},
"message_info": {
"action_type": 19,
"flattened_destinations": "gmail-for-work-catchall::denethor@lotr.com",
"is_spam": true,
"link_domain": [
"evil.com"
],
"message_set": {
"type": 46
},
"payload_size": 12345,
"subject": "You won 1 Million Dollar"
}
},
"type": "delivery_type"
}
GSuite Login Type
#A login of a non-approved type was detected for this user.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Detection logic
# allow-list of approved login types
# comment or uncomment approved login types as needed
APPROVED_LOGIN_TYPES = {
"exchange",
"google_password",
"reauth",
"saml",
# "unknown",
}
# allow-list any application names here
APPROVED_APPLICATION_NAMES = {"saml"}
def rule(event):
if event.get("type") != "login":
return False
if event.get("name") == "logout":
return False
if (
event.deep_get("parameters", "login_type") in APPROVED_LOGIN_TYPES
or event.deep_get("id", "applicationName") in APPROVED_APPLICATION_NAMES
):
return False
return True
def title(event):
return (
f"A login attempt of a non-approved type was detected for user "
f"[{event.deep_get('actor', 'email', default='<UNKNOWN_USER>')}]"
)
Rule specification
AnalysisType: rule
Filename: gsuite_login_type.py
RuleID: "GSuite.LoginType"
DisplayName: "GSuite Login Type"
Enabled: false
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
- Configuration Required
- Initial Access:Valid Accounts
Reports:
MITRE ATT&CK:
- TA0001:T1078
Severity: Medium
Description: >
A login of a non-approved type was detected for this user.
Reference: https://support.google.com/a/answer/9039184?hl=en&sjid=864417124752637253-EU
Runbook: >
Correct the user account settings so that only logins of approved types are available.
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
typeisloginnameis notlogoutparameters.login_typeis not one ofexchange,google_password,reauth,samlid.applicationNameis not one ofsaml
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
id.applicationName | eq | saml | excludes:id.applicationName field:"id.applicationName" value:"saml" |
parameters.login_type | in | exchange, google_password, reauth, saml | excludes:parameters.login_type |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
name | ne |
| field:"name" kind:ne value:"logout" |
type | eq |
| field:"type" kind:eq value:"login" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
email | actor.email |
Response runbook
Correct the user account settings so that only logins of approved types are available.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"email": "some.user@somedomain.com"
},
"id": {
"applicationName": "login"
},
"name": "login_success",
"parameters": {
"login_type": "turbo-snail"
},
"type": "login"
}
Gsuite Mail forwarded to external domain
#A user has configured mail forwarding to an external domain
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Collection |
Telemetry coverage
Detection logic
def rule(event):
if event.deep_get("id", "applicationName") not in ("user_accounts", "login"):
return False
if event.get("name") == "email_forwarding_out_of_domain":
actor_domain = event.deep_get("actor", "email", default="@").split("@")[-1]
target_domain = event.deep_get(
"parameters", "email_forwarding_destination_address", default="@"
).split("@")[-1]
if actor_domain != target_domain:
return True
return False
def title(event):
external_address = event.deep_get("parameters", "email_forwarding_destination_address")
user = event.deep_get("actor", "email")
return f"An email forwarding rule was created by {user} to {external_address}"
Rule specification
AnalysisType: rule
Filename: gsuite_external_forwarding.py
RuleID: "GSuite.ExternalMailForwarding"
DisplayName: "Gsuite Mail forwarded to external domain"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
- Collection:Email Collection
Reports:
MITRE ATT&CK:
- TA0009:T1114
Severity: Medium
Description: >
A user has configured mail forwarding to an external domain
Reference: https://support.google.com/mail/answer/10957?hl=en&sjid=864417124752637253-EU
Runbook: >
Follow up with user to remove this forwarding rule if not allowed.
SummaryAttributes:
- p_any_emails
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameis one ofuser_accounts,loginnameisemail_forwarding_out_of_domain
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 |
|---|---|---|---|
id.applicationName | in |
| field:"id.applicationName" kind:in |
name | eq |
| field:"name" kind:eq value:"email_forwarding_out_of_domain" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
email | actor.email |
email_forwarding_destination_address | parameters.email_forwarding_destination_address |
Response runbook
Follow up with user to remove this forwarding rule if not allowed.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"email": "homer.simpson@.springfield.io"
},
"id": {
"applicationName": "user_accounts",
"customerId": "D12345"
},
"name": "email_forwarding_out_of_domain",
"parameters": {
"email_forwarding_destination_address": "HSimpson@gmail.com"
},
"type": "email_forwarding_change"
}
GSuite Many Docs Deleted Query
#Query to search for a user deleting many documents.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | TRASH: Trash |
| Google Workspace | trash: Trash |
Rules detecting the same action
These rules filter on the same operation.
- GSuite Drive Many Documents Deleted (Panther)
Rule specification
AnalysisType: scheduled_query
Description: Query to search for a user deleting many documents.
Enabled: false
SnowflakeQuery: |
SELECT
actor:email AS user,
ARRAY_AGG( DISTINCT parameters:doc_title) AS deleted_files,
ARRAY_SIZE(deleted_files) as delete_count,
TIME_SLICE(p_event_time, 60, 'minute') as t_s
FROM panther_logs.public.gsuite_activityevent
WHERE p_occurs_since('1 day')
AND name = 'trash'
GROUP BY actor:email, t_s
HAVING delete_count > 10
ORDER BY delete_count DESC
DatabricksQuery: |
SELECT
actor:email AS user,
COLLECT_SET(parameters:doc_title) AS deleted_files,
SIZE(COLLECT_SET(parameters:doc_title)) AS delete_count,
DATE_TRUNC('hour', p_event_time) AS t_s
FROM panther_logs.gsuite_activityevent
WHERE p_occurs_since('1 day')
AND name = 'trash'
GROUP BY actor:email, DATE_TRUNC('hour', p_event_time)
HAVING delete_count > 10
ORDER BY delete_count DESC
QueryName: "GSuite Many Docs Deleted Query"
Schedule:
RateMinutes: 1440
TimeoutMinutes: 1
Stages and Predicates
Stage 1: source
Stage 2: filter
nameistrash
Stage 3: having
delete_countis greater than10
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
delete_count | gt |
| field:"delete_count" kind:gt value:"10" |
name | eq |
| field:"name" kind:eq value:"trash" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
user | actor:email |
deleted_files | ARRAY_AGG ( DISTINCT parameters:doc_title ) |
delete_count | ARRAY_SIZE ( deleted_files ) |
t_s | TIME_SLICE ( p_event_time , 60 , 'minute' ) |
GSuite Many Docs Downloaded Query
#Query to search high document download counts by users.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | download: Download |
Rules detecting the same action
These rules filter on the same operation.
- Google Drive High Download Count (Panther)
Rule specification
AnalysisType: scheduled_query
Description: Query to search high document download counts by users.
Enabled: false
SnowflakeQuery: |
SELECT
actor:email AS user,
ARRAY_SLICE(ARRAY_UNIQUE_AGG(parameters:doc_title), 0, 100) AS downloaded_files,
count(distinct parameters:doc_title) as download_count,
TIME_SLICE(p_event_time, 60, 'minute') as t_s
FROM panther_logs.public.gsuite_activityevent
WHERE p_occurs_since('1 day')
AND name = 'download'
GROUP BY actor:email, t_s
HAVING download_count > 10
ORDER BY download_count DESC
DatabricksQuery: |
SELECT
actor:email AS user,
SLICE(COLLECT_SET(parameters:doc_title), 1, 100) AS downloaded_files,
COUNT(DISTINCT parameters:doc_title) AS download_count,
DATE_TRUNC('hour', p_event_time) AS t_s
FROM panther_logs.gsuite_activityevent
WHERE p_occurs_since('1 day')
AND name = 'download'
GROUP BY actor:email, DATE_TRUNC('hour', p_event_time)
HAVING download_count > 10
ORDER BY download_count DESC
QueryName: "GSuite Many Docs Downloaded Query"
Schedule:
RateMinutes: 1440
TimeoutMinutes: 2
Stages and Predicates
Stage 1: source
Stage 2: filter
nameisdownload
Stage 3: having
download_countis greater than10
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
download_count | gt |
| field:"download_count" kind:gt value:"10" |
name | eq |
| field:"name" kind:eq value:"download" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
user | actor:email |
downloaded_files | ARRAY_SLICE ( ARRAY_UNIQUE_AGG ( parameters:doc_title ) , 0 , 100 ) |
download_count | count ( DISTINCT parameters:doc_title ) |
t_s | TIME_SLICE ( p_event_time , 60 , 'minute' ) |
GSuite Overly Visible Drive Document
#A Google drive resource that is overly visible has been modified.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Collection |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | any: Drive (any event) |
Rules detecting the same action
These rules filter on the same operation.
Detection logic
from panther_gsuite_helpers import gsuite_details_lookup as details_lookup
from panther_gsuite_helpers import gsuite_parameter_lookup as param_lookup
RESOURCE_CHANGE_EVENTS = {
"create",
"move",
"upload",
"edit",
}
PERMISSIVE_VISIBILITY = {
"people_with_link",
"public_on_the_web",
}
def rule(event):
if event.deep_get("id", "applicationName") != "drive":
return False
details = details_lookup("access", RESOURCE_CHANGE_EVENTS, event)
return (
bool(details)
and param_lookup(details.get("parameters", {}), "visibility") in PERMISSIVE_VISIBILITY
)
def dedup(event):
user = event.deep_get("actor", "email")
if user is None:
user = event.deep_get("actor", "profileId", default="<UNKNOWN_PROFILEID>")
return user
def title(event):
details = details_lookup("access", RESOURCE_CHANGE_EVENTS, event)
doc_title = param_lookup(details.get("parameters", {}), "doc_title")
share_settings = param_lookup(details.get("parameters", {}), "visibility")
user = event.deep_get("actor", "email")
if user is None:
user = event.deep_get("actor", "profileId", default="<UNKNOWN_PROFILEID>")
return (
f"User [{user}]"
f" modified a document [{doc_title}] that has overly permissive share"
f" settings [{share_settings}]"
)
Rule specification
AnalysisType: rule
Filename: gsuite_drive_overly_visible.py
RuleID: "GSuite.DriveOverlyVisible"
DisplayName: "GSuite Overly Visible Drive Document"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
- Collection:Data from Information Repositories
Reports:
MITRE ATT&CK:
- TA0009:T1213
Severity: Info
Description: >
A Google drive resource that is overly visible has been modified.
Reference: https://support.google.com/docs/answer/2494822?hl=en&co=GENIE.Platform%3DDesktop&sjid=864417124752637253-EU
Runbook: >
Investigate whether the drive document is appropriate to be this visible.
SummaryAttributes:
- actor:email
DedupPeriodMinutes: 360 # 6 hours
Stages and Predicates
Fires on GSuite.ActivityEvent events when the condition below holds.
Condition
id.applicationNameisdrive
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 |
|---|---|---|---|
id.applicationName | eq |
| field:"id.applicationName" kind:eq value:"drive" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
profileId | actor.profileId |
Response runbook
Investigate whether the drive document is appropriate to be this visible.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"email": "bobert@example.com"
},
"id": {
"applicationName": "drive"
},
"name": "edit",
"p_log_type": "GSuite.ActivityEvent",
"p_row_id": "111222",
"parameters": {
"doc_title": "my shared document",
"visibility": "people_with_link"
},
"type": "access"
}
GSuite Passthrough Rule Triggered
#A GSuite rule was triggered.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | any: Rules (any event) |
Detection logic
def rule(event):
if event.deep_get("id", "applicationName") != "rules":
return False
if not event.deep_get("parameters", "triggered_actions"):
return False
return True
def title(event):
rule_severity = event.deep_get("parameters", "severity")
if event.deep_get("parameters", "rule_name"):
return (
"GSuite "
+ rule_severity
+ " Severity Rule Triggered: "
+ event.deep_get("parameters", "rule_name")
)
return "GSuite " + rule_severity + " Severity Rule Triggered"
def severity(event):
return event.deep_get("parameters", "severity", default="INFO")
Rule specification
AnalysisType: rule
Filename: gsuite_passthrough_rule.py
RuleID: "GSuite.Rule"
DisplayName: "GSuite Passthrough Rule Triggered"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
Severity: Info
Description: >
A GSuite rule was triggered.
Reference: https://support.google.com/a/answer/9420866
Runbook: >
Investigate what triggered the rule.
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameisrulesparameters.triggered_actionsis present
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
id.applicationName | eq |
| field:"id.applicationName" kind:eq value:"rules" |
parameters.triggered_actions | is_not_null | field:"parameters.triggered_actions" kind:is_not_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
severity | parameters.severity |
rule_name | parameters.rule_name |
Response runbook
Investigate what triggered the rule.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"email": "some.user@somedomain.com"
},
"id": {
"applicationName": "rules"
},
"parameters": {
"data_source": "DRIVE",
"severity": "HIGH",
"triggered_actions": [
{
"action_type": "DRIVE_UNFLAG_DOCUMENT"
}
]
}
}
GSuite User Advanced Protection Change
#A user disabled advanced protection for themselves.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | titanium_unenroll: Advanced Protection Unenrolled |
Detection logic
def rule(event):
if event.deep_get("id", "applicationName") != "user_accounts":
return False
return bool(event.get("name") == "titanium_unenroll")
def title(event):
return (
f"Advanced protection was disabled for user "
f"[{event.deep_get('actor', 'email', default='<UNKNOWN_EMAIL>')}]"
)
Rule specification
AnalysisType: rule
Filename: gsuite_advanced_protection.py
RuleID: "GSuite.AdvancedProtection"
DisplayName: "GSuite User Advanced Protection Change"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
- Defense Evasion:Impair Defenses
Reports:
MITRE ATT&CK:
- TA0005:T1562
Severity: Low
Description: >
A user disabled advanced protection for themselves.
Reference: https://support.google.com/a/answer/9378686?hl=en&sjid=864417124752637253-EU
Runbook: >
Have the user re-enable Google Advanced Protection
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameisuser_accountsnameistitanium_unenroll
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
id.applicationName | eq |
| field:"id.applicationName" kind:eq value:"user_accounts" |
name | eq |
| field:"name" kind:eq value:"titanium_unenroll" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
email | actor.email |
Response runbook
Have the user re-enable Google Advanced Protection
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"callerType": "USER",
"email": "homer.simpson@example.com"
},
"id": {
"applicationName": "user_accounts"
},
"name": "titanium_unenroll",
"type": "titanium_change"
}
GSuite User Banned from Group
#A GSuite user was banned from an enterprise group by moderator action.
Detection logic
def rule(event):
if event.deep_get("id", "applicationName") != "groups_enterprise":
return False
if event.get("type") == "moderator_action":
return bool(event.get("name") == "ban_user_with_moderation")
return False
def title(event):
return (
f"User [{event.deep_get('actor', 'email', default='<UNKNOWN_EMAIL>')}] "
f"banned another user from a group."
)
Rule specification
AnalysisType: rule
Filename: gsuite_group_banned_user.py
RuleID: "GSuite.GroupBannedUser"
DisplayName: "GSuite User Banned from Group"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
Severity: Low
Description: >
A GSuite user was banned from an enterprise group by moderator action.
Reference: https://support.google.com/a/users/answer/9303224?hl=en&sjid=864417124752637253-EU
Runbook: >
Investigate the banned user to see if further disciplinary action needs to be taken.
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameisgroups_enterprisetypeismoderator_actionnameisban_user_with_moderation
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
id.applicationName | eq |
| field:"id.applicationName" kind:eq value:"groups_enterprise" |
name | eq |
| field:"name" kind:eq value:"ban_user_with_moderation" |
type | eq |
| field:"type" kind:eq value:"moderator_action" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
email | actor.email |
Response runbook
Investigate the banned user to see if further disciplinary action needs to be taken.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"email": "homer.simpson@example.com"
},
"id": {
"applicationName": "groups_enterprise"
},
"name": "ban_user_with_moderation",
"type": "moderator_action"
}
GSuite User Device Compromised
#GSuite reported a user's device has been compromised.
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | DEVICE_COMPROMISED_EVENT: Device Compromised Event |
Detection logic
def rule(event):
if event.deep_get("id", "applicationName") != "mobile":
return False
if event.get("name") == "DEVICE_COMPROMISED_EVENT":
return bool(event.deep_get("parameters", "DEVICE_COMPROMISED_STATE") == "COMPROMISED")
return False
def title(event):
return (
f"User [{event.deep_get('parameters', 'USER_EMAIL', default='<UNKNOWN_USER>')}]'s "
f"device was compromised"
)
Rule specification
AnalysisType: rule
Filename: gsuite_mobile_device_compromise.py
RuleID: "GSuite.DeviceCompromise"
DisplayName: "GSuite User Device Compromised"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
Severity: Medium
Description: >
GSuite reported a user's device has been compromised.
Reference: https://support.google.com/a/answer/7562165?hl=en&sjid=864417124752637253-EU
Runbook: >
Have the user change their passwords and reset the device.
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameismobilenameisDEVICE_COMPROMISED_EVENTparameters.DEVICE_COMPROMISED_STATEisCOMPROMISED
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
id.applicationName | eq |
| field:"id.applicationName" kind:eq value:"mobile" |
name | eq |
| field:"name" kind:eq value:"DEVICE_COMPROMISED_EVENT" |
parameters.DEVICE_COMPROMISED_STATE | eq |
| field:"parameters.DEVICE_COMPROMISED_STATE" kind:eq value:"COMPROMISED" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
USER_EMAIL | parameters.USER_EMAIL |
Response runbook
Have the user change their passwords and reset the device.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"callerType": "USER",
"email": "homer.simpson@example.io"
},
"id": {
"applicationName": "mobile"
},
"name": "DEVICE_COMPROMISED_EVENT",
"parameters": {
"DEVICE_COMPROMISED_STATE": "COMPROMISED",
"USER_EMAIL": "homer.simpson@example.io"
},
"type": "device_updates"
}
GSuite User Device Unlock Failures
#Someone failed to unlock a user's device multiple times in quick succession.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | FAILED_PASSWORD_ATTEMPTS_EVENT: Failed Password Attempts Event |
Detection logic
MAX_UNLOCK_ATTEMPTS = 10
def rule(event):
if event.deep_get("id", "applicationName") != "mobile":
return False
if event.get("name") == "FAILED_PASSWORD_ATTEMPTS_EVENT":
attempts = event.deep_get("parameters", "FAILED_PASSWD_ATTEMPTS")
return int(attempts if attempts else 0) > MAX_UNLOCK_ATTEMPTS
return False
def title(event):
return (
f"User [{event.deep_get('actor', 'email', default='<UNKNOWN_USER>')}]"
f"'s device had multiple failed unlock attempts"
)
Rule specification
AnalysisType: rule
Filename: gsuite_mobile_device_screen_unlock_fail.py
RuleID: "GSuite.DeviceUnlockFailure"
DisplayName: "GSuite User Device Unlock Failures"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
- Credential Access:Brute Force
Reports:
MITRE ATT&CK:
- TA0006:T1110
Severity: Medium
Description: >
Someone failed to unlock a user's device multiple times in quick succession.
Reference: https://support.google.com/a/answer/6350074?hl=en
Runbook: >
Verify that these unlock attempts came from the user, and not a malicious actor which has acquired the user's device.
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameismobilenameisFAILED_PASSWORD_ATTEMPTS_EVENT
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 |
|---|---|---|---|
id.applicationName | eq |
| field:"id.applicationName" kind:eq value:"mobile" |
name | eq |
| field:"name" kind:eq value:"FAILED_PASSWORD_ATTEMPTS_EVENT" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
email | actor.email |
Response runbook
Verify that these unlock attempts came from the user, and not a malicious actor which has acquired the user's device.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"callerType": "USER",
"email": "homer.simpson@example.io"
},
"id": {
"applicationName": "mobile"
},
"name": "FAILED_PASSWORD_ATTEMPTS_EVENT",
"parameters": {
"FAILED_PASSWD_ATTEMPTS": 100,
"USER_EMAIL": "homer.simpson@example.io"
},
"type": "device_updates"
}
GSuite User Password Leaked
#GSuite reported a user's password has been compromised, so they disabled the account.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | account_disabled_password_leak: Account Disabled (Password Leak) |
Detection logic
PASSWORD_LEAKED_EVENTS = {
"account_disabled_password_leak",
}
def rule(event):
if event.deep_get("id", "applicationName") != "login":
return False
if event.get("type") == "account_warning":
return bool(event.get("name") in PASSWORD_LEAKED_EVENTS)
return False
def title(event):
user = event.deep_get("parameters", "affected_email_address")
if not user:
user = "<UNKNOWN_USER>"
return f"User [{user}]'s account was disabled due to a password leak"
Rule specification
AnalysisType: rule
Filename: gsuite_leaked_password.py
RuleID: "GSuite.LeakedPassword"
DisplayName: "GSuite User Password Leaked"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
- Credential Access:Unsecured Credentials
Reports:
MITRE ATT&CK:
- TA0006:T1552
Severity: High
Description: >
GSuite reported a user's password has been compromised, so they disabled the account.
Reference: https://support.google.com/a/answer/2984349?hl=en#zippy=%2Cstep-temporarily-suspend-the-suspected-compromised-user-account%2Cstep-investigate-the-account-for-unauthorized-activity%2Cstep-revoke-access-to-the-affected-account%2Cstep-return-access-to-the-user-again%2Cstep-enroll-in--step-verification-with-security-keys%2Cstep-add-secure-or-update-recovery-options%2Cstep-enable-account-activity-alerts
Runbook: >
GSuite has already disabled the compromised user's account. Consider investigating how the user's account was compromised, and reset their account and password. Advise the user to change any other passwords in use that are the sae as the compromised password.
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameislogintypeisaccount_warningnameis one ofaccount_disabled_password_leak
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
id.applicationName | eq |
| field:"id.applicationName" kind:eq value:"login" |
name | in |
| field:"name" kind:in value:"account_disabled_password_leak" |
type | eq |
| field:"type" kind:eq value:"account_warning" |
Response runbook
GSuite has already disabled the compromised user's account. Consider investigating how the user's account was compromised, and reset their account and password. Advise the user to change any other passwords in use that are the sae as the compromised password.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"id": {
"applicationName": "login"
},
"name": "account_disabled_password_leak",
"parameters": {
"affected_email_address": "homer.simpson@example.com"
},
"type": "account_warning"
}
GSuite User Suspended
#A GSuite user was suspended, the account may have been compromised by a spam network.
Telemetry coverage
Detection logic
from panther_gsuite_helpers import gsuite_activityevent_alert_context
USER_SUSPENDED_EVENTS = {
"account_disabled_generic",
"account_disabled_spamming_through_relay",
"account_disabled_spamming",
"account_disabled_hijacked",
}
def rule(event):
if event.deep_get("id", "applicationName") != "login":
return False
return bool(event.get("name") in USER_SUSPENDED_EVENTS)
def title(event):
user = event.deep_get("parameters", "affected_email_address")
if not user:
user = "<UNKNOWN_USER>"
return f"User [{user}]'s account was disabled"
def alert_context(event):
return gsuite_activityevent_alert_context(event)
Rule specification
AnalysisType: rule
Filename: gsuite_user_suspended.py
RuleID: "GSuite.UserSuspended"
DisplayName: "GSuite User Suspended"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
Severity: High
Description: >
A GSuite user was suspended, the account may have been compromised by a spam network.
Reference: https://support.google.com/drive/answer/40695?hl=en&sjid=864417124752637253-EU
Runbook: >
Investigate the behavior that got the account suspended. Verify with the user that this intended behavior. If not, the account may have been compromised.
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameisloginnameis one ofaccount_disabled_generic,account_disabled_spamming_through_relay,account_disabled_spamming,account_disabled_hijacked
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
id.applicationName | eq |
| field:"id.applicationName" kind:eq value:"login" |
name | in |
| field:"name" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
actor | actor.email |
applicationName | id.applicationName |
name | |
type | |
parameters |
Response runbook
Investigate the behavior that got the account suspended. Verify with the user that this intended behavior. If not, the account may have been compromised.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"id": {
"applicationName": "login"
},
"kind": "admin#reports#activity",
"name": "account_disabled_spamming",
"parameters": {
"affected_email_address": "bobert@ext.runpanther.io"
},
"type": "account_warning"
}
GSuite User Two Step Verification Change
#A user disabled two step verification for themselves.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Defense Impairment | |
| Credential Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | 2sv_disable: 2-Step Verification Disabled |
Rules detecting the same action
These rules filter on the same operation.
Detection logic
def rule(event):
if event.deep_get("id", "applicationName") != "user_accounts":
return False
if event.get("type") == "2sv_change" and event.get("name") == "2sv_disable":
return True
return False
def title(event):
return (
f"Two step verification was disabled for user"
f" [{event.deep_get('actor', 'email', default='<UNKNOWN_USER>')}]"
)
Rule specification
AnalysisType: rule
Filename: gsuite_two_step_verification.py
RuleID: "GSuite.TwoStepVerification"
DisplayName: "GSuite User Two Step Verification Change"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
- Defense Evasion:Modify Authentication Process
Reports:
MITRE ATT&CK:
- TA0005:T1556
Severity: Low
Description: >
A user disabled two step verification for themselves.
Reference: https://support.google.com/mail/answer/185839?hl=en&co=GENIE.Platform%3DDesktop&sjid=864417124752637253-EU
Runbook: >
Depending on company policy, either suggest or require the user re-enable two step verification.
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameisuser_accountstypeis2sv_changenameis2sv_disable
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
id.applicationName | eq |
| field:"id.applicationName" kind:eq value:"user_accounts" |
name | eq |
| field:"name" kind:eq value:"2sv_disable" |
type | eq |
| field:"type" kind:eq value:"2sv_change" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
email | actor.email |
Response runbook
Depending on company policy, either suggest or require the user re-enable two step verification.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"callerType": "USER",
"email": "some.user@somedomain.com"
},
"id": {
"applicationName": "user_accounts"
},
"kind": "admin#reports#activity",
"name": "2sv_disable",
"type": "2sv_change"
}
GSuite Workspace Calendar External Sharing Setting Change
#A Workspace Admin Changed The Sharing Settings for Primary Calendars
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Discovery |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | CHANGE_CALENDAR_SETTING: Calendar Setting Change |
Detection logic
from panther_gsuite_helpers import gsuite_activityevent_alert_context
def rule(event):
if not all(
[
(event.get("name", "") == "CHANGE_CALENDAR_SETTING"),
(event.deep_get("parameters", "SETTING_NAME", default="") == "SHARING_OUTSIDE_DOMAIN"),
]
):
return False
return event.deep_get("parameters", "NEW_VALUE", default="") in [
"READ_WRITE_ACCESS",
"READ_ONLY_ACCESS",
"MANAGE_ACCESS",
]
def title(event):
return (
f"GSuite workspace setting for default calendar sharing was changed by "
f"[{event.deep_get('actor', 'email', default='<UNKNOWN_EMAIL>')}] "
+ f"from [{event.deep_get('parameters', 'OLD_VALUE', default='<NO_OLD_SETTING_FOUND>')}] "
+ f"to [{event.deep_get('parameters', 'NEW_VALUE', default='<NO_NEW_SETTING_FOUND>')}]"
)
def alert_context(event):
return gsuite_activityevent_alert_context(event)
Rule specification
AnalysisType: rule
Filename: gsuite_workspace_calendar_external_sharing.py
RuleID: "GSuite.Workspace.CalendarExternalSharingSetting"
DisplayName: "GSuite Workspace Calendar External Sharing Setting Change"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
Reports:
MITRE ATT&CK:
- TA0007:T1087
Severity: Medium
Description: >
A Workspace Admin Changed The Sharing Settings for Primary Calendars
Reference: https://support.google.com/a/answer/60765?hl=en
Runbook: >
Restore the calendar sharing setting to the previous value.
If unplanned, use indicator search to identify other activity from this administrator.
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
nameisCHANGE_CALENDAR_SETTINGparameters.SETTING_NAMEisSHARING_OUTSIDE_DOMAINparameters.NEW_VALUEis one ofREAD_WRITE_ACCESS,READ_ONLY_ACCESS,MANAGE_ACCESS
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
name | eq |
| field:"name" kind:eq value:"CHANGE_CALENDAR_SETTING" |
parameters.NEW_VALUE | in |
| field:"parameters.NEW_VALUE" kind:in |
parameters.SETTING_NAME | eq |
| field:"parameters.SETTING_NAME" kind:eq value:"SHARING_OUTSIDE_DOMAIN" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
actor | actor.email |
applicationName | id.applicationName |
name | |
type | |
parameters | |
OLD_VALUE | parameters.OLD_VALUE |
NEW_VALUE | parameters.NEW_VALUE |
Response runbook
Restore the calendar sharing setting to the previous value. If unplanned, use indicator search to identify other activity from this administrator.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"callerType": "USER",
"email": "example@example.io",
"profileId": "12345"
},
"id": {
"applicationName": "admin",
"customerId": "D12345",
"time": "2022-12-11 01:06:26.303000000",
"uniqueQualifier": "-12345"
},
"ipAddress": "12.12.12.12",
"kind": "admin#reports#activity",
"name": "CHANGE_CALENDAR_SETTING",
"parameters": {
"DOMAIN_NAME": "example.io",
"NEW_VALUE": "READ_ONLY_ACCESS",
"OLD_VALUE": "DEFAULT",
"ORG_UNIT_NAME": "Example IO",
"SETTING_NAME": "SHARING_OUTSIDE_DOMAIN"
},
"type": "CALENDAR_SETTINGS"
}
GSuite Workspace Data Export Has Been Created
#A Workspace Admin Has Created a Data Export
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | CUSTOMER_TAKEOUT_CREATED: Customer Takeout Created |
Detection logic
from panther_gsuite_helpers import gsuite_activityevent_alert_context
def rule(event):
return event.get("name", "").startswith("CUSTOMER_TAKEOUT_")
def title(event):
return (
f"GSuite Workspace Data Export "
f"[{event.get('name', '<NO_EVENT_NAME>')}] "
f"performed by [{event.deep_get('actor', 'email', default='<NO_ACTOR_FOUND>')}]"
)
def alert_context(event):
return gsuite_activityevent_alert_context(event)
Rule specification
AnalysisType: rule
Filename: gsuite_workspace_data_export_created.py
RuleID: "GSuite.Workspace.DataExportCreated"
DisplayName: "GSuite Workspace Data Export Has Been Created"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
Severity: Medium
Description: >
A Workspace Admin Has Created a Data Export
Reference: https://support.google.com/a/answer/100458?hl=en&sjid=864417124752637253-EU
Runbook: |
Verify the intent of this Data Export. If intent cannot be verified, then
a search on the actor's other activities is advised.
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when the condition below holds.
Condition
namestarts withCUSTOMER_TAKEOUT_
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
name | starts_with |
| field:"name" kind:starts_with value:"CUSTOMER_TAKEOUT_" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
actor | actor.email |
applicationName | id.applicationName |
name | |
type | |
parameters |
Response runbook
Verify the intent of this Data Export. If intent cannot be verified, then
a search on the actor's other activities is advised.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"callerType": "USER",
"email": "admin@example.io",
"profileId": "11011111111111111111111"
},
"id": {
"applicationName": "admin",
"customerId": "D12345",
"time": "2022-12-10 22:21:40.079000000",
"uniqueQualifier": "-2833899999999999999"
},
"kind": "admin#reports#activity",
"name": "CUSTOMER_TAKEOUT_CREATED",
"parameters": {
"OBFUSCATED_CUSTOMER_TAKEOUT_REQUEST_ID": "00mmmmmmmmmmmmm"
},
"type": "CUSTOMER_TAKEOUT"
}
GSuite Workspace Gmail Default Routing Rule Modified
#A Workspace Admin Has Modified A Default Routing Rule In Gmail
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry coverage
Detection logic
from panther_gsuite_helpers import gsuite_activityevent_alert_context
def rule(event):
if all(
[
(event.get("type", "") == "EMAIL_SETTINGS"),
(event.get("name", "").endswith("_GMAIL_SETTING")),
(event.deep_get("parameters", "SETTING_NAME", default="") == "MESSAGE_SECURITY_RULE"),
]
):
return True
return False
def title(event):
# Gmail records the event name as DELETE_GMAIL_SETTING/CREATE_GMAIL_SETTING
# We shouldn't be able to enter title() unless event[name] ends with
# _GMAIL_SETTING, and as such change_type assumes the happy path.
change_type = f"{event.get('name', '').split('_')[0].lower()}d"
return (
f"GSuite Gmail Default Routing Rule Was "
f"[{change_type}] "
f"by [{event.deep_get('actor', 'email', default='<UNKNOWN_EMAIL>')}]"
)
def alert_context(event):
return gsuite_activityevent_alert_context(event)
Rule specification
AnalysisType: rule
Filename: gsuite_workspace_gmail_default_routing_rule.py
RuleID: "GSuite.Workspace.GmailDefaultRoutingRuleModified"
DisplayName: "GSuite Workspace Gmail Default Routing Rule Modified"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
Reports:
MITRE ATT&CK:
- TA0003:T1098
Severity: High
Description: >
A Workspace Admin Has Modified A Default Routing Rule In Gmail
Reference: https://support.google.com/a/answer/2368153?hl=en
Runbook: |
Administrators use Default Routing to set up how inbound email is
delivered within an organization. The configuration of the default routing
rule needs to be inspected in order to verify the intent of the rule is benign.
If this change was not planned, inspect the other actions taken by this actor.
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
typeisEMAIL_SETTINGSnameends with_GMAIL_SETTINGparameters.SETTING_NAMEisMESSAGE_SECURITY_RULE
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
name | ends_with |
| field:"name" kind:ends_with value:"_GMAIL_SETTING" |
parameters.SETTING_NAME | eq |
| field:"parameters.SETTING_NAME" kind:eq value:"MESSAGE_SECURITY_RULE" |
type | eq |
| field:"type" kind:eq value:"EMAIL_SETTINGS" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
actor | actor.email |
applicationName | id.applicationName |
name | |
type | |
parameters |
Response runbook
Administrators use Default Routing to set up how inbound email is
delivered within an organization. The configuration of the default routing
rule needs to be inspected in order to verify the intent of the rule is benign.
If this change was not planned, inspect the other actions taken by this actor.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"callerType": "USER",
"email": "user@example.io",
"profileId": "110555555555555555555"
},
"id": {
"applicationName": "admin",
"customerId": "D12345",
"time": "2022-12-11 00:50:03.493000000",
"uniqueQualifier": "-6333333333333333333"
},
"ipAddress": "12.12.12.12",
"kind": "admin#reports#activity",
"name": "CREATE_GMAIL_SETTING",
"parameters": {
"SETTING_NAME": "MESSAGE_SECURITY_RULE",
"USER_DEFINED_SETTING_NAME": "44444"
},
"type": "EMAIL_SETTINGS"
}
GSuite Workspace Gmail Pre-Delivery Message Scanning Disabled
#A Workspace Admin Has Disabled Pre-Delivery Scanning For Gmail.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | CHANGE_APPLICATION_SETTING: Application Setting Change |
Rules detecting the same action
These rules filter on the same operation.
- Application Removed from Blocklist in Google Workspace (Elastic)
- Google Workspace Bitlocker Setting Disabled (Elastic)
- Google Workspace Gmail Routing or Forwarding Rule Created or Modified (Elastic)
- Google Workspace Marketplace Allowlist Configuration (YARA-L)
- Google Workspace Password Policy Changed (YARA-L)
- Google Workspace Password Policy Modified (Elastic)
- Google Workspace Restrictions for Marketplace Modified to Allow Any App (Elastic)
- GSuite Workspace Gmail Security Sandbox Disabled (Panther)
Detection logic
from panther_gsuite_helpers import gsuite_activityevent_alert_context
def rule(event):
# the shape of the items in parameters can change a bit ( like NEW_VALUE can be an array )
# when the applicationName is something other than admin
if event.deep_get("id", "applicationName", default="").lower() != "admin":
return False
if all(
[
(event.get("name", "") == "CHANGE_APPLICATION_SETTING"),
(event.deep_get("parameters", "APPLICATION_NAME", default="").lower() == "gmail"),
(event.deep_get("parameters", "NEW_VALUE", default="").lower() == "true"),
(
event.deep_get("parameters", "SETTING_NAME", default="")
== "DelayedDeliverySettingsProto disable_delayed_delivery_for_suspicious_email"
),
]
):
return True
return False
def title(event):
return (
f"GSuite Gmail Enhanced Pre-Delivery Scanning was disabled "
f"for [{event.deep_get('parameters', 'ORG_UNIT_NAME', default='<NO_ORG_UNIT_NAME>')}] "
f"by [{event.deep_get('actor', 'email', default='<UNKNOWN_EMAIL>')}]"
)
def alert_context(event):
return gsuite_activityevent_alert_context(event)
Rule specification
AnalysisType: rule
Filename: gsuite_workspace_gmail_enhanced_predelivery_scanning.py
RuleID: "GSuite.Workspace.GmailPredeliveryScanningDisabled"
DisplayName: "GSuite Workspace Gmail Pre-Delivery Message Scanning Disabled"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
Reports:
MITRE ATT&CK:
- TA0001:T1566
Severity: Medium
Description: >
A Workspace Admin Has Disabled Pre-Delivery Scanning For Gmail.
Reference: https://support.google.com/a/answer/7380368
Runbook: |
Pre-delivery scanning is a feature in Gmail that subjects suspicious emails
to additional automated scrutiny by Google.
If this change was not intentional, inspect the other actions taken by this actor.
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameisadminnameisCHANGE_APPLICATION_SETTINGparameters.APPLICATION_NAMEisgmailparameters.NEW_VALUEistrueparameters.SETTING_NAMEisDelayedDeliverySettingsProto disable_delayed_delivery_for_suspicious_email
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
id.applicationName | eq |
| field:"id.applicationName" kind:eq value:"admin" |
name | eq |
| field:"name" kind:eq value:"CHANGE_APPLICATION_SETTING" |
parameters.APPLICATION_NAME | eq |
| field:"parameters.APPLICATION_NAME" kind:eq value:"gmail" |
parameters.NEW_VALUE | eq |
| field:"parameters.NEW_VALUE" kind:eq value:"true" |
parameters.SETTING_NAME | eq |
| field:"parameters.SETTING_NAME" kind:eq value:"DelayedDeliverySettingsProto disable_delayed_delivery_for_suspicious_email" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
actor | actor.email |
applicationName | id.applicationName |
name | |
type | |
parameters | |
ORG_UNIT_NAME | parameters.ORG_UNIT_NAME |
Response runbook
Pre-delivery scanning is a feature in Gmail that subjects suspicious emails
to additional automated scrutiny by Google.
If this change was not intentional, inspect the other actions taken by this actor.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"callerType": "USER",
"email": "example@example.io",
"profileId": "12345"
},
"id": {
"applicationName": "admin",
"customerId": "D12345",
"time": "2022-12-11 03:42:54.859000000",
"uniqueQualifier": "-12345"
},
"ipAddress": "12.12.12.12",
"kind": "admin#reports#activity",
"name": "CHANGE_APPLICATION_SETTING",
"parameters": {
"APPLICATION_EDITION": "business_plus_2021",
"APPLICATION_NAME": "Gmail",
"NEW_VALUE": "true",
"ORG_UNIT_NAME": "Example IO",
"SETTING_NAME": "DelayedDeliverySettingsProto disable_delayed_delivery_for_suspicious_email"
},
"type": "APPLICATION_SETTINGS"
}
GSuite Workspace Gmail Security Sandbox Disabled
#A Workspace Admin Has Disabled The Security Sandbox
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | CHANGE_APPLICATION_SETTING: Application Setting Change |
Rules detecting the same action
These rules filter on the same operation.
- Application Removed from Blocklist in Google Workspace (Elastic)
- Google Workspace Bitlocker Setting Disabled (Elastic)
- Google Workspace Gmail Routing or Forwarding Rule Created or Modified (Elastic)
- Google Workspace Marketplace Allowlist Configuration (YARA-L)
- Google Workspace Password Policy Changed (YARA-L)
- Google Workspace Password Policy Modified (Elastic)
- Google Workspace Restrictions for Marketplace Modified to Allow Any App (Elastic)
- GSuite Workspace Gmail Pre-Delivery Message Scanning Disabled (Panther)
Detection logic
from panther_gsuite_helpers import gsuite_activityevent_alert_context
def rule(event):
if event.deep_get("id", "applicationName", default="").lower() != "admin":
return False
if all(
[
(event.get("name", "") == "CHANGE_APPLICATION_SETTING"),
(event.deep_get("parameters", "APPLICATION_NAME", default="").lower() == "gmail"),
(event.deep_get("parameters", "NEW_VALUE", default="").lower() == "false"),
(
event.deep_get("parameters", "SETTING_NAME", default="")
== "AttachmentDeepScanningSettingsProto deep_scanning_enabled"
),
]
):
return True
return False
def title(event):
return (
f"GSuite Gmail Security Sandbox was disabled "
f"for [{event.deep_get('parameters', 'ORG_UNIT_NAME', default='<NO_ORG_UNIT_NAME>')}] "
f"by [{event.deep_get('actor', 'email', default='<UNKNOWN_EMAIL>')}]"
)
def alert_context(event):
return gsuite_activityevent_alert_context(event)
Rule specification
AnalysisType: rule
Filename: gsuite_workspace_gmail_security_sandbox_disabled.py
RuleID: "GSuite.Workspace.GmailSecuritySandboxDisabled"
DisplayName: "GSuite Workspace Gmail Security Sandbox Disabled"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
Reports:
MITRE ATT&CK:
- TA0001:T1566
Severity: Medium
Description: >
A Workspace Admin Has Disabled The Security Sandbox
Reference: https://support.google.com/a/answer/7676854?hl=en#zippy=%2Cfind-security-sandbox-settings%2Cabout-security-sandbox-rules-and-other-scans
Runbook: >
Gmail's Security Sandbox enables rule based scanning of email content.
If this change was not intentional, inspect the other actions taken by this actor.
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameisadminnameisCHANGE_APPLICATION_SETTINGparameters.APPLICATION_NAMEisgmailparameters.NEW_VALUEisfalseparameters.SETTING_NAMEisAttachmentDeepScanningSettingsProto deep_scanning_enabled
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
id.applicationName | eq |
| field:"id.applicationName" kind:eq value:"admin" |
name | eq |
| field:"name" kind:eq value:"CHANGE_APPLICATION_SETTING" |
parameters.APPLICATION_NAME | eq |
| field:"parameters.APPLICATION_NAME" kind:eq value:"gmail" |
parameters.NEW_VALUE | eq |
| field:"parameters.NEW_VALUE" kind:eq value:"false" |
parameters.SETTING_NAME | eq |
| field:"parameters.SETTING_NAME" kind:eq value:"AttachmentDeepScanningSettingsProto deep_scanning_enabled" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
actor | actor.email |
applicationName | id.applicationName |
name | |
type | |
parameters | |
ORG_UNIT_NAME | parameters.ORG_UNIT_NAME |
Response runbook
Gmail's Security Sandbox enables rule based scanning of email content.
If this change was not intentional, inspect the other actions taken by this actor.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"callerType": "USER",
"email": "example@example.io",
"profileId": "12345"
},
"id": {
"applicationName": "admin",
"customerId": "D12345",
"time": "2022-12-11 03:31:41.212000000",
"uniqueQualifier": "-12345"
},
"ipAddress": "12.12.12.12",
"kind": "admin#reports#activity",
"name": "CHANGE_APPLICATION_SETTING",
"parameters": {
"APPLICATION_EDITION": "enterprise",
"APPLICATION_NAME": "Gmail",
"NEW_VALUE": "false",
"ORG_UNIT_NAME": "Example IO",
"SETTING_NAME": "AttachmentDeepScanningSettingsProto deep_scanning_enabled"
},
"type": "APPLICATION_SETTINGS"
}
GSuite Workspace Password Reuse Has Been Enabled
#A Workspace Admin Has Enabled Password Reuse
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | CHANGE_APPLICATION_SETTING: Application Setting Change |
Rules detecting the same action
These rules filter on the same operation.
- Application Removed from Blocklist in Google Workspace (Elastic)
- Google Workspace Bitlocker Setting Disabled (Elastic)
- Google Workspace Gmail Routing or Forwarding Rule Created or Modified (Elastic)
- Google Workspace Marketplace Allowlist Configuration (YARA-L)
- Google Workspace Password Policy Changed (YARA-L)
- Google Workspace Password Policy Modified (Elastic)
- Google Workspace Restrictions for Marketplace Modified to Allow Any App (Elastic)
- GSuite Workspace Gmail Pre-Delivery Message Scanning Disabled (Panther)
Detection logic
from panther_gsuite_helpers import gsuite_activityevent_alert_context
def rule(event):
if event.deep_get("id", "applicationName", default="").lower() != "admin":
return False
if all(
[
(event.get("name", "") == "CHANGE_APPLICATION_SETTING"),
(event.get("type", "") == "APPLICATION_SETTINGS"),
(event.deep_get("parameters", "NEW_VALUE", default="").lower() == "true"),
(
event.deep_get("parameters", "SETTING_NAME", default="")
== "Password Management - Enable password reuse"
),
]
):
return True
return False
def title(event):
return (
f"GSuite Workspace Password Reuse Has Been Enabled "
f"By [{event.deep_get('actor', 'email', default='<NO_ACTOR_FOUND>')}]"
)
def alert_context(event):
return gsuite_activityevent_alert_context(event)
Rule specification
AnalysisType: rule
Filename: gsuite_workspace_password_reuse_enabled.py
RuleID: "GSuite.Workspace.PasswordReuseEnabled"
DisplayName: "GSuite Workspace Password Reuse Has Been Enabled"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
Severity: High
Reports:
MITRE ATT&CK:
- TA0006:T1110
Description: >
A Workspace Admin Has Enabled Password Reuse
Reference: https://support.google.com/a/answer/139399?hl=en#
Runbook: |
Verify the intent of this Password Reuse Setting Change. If intent cannot be verified, then
a search on the actor's other activities is advised.
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameisadminnameisCHANGE_APPLICATION_SETTINGtypeisAPPLICATION_SETTINGSparameters.NEW_VALUEistrueparameters.SETTING_NAMEisPassword Management - Enable password reuse
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
id.applicationName | eq |
| field:"id.applicationName" kind:eq value:"admin" |
name | eq |
| field:"name" kind:eq value:"CHANGE_APPLICATION_SETTING" |
parameters.NEW_VALUE | eq |
| field:"parameters.NEW_VALUE" kind:eq value:"true" |
parameters.SETTING_NAME | eq |
| field:"parameters.SETTING_NAME" kind:eq value:"Password Management - Enable password reuse" |
type | eq |
| field:"type" kind:eq value:"APPLICATION_SETTINGS" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
actor | actor.email |
applicationName | id.applicationName |
name | |
type | |
parameters |
Response runbook
Verify the intent of this Password Reuse Setting Change. If intent cannot be verified, then
a search on the actor's other activities is advised.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"callerType": "USER",
"email": "example@example.io",
"profileId": "12345"
},
"id": {
"applicationName": "admin",
"customerId": "D12345",
"time": "2022-12-11 01:18:47.973000000",
"uniqueQualifier": "-12345"
},
"ipAddress": "12.12.12.12",
"kind": "admin#reports#activity",
"name": "CHANGE_APPLICATION_SETTING",
"parameters": {
"APPLICATION_EDITION": "standard",
"APPLICATION_NAME": "Security",
"NEW_VALUE": "true",
"OLD_VALUE": "false",
"ORG_UNIT_NAME": "Example IO",
"SETTING_NAME": "Password Management - Enable password reuse"
},
"type": "APPLICATION_SETTINGS"
}
GSuite Workspace Strong Password Enforcement Has Been Disabled
#A Workspace Admin Has Disabled The Enforcement Of Strong Passwords
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | CHANGE_APPLICATION_SETTING: Application Setting Change |
Rules detecting the same action
These rules filter on the same operation.
- Application Removed from Blocklist in Google Workspace (Elastic)
- Google Workspace Bitlocker Setting Disabled (Elastic)
- Google Workspace Gmail Routing or Forwarding Rule Created or Modified (Elastic)
- Google Workspace Marketplace Allowlist Configuration (YARA-L)
- Google Workspace Password Policy Changed (YARA-L)
- Google Workspace Password Policy Modified (Elastic)
- Google Workspace Restrictions for Marketplace Modified to Allow Any App (Elastic)
- GSuite Workspace Gmail Pre-Delivery Message Scanning Disabled (Panther)
Detection logic
from panther_gsuite_helpers import gsuite_activityevent_alert_context
def rule(event):
if event.deep_get("id", "applicationName", default="").lower() != "admin":
return False
if all(
[
(event.get("name", "") == "CHANGE_APPLICATION_SETTING"),
(event.get("type", "") == "APPLICATION_SETTINGS"),
(event.deep_get("parameters", "NEW_VALUE", default="").lower() == "off"),
(
event.deep_get("parameters", "SETTING_NAME", default="")
== "Password Management - Enforce strong password"
),
]
):
return True
return False
def title(event):
return (
f"GSuite Workspace Strong Password Enforcement Has Been Disabled "
f"By [{event.deep_get('actor', 'email', default='<NO_ACTOR_FOUND>')}]"
)
def alert_context(event):
return gsuite_activityevent_alert_context(event)
Rule specification
AnalysisType: rule
Filename: gsuite_workspace_password_enforce_strong_disabled.py
RuleID: "GSuite.Workspace.PasswordEnforceStrongDisabled"
DisplayName: "GSuite Workspace Strong Password Enforcement Has Been Disabled"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
Severity: High
Reports:
MITRE ATT&CK:
- TA0006:T1110
Description: >
A Workspace Admin Has Disabled The Enforcement Of Strong Passwords
Reference: https://support.google.com/a/answer/139399?hl=en
Runbook: |
Verify the intent of this Password Strength Setting Change. If intent cannot be verified, then
a search on the actor's other activities is advised.
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameisadminnameisCHANGE_APPLICATION_SETTINGtypeisAPPLICATION_SETTINGSparameters.NEW_VALUEisoffparameters.SETTING_NAMEisPassword Management - Enforce strong password
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
id.applicationName | eq |
| field:"id.applicationName" kind:eq value:"admin" |
name | eq |
| field:"name" kind:eq value:"CHANGE_APPLICATION_SETTING" |
parameters.NEW_VALUE | eq |
| field:"parameters.NEW_VALUE" kind:eq value:"off" |
parameters.SETTING_NAME | eq |
| field:"parameters.SETTING_NAME" kind:eq value:"Password Management - Enforce strong password" |
type | eq |
| field:"type" kind:eq value:"APPLICATION_SETTINGS" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
actor | actor.email |
applicationName | id.applicationName |
name | |
type | |
parameters |
Response runbook
Verify the intent of this Password Strength Setting Change. If intent cannot be verified, then
a search on the actor's other activities is advised.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"callerType": "USER",
"email": "user@example.io",
"profileId": "110111111111111111111"
},
"id": {
"applicationName": "admin",
"customerId": "D12345",
"time": "2022-12-11 01:33:56.306000000",
"uniqueQualifier": "-6444444444444444444"
},
"ipAddress": "12.12.12.12",
"kind": "admin#reports#activity",
"name": "CHANGE_APPLICATION_SETTING",
"parameters": {
"APPLICATION_EDITION": "enterprise",
"APPLICATION_NAME": "Security",
"NEW_VALUE": "off",
"OLD_VALUE": "on",
"ORG_UNIT_NAME": "Example IO",
"SETTING_NAME": "Password Management - Enforce strong password"
},
"type": "APPLICATION_SETTINGS"
}
GSuite Workspace Trusted Domain Allowlist Modified
#A Workspace Admin Has Modified The Trusted Domains List
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry coverage
Detection logic
from panther_gsuite_helpers import gsuite_activityevent_alert_context
def rule(event):
return event.get("type") == "DOMAIN_SETTINGS" and event.get("name", "").endswith(
"_TRUSTED_DOMAINS"
)
def title(event):
return (
f"GSuite Workspace Trusted Domains Modified "
f"[{event.get('name', '<NO_EVENT_NAME>')}] "
f"with [{event.deep_get('parameters', 'DOMAIN_NAME', default='<NO_DOMAIN_NAME>')}] "
f"performed by [{event.deep_get('actor', 'email', default='<NO_ACTOR_FOUND>')}]"
)
def alert_context(event):
return gsuite_activityevent_alert_context(event)
Rule specification
AnalysisType: rule
Filename: gsuite_workspace_trusted_domains_allowlist.py
RuleID: "GSuite.Workspace.TrustedDomainsAllowlist"
DisplayName: "GSuite Workspace Trusted Domain Allowlist Modified"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
Severity: Medium
Description: >
A Workspace Admin Has Modified The Trusted Domains List
Reference: https://support.google.com/a/answer/6160020?hl=en&sjid=864417124752637253-EU
Runbook: |
Verify the intent of this modification. If intent cannot be verified, then
an indicator search on the actor is advised.
SummaryAttributes:
- actor:email
Reports:
MITRE ATT&CK:
- TA0003:T1098
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
typeisDOMAIN_SETTINGSnameends with_TRUSTED_DOMAINS
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
name | ends_with |
| field:"name" kind:ends_with value:"_TRUSTED_DOMAINS" |
type | eq |
| field:"type" kind:eq value:"DOMAIN_SETTINGS" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
actor | actor.email |
applicationName | id.applicationName |
name | |
type | |
parameters | |
DOMAIN_NAME | parameters.DOMAIN_NAME |
Response runbook
Verify the intent of this modification. If intent cannot be verified, then
an indicator search on the actor is advised.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"callerType": "USER",
"email": "user@example.io",
"profileId": "110506209185950390992"
},
"id": {
"applicationName": "admin",
"customerId": "D12345",
"time": "2022-12-11 00:01:34.643000000",
"uniqueQualifier": "-2972206985263071668"
},
"kind": "admin#reports#activity",
"name": "REMOVE_TRUSTED_DOMAINS",
"p_source_label": "Staging",
"parameters": {
"DOMAIN_NAME": "evilexample.com"
},
"type": "DOMAIN_SETTINGS"
}
Malware Detected in Email
#Detects when malware is found in an email received by a user. Identifies different malware families including known malicious programs, viruses, worms, harmful content, and unwanted content. Severity is dynamically assigned based on the malware type, with known malicious programs and viruses triggering high-severity alerts.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | any: Gmail (any event) |
Rules detecting the same action
These rules filter on the same operation.
- Gmail Malicious SMTP Response (Panther)
- Gmail Potential Spoofed Email Delivered (Panther)
- gmail spike in undeliverables (YARA-L)
- Gsuite Attachments Downloaded from Spam Email (Panther)
- Gsuite Email Bypassed Spam Filter (Panther)
- Gsuite Link Clicked in Spam Email (Panther)
- Spam Email Surge (Panther)
Detection logic
# Malware family type mapping based on Google Workspace Gmail schema
# Reference: https://support.google.com/a/answer/12384955
MALWARE_FAMILY_TYPES = {
1: "Known malicious program",
2: "Virus or worm",
3: "Possible harmful message content",
4: "Possible unwanted message content",
5: "Other malware type",
}
def rule(event):
if event.deep_get("id", "applicationName", default="<UNKNOWN_APPLICATION>") != "gmail":
return False
# Check if malware was detected in the message
malware_family = event.deep_get(
"parameters", "message_info", "attachment", "malware_family", default=None
)
return malware_family is not None
def title(event):
user = event.deep_get("actor", "email", default="<UNKNOWN_USER>")
malware_family = event.deep_get(
"parameters", "message_info", "attachment", "malware_family", default=0
)
malware_type = MALWARE_FAMILY_TYPES.get(malware_family, f"Unknown Type ({malware_family})")
malware_sha256 = event.deep_get(
"parameters", "message_info", "attachment", "sha256", default="<UNKNOWN_SHA256>"
)
return (
f"Malicious attachment of type [{malware_type}] "
f"with SHA256 [{malware_sha256}] "
f"detected in email to [{user}]"
)
def alert_context(event):
malware_family = event.deep_get(
"parameters", "message_info", "attachment", "malware_family", default=0
)
malware_sha256 = event.deep_get(
"parameters", "message_info", "attachment", "sha256", default="<UNKNOWN_SHA256>"
)
filename = event.deep_get(
"parameters", "message_info", "attachment", "file_name", default="<UNKNOWN_FILENAME>"
)
context = {
"recipient": event.deep_get("actor", "email", default="<UNKNOWN_USER>"),
"malware_family_code": malware_family,
"malware_type": MALWARE_FAMILY_TYPES.get(malware_family, "Unknown"),
"source_ip": event.get("ipAddress"),
"sha256": malware_sha256,
"filename": filename,
}
return context
Rule specification
AnalysisType: rule
Filename: gsuite_malware_in_email.py
RuleID: "GSuite.Gmail.Malware.In.Email"
DisplayName: "Malware Detected in Email"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
- Gmail
- Malware
- Initial Access
Reports:
MITRE ATT&CK:
- TA0001:T1566.001 # Initial Access: Phishing - Spearphishing Attachment
- TA0011:T1204.002 # Execution: User Execution - Malicious File
Severity: High
Description: >
Detects when malware is found in an email received by a user. Identifies different malware families including known malicious programs, viruses, worms, harmful content, and unwanted content. Severity is dynamically assigned based on the malware type, with known malicious programs and viruses triggering high-severity alerts.
Runbook: |
1. Review the malware type and affected user
2. Check if the email was quarantined or delivered
3. Verify if the user interacted with the email or opened attachments
4. Check for similar emails to other users in the organization
5. Consider blocking the sender domain if appropriate
6. Notify the affected user and provide security awareness guidance
Reference: https://support.google.com/a/answer/12384955
DedupPeriodMinutes: 60
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameisgmailparameters.message_info.attachment.malware_familyis 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 |
|---|---|
recipient | actor.email |
malware_family_code | parameters.message_info.attachment.malware_family |
source_ip | ipAddress |
sha256 | parameters.message_info.attachment.sha256 |
filename | parameters.message_info.attachment.file_name |
Response runbook
1. Review the malware type and affected user
2. Check if the email was quarantined or delivered
3. Verify if the user interacted with the email or opened attachments
4. Check for similar emails to other users in the organization
5. Consider blocking the sender domain if appropriate
6. Notify the affected user and provide security awareness guidance
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"callerType": "USER",
"email": "oliver@justice.org",
"profileId": "123456789"
},
"id": {
"applicationName": "gmail",
"customerId": "C12345",
"time": "2025-11-04 20:44:43.248000000",
"uniqueQualifier": "-123456789"
},
"ipAddress": "1.1.1.1",
"kind": "admin#reports#activity",
"name": "delivery",
"p_any_actor_ids": [
"1234567891234"
],
"p_any_domain_names": [
"malicious-sender.com"
],
"p_any_ip_addresses": [
"1.1.1.1"
],
"p_event_time": "2025-11-04 20:44:43.248000000",
"p_log_type": "GSuite.ActivityEvent",
"p_parse_time": "2025-11-04 20:49:46.688935963",
"p_row_id": "0000000000de09c1dc6f0828cbad2ca5",
"p_schema_version": 0,
"p_source_id": "7ee69d4d-df1b-40b3-b5e8-6826dee34b1c",
"p_source_label": "Google Workspace",
"parameters": {
"event_info": {
"elapsed_time_usec": 368746,
"timestamp_usec": 1730751883248347
},
"message_info": {
"action_type": 19,
"attachment": {
"file_name": "invoice.exe",
"malware_family": 1,
"sha256": "000000000045c5798d026b67c03d54273fd0996f5cb789d0a959dac0c7cc456c"
},
"link_domain": [
"malicious-sender.com"
],
"num_message_attachments": 1,
"payload_size": 54321,
"subject": "Important Invoice Attached"
}
},
"type": "delivery_type"
}
Spam Email Surge
#Detects a high number of spam emails received by a single user in a short timeframe. This could indicate the user's email has appeared in data leaks and is being targeted for spam.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance | |
| Initial Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Google Workspace | any: Gmail (any event) |
Rules detecting the same action
These rules filter on the same operation.
- Gmail Malicious SMTP Response (Panther)
- Gmail Potential Spoofed Email Delivered (Panther)
- gmail spike in undeliverables (YARA-L)
- Gsuite Attachments Downloaded from Spam Email (Panther)
- Gsuite Email Bypassed Spam Filter (Panther)
- Gsuite Link Clicked in Spam Email (Panther)
- Malware Detected in Email (Panther)
Detection logic
from panther_gsuite_helpers import gsuite_activityevent_alert_context
def rule(event):
if event.deep_get("id", "applicationName", default="<UNKNOWN_APPLICATION>") != "gmail":
return False
# Exclude domain-level actor
if "/hd/domain/" in event.deep_get("actor", "email"):
return False
return event.deep_get("parameters", "message_info", "is_spam", default=False) is True
def title(event):
user = event.deep_get("actor", "email", default="<UNKNOWN_USER>")
return f"Surge in spam emails received by user [{user}]"
def alert_context(event):
return gsuite_activityevent_alert_context(event)
Rule specification
AnalysisType: rule
Filename: gsuite_spam_email.py
RuleID: "GSuite.Gmail.Spam.Email.Surge"
DisplayName: "Spam Email Surge"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
Reports:
MITRE ATT&CK:
- TA0001:T1566 # Initial Access: Phishing
- TA0043:T1598 # Reconnaissance: Phishing for Information
Severity: Medium
Status: Experimental
Description: >
Detects a high number of spam emails received by a single user in a short timeframe. This could indicate the user's email has appeared in data leaks and is being targeted for spam.
Threshold: 50
DedupPeriodMinutes: 60
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameisgmailactor.emaildoes not contain/hd/domain/parameters.message_info.is_spamistrue
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
actor.email | contains | /hd/domain/ | excludes:actor.email field:"actor.email" value:"/hd/domain/" |
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 |
|---|---|
actor | actor.email |
applicationName | id.applicationName |
name | |
type | |
parameters |
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"callerType": "USER",
"email": "denethor@lotr.com",
"profileId": "123456789"
},
"id": {
"applicationName": "gmail",
"customerId": "1A2B3C",
"time": "2025-11-04 20:44:43.248000000",
"uniqueQualifier": "-123456789"
},
"ipAddress": "1.1.1.1",
"kind": "admin#reports#activity",
"name": "delivery",
"p_any_actor_ids": [
"1234567891234"
],
"p_any_domain_names": [
"evil.com"
],
"p_any_ip_addresses": [
"1.1.1.1"
],
"p_event_time": "2025-11-04 20:44:43.248000000",
"p_log_type": "GSuite.ActivityEvent",
"p_parse_time": "2025-11-04 20:49:46.688935963",
"p_row_id": "0000000000de09c1dc6f0828cbad2ca5",
"p_schema_version": 0,
"p_source_id": "7ee69d4d-df1b-40b3-b5e8-6826dee34b1c",
"p_source_label": "Google Workspace",
"p_udm": {
"source": {
"address": "1.1.1.1",
"ip": "1.1.1.1"
},
"user": {
"provider_id": "123456789"
}
},
"parameters": {
"event_info": {
"elapsed_time_usec": 368746,
"timestamp_usec": 1762289083248347
},
"message_info": {
"action_type": 19,
"flattened_destinations": "gmail-for-work-catchall::denethor@lotr.com",
"is_spam": true,
"link_domain": [
"evil.com"
],
"payload_size": 12345,
"subject": "You won 1 Million Dollar"
}
},
"type": "delivery_type"
}
Suspicious GSuite Login
#GSuite reported a suspicious login for this user.
Telemetry coverage
Detection logic
SUSPICIOUS_LOGIN_TYPES = {
"suspicious_login",
"suspicious_login_less_secure_app",
"suspicious_programmatic_login",
}
def rule(event):
if event.deep_get("id", "applicationName") != "login":
return False
if event.get("name") in SUSPICIOUS_LOGIN_TYPES:
return True
return False
def title(event):
user = event.deep_get("actor", "email") or event.deep_get(
"parameters", "affected_email_address", default="<UNKNOWN_USER>"
)
return f"A suspicious login was reported for user [{user}]"
Rule specification
AnalysisType: rule
Filename: gsuite_suspicious_logins.py
RuleID: "GSuite.SuspiciousLogins"
DisplayName: "Suspicious GSuite Login"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
Severity: Medium
Description: >
GSuite reported a suspicious login for this user.
Reference: https://support.google.com/a/answer/7102416?hl=en
Runbook: >
Checkout the details of the login and verify this behavior with the user to ensure the account wasn't compromised.
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when all of the conditions below hold.
Condition
id.applicationNameisloginnameis one ofsuspicious_login,suspicious_login_less_secure_app,suspicious_programmatic_login
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
id.applicationName | eq |
| field:"id.applicationName" kind:eq value:"login" |
name | in |
| field:"name" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
email | actor.email |
Response runbook
Checkout the details of the login and verify this behavior with the user to ensure the account wasn't compromised.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"id": {
"applicationName": "login"
},
"kind": "admin#reports#activity",
"name": "suspicious_login",
"parameters": {
"affected_email_address": "bobert@ext.runpanther.io"
},
"type": "account_warning"
}
Suspicious is_suspicious tag
#GSuite reported a suspicious activity for this user.
Detection logic
def rule(event):
return event.deep_get("parameters", "is_suspicious") is True
def title(event):
user = event.deep_get("actor", "email", default="<UNKNOWN_USER>")
return f"A suspicious action was reported for user [{user}]"
Rule specification
AnalysisType: rule
Filename: gsuite_is_suspicious_tag.py
RuleID: "GSuite.IsSuspiciousTag"
DisplayName: "Suspicious is_suspicious tag"
Enabled: true
LogTypes:
- GSuite.ActivityEvent
Tags:
- GSuite
Status: Experimental
Severity: Info # Will be Medium in the future
Description: >
GSuite reported a suspicious activity for this user.
Reference: https://support.google.com/a/answer/7102416?hl=en
Runbook: >
Checkout the details of the activity and verify this behavior with the user to ensure the account wasn't compromised.
SummaryAttributes:
- actor:email
Stages and Predicates
Fires on GSuite.ActivityEvent events when the condition below holds.
Condition
parameters.is_suspiciousistrue
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
parameters.is_suspicious | eq |
| field:"parameters.is_suspicious" kind:eq value:"true" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
email | actor.email |
Response runbook
Checkout the details of the activity and verify this behavior with the user to ensure the account wasn't compromised.
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"actor": {
"email": "bobert@ext.runpanther.io"
},
"id": {
"applicationName": "login"
},
"kind": "admin#reports#activity",
"name": "login_success",
"parameters": {
"affected_email_address": "bobert@ext.runpanther.io",
"is_suspicious": true
},
"type": "login"
}