GSuite External Drive Document
Description
A Google drive resource became externally accessible.
Query · python
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"
Analyst notes
Investigate whether the drive document is appropriate to be publicly accessible.