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.

Raw source GSuite External Drive Document · Panther Python
Esc
Published by panther-labs/panther-analysis ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
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
Tests:
  - Name: Access Event
    ExpectedResult: false
    Log:
      {
            "p_log_type": "GSuite.ActivityEvent",
            "p_row_id": "111222",
            "actor": {
                  "email": "bobert@example.com"
            },
            "id": {
                  "applicationName": "drive"
            },
            "type": "access",
            "name": "upload"
      }
  - Name: ACL Change without Visibility Change
    ExpectedResult: false
    Log:
      {
            "p_log_type": "GSuite.ActivityEvent",
            "p_row_id": "111222",
            "actor": {
                  "email": "bobert@example.com"
            },
            "id": {
                  "applicationName": "drive"
            },
            "type": "acl_change",
            "name": "shared_drive_settings_change"
      }
  - Name: Doc Became Public - Link (Unrestricted)
    ExpectedResult: true
    Log:
      {
            "p_log_type": "GSuite.ActivityEvent",
            "p_row_id": "111222",
            "actor": {
                  "email": "bobert@gmail.com"
            },
            "id": {
                  "applicationName": "drive"
            },
            "type": "acl_change",
            "name": "change_document_visibility",
            "parameters": {
                  "visibility_change": "external",
                  "doc_title": "my shared document",
                  "target_domain": "all",
                  "visibility": "people_with_link",
                  "new_value": [
                        "people_with_link"
                  ]
            }
      }
  - Name: Doc Became Public - Link (Allowlisted Domain Not Configured)
    ExpectedResult: true
    Log:
      {
            "p_log_type": "GSuite.ActivityEvent",
            "p_row_id": "111222",
            "actor": {
                  "email": "bobert@example.com"
            },
            "id": {
                  "applicationName": "drive"
            },
            "type": "acl_change",
            "name": "change_document_visibility",
            "parameters": {
                  "visibility_change": "external",
                  "doc_title": "my shared document",
                  "target_domain": "example.com",
                  "visibility": "people_within_domain_with_link",
                  "new_value": [
                        "people_with_link"
                  ]
            }
      }
  - Name: Doc Became Public - Link (Allowlisted Domain Is Configured)
    ExpectedResult: false
    Mocks:
      - objectName: ALLOWED_DOMAINS
        returnValue: "[\n  \"example.com\"\n]"
    Log:
      {
            "p_log_type": "GSuite.ActivityEvent",
            "p_row_id": "111222",
            "actor": {
                  "email": "bobert@example.com"
            },
            "id": {
                  "applicationName": "drive"
            },
            "type": "acl_change",
            "name": "change_document_visibility",
            "parameters": {
                  "visibility_change": "external",
                  "doc_title": "my shared document",
                  "target_domain": "example.com",
                  "visibility": "people_within_domain_with_link",
                  "new_value": [
                        "people_with_link"
                  ]
            }
      }
  - Name: Doc Became Private - Link
    ExpectedResult: false
    Log:
      {
            "p_log_type": "GSuite.ActivityEvent",
            "p_row_id": "111222",
            "actor": {
                  "email": "bobert@example.com"
            },
            "id": {
                  "applicationName": "drive"
            },
            "type": "acl_change",
            "name": "change_document_visibility",
            "parameters": {
                  "visibility_change": "external",
                  "doc_title": "my shared document",
                  "target_domain": "all",
                  "visibility": "people_with_link",
                  "new_value": [
                        "private"
                  ]
            }
      }
  - Name: Doc Became Public - User
    ExpectedResult: true
    Log:
      {
            "p_log_type": "GSuite.ActivityEvent",
            "actor": {
                  "email": "bobert@example.com"
            },
            "id": {
                  "applicationName": "drive"
            },
            "kind": "admin#reports#activity",
            "ipAddress": "1.1.1.1",
            "type": "acl_change",
            "name": "change_user_access",
            "parameters": {
                  "primary_event": true,
                  "visibility_change": "external",
                  "target_user": "someone@random.com",
                  "old_value": [
                        "none"
                  ],
                  "new_value": [
                        "can_view"
                  ],
                  "old_visibility": "people_within_domain_with_link",
                  "doc_title": "Hosted Accounts",
                  "visibility": "shared_externally"
            }
      }
  - Name: Doc Became Public - User (Multiple)
    ExpectedResult: true
    Log:
      {
            "p_log_type": "GSuite.ActivityEvent",
            "actor": {
                  "email": "bobert@example.com"
            },
            "id": {
                  "applicationName": "drive"
            },
            "kind": "admin#reports#activity",
            "ipAddress": "1.1.1.1",
            "type": "acl_change",
            "name": "change_user_access",
            "parameters": {
                  "primary_event": true,
                  "visibility_change": "external",
                  "target_user": "someone@random.com",
                  "old_value": [
                        "none"
                  ],
                  "new_value": [
                        "can_view"
                  ],
                  "old_visibility": "people_within_domain_with_link",
                  "doc_title": "Hosted Accounts",
                  "visibility": "shared_externally"
            }
      }
  - Name: Doc Inherits Folder Permissions
    ExpectedResult: false
    Log:
      {
            "p_log_type": "GSuite.ActivityEvent",
            "p_row_id": "111222",
            "actor": {
                  "email": "bobert@example.com"
            },
            "id": {
                  "applicationName": "drive"
            },
            "type": "acl_change",
            "name": "change_user_access_hierarchy_reconciled",
            "parameters": {
                  "visibility_change": "internal"
            }
      }
  - Name: Doc Inherits Folder Permissions - Sharing Link
    ExpectedResult: false
    Log:
      {
            "p_log_type": "GSuite.ActivityEvent",
            "p_row_id": "111222",
            "actor": {
                  "email": "bobert@example.com"
            },
            "id": {
                  "applicationName": "drive"
            },
            "type": "acl_change",
            "name": "change_document_access_scope_hierarchy_reconciled",
            "parameters": {
                  "visibility_change": "internal"
            }
      }
  - Name: Doc Became Public - Public email provider
    ExpectedResult: true
    Log:
      {
            "p_log_type": "GSuite.ActivityEvent",
            "actor": {
                  "email": "bobert@example.com"
            },
            "id": {
                  "applicationName": "drive"
            },
            "kind": "admin#reports#activity",
            "ipAddress": "1.1.1.1",
            "type": "acl_change",
            "name": "change_user_access",
            "parameters": {
                  "primary_event": true,
                  "visibility_change": "external",
                  "target_user": "someone@yandex.com",
                  "old_value": [
                        "none"
                  ],
                  "new_value": [
                        "can_view"
                  ],
                  "old_visibility": "people_within_domain_with_link",
                  "doc_title": "Hosted Accounts",
                  "visibility": "shared_externally"
            }
      }
  - Name: Doc Shared With Multiple Users All From ALLOWED_DOMAINS
    ExpectedResult: false
    Mocks:
      - objectName: ALLOWED_DOMAINS
        returnValue: "[\n  \"example.com\", \"notexample.com\"\n]"
    Log:
      {
            "p_log_type": "GSuite.ActivityEvent",
            "actor": {
                  "email": "bobert@example.com"
            },
            "id": {
                  "applicationName": "drive"
            },
            "kind": "admin#reports#activity",
            "ipAddress": "1.1.1.1",
            "type": "acl_change",
            "name": "change_user_access",
            "parameters": {
                  "primary_event": true,
                  "visibility_change": "external",
                  "target_user": "someone@notexample.com",
                  "old_value": [
                        "none"
                  ],
                  "new_value": [
                        "can_view"
                  ],
                  "old_visibility": "people_within_domain_with_link",
                  "doc_title": "Hosted Accounts",
                  "visibility": "shared_externally"
            }
      }


# ------ paired body: gsuite_drive_visibility_change.py ------

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"

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.