External GSuite File Share


Description

An employee shared a sensitive file externally with another organization

Query · python

import datetime

from panther_base_helpers import pattern_match, pattern_match_list

COMPANY_DOMAIN = "your-company-name.com"
EXCEPTION_PATTERNS = {
    # The glob pattern for the document title (lowercased)
    "1 document title p*": {  # allow any title "all"
        "allowed_to_send": {
            "alice@acme.com",
            "samuel@acme.com",
            "nathan@acme.com",
            "barry@acme.com",
            # Allow any user
            # "all"
            # Allow any user in a specific domain
            # "*@acme.com"
        },
        "allowed_to_receive": {
            "alice@abc.com",
            "samuel@abc.com",
            "nathan@abc.com",
            "barry@abc.com",
            # Allow any user
            # "all"
            # Allow any user in a specific domain
            # "*@acme.com"
        },
        # The time limit for how long the file share stays valid
        "allowed_until": datetime.datetime(year=2030, month=6, day=2),
    },
    "2 document title p*": {
        "allowed_to_send": {
            "alice@abc.com",
        },
        "allowed_to_receive": {
            "*@acme.com",
        },
        # The time limit for how long the file share stays valid
        "allowed_until": datetime.datetime(year=2030, month=6, day=2),
    },
}


def _check_acl_change_event(actor_email, event):
    # For GSuite.ActivityEvent, parameters is a dict, not an array
    parameters = event.get("parameters", {})

    doc_title = parameters.get("doc_title", "TITLE_UNKNOWN")
    old_visibility = parameters.get("old_visibility", "OLD_VISIBILITY_UNKNOWN")
    new_visibility = parameters.get("visibility", "NEW_VISIBILITY_UNKNOWN")
    target_user = parameters.get("target_user") or parameters.get("target_domain") or "USER_UNKNOWN"
    current_time = datetime.datetime.now()

    if (
        new_visibility == "shared_externally"
        and old_visibility == "private"
        and not target_user.endswith(f"@{COMPANY_DOMAIN}")
    ):
        # This is a dangerous share, check exceptions:

        for pattern, details in EXCEPTION_PATTERNS.items():
            proper_title = pattern_match(doc_title.lower(), pattern) or pattern == "all"

            proper_sender = pattern_match_list(
                actor_email, details.get("allowed_to_send")
            ) or details.get("allowed_to_send") == {"all"}

            proper_receiver = pattern_match_list(
                target_user, details.get("allowed_to_receive")
            ) or details.get("allowed_to_receive") == {"all"}

            if (
                proper_title
                and proper_sender
                and proper_receiver
                and current_time < details.get("allowed_until")
            ):
                return False
        # No exceptions match.
        # Return the event summary (which is True) to alert & use in title.
        return {
            "actor": actor_email,
            "doc_title": doc_title,
            "target_user": target_user,
        }
    return False


def rule(event):
    application_name = event.deep_get("id", "applicationName")
    actor_email = event.deep_get("actor", "email", default="EMAIL_UNKNOWN")

    # For GSuite.ActivityEvent, each log is a single event (no events array)
    if application_name == "drive" and event.get("type") == "acl_change":
        # If this event is a dangerous file share, alert:
        return bool(_check_acl_change_event(actor_email, event))
    return False


def title(event):
    actor_email = event.deep_get("actor", "email", default="EMAIL_UNKNOWN")
    matching_event = _check_acl_change_event(actor_email, event)

    if matching_event:
        actor = matching_event.get("actor", "ACTOR_UNKNOWN")
        doc_title = matching_event.get("doc_title", "DOC_TITLE_UNKNOWN")
        target_user = matching_event.get("target_user", "USER_UNKNOWN")
        return f'Dangerous file share by [{actor}]: "{doc_title}" to {target_user}'
    return "No matching events, but DangerousShares still fired"

Analyst notes

Contact the employee who made the share and make sure they redact the access. If the share was legitimate, add to the EXCEPTION_PATTERNS in the detection.

Raw source External GSuite File Share · Panther Python
Esc
Published by panther-labs/panther-analysis ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
AnalysisType: rule
Filename: gsuite_drive_external_share.py
RuleID: "GSuite.Drive.ExternalFileShare"
DisplayName: "External GSuite File Share"
Enabled: false
LogTypes:
  - GSuite.ActivityEvent
Tags:
  - GSuite
  - Security Control
  - Configuration Required
  - Collection:Data from Information Repositories
Reports:
  MITRE ATT&CK:
    - TA0009:T1213
Severity: High
Description: An employee shared a sensitive file externally with another organization
Runbook: |
  Contact the employee who made the share and make sure they redact the access.
  If the share was legitimate, add to the EXCEPTION_PATTERNS in the detection.
Reference: https://support.google.com/docs/answer/2494822?hl=en&co=GENIE.Platform%3DiOS&sjid=864417124752637253-EU
Tests:
  - Name: Dangerous Share of Known Document with a Missing User
    LogType: GSuite.ActivityEvent
    ExpectedResult: true
    Log:
      {
        "kind": "admin#reports#activity",
        "id":
          {
            "time": "2020-09-07T15:50:49.617Z",
            "uniqueQualifier": "1111111111111111111",
            "applicationName": "drive",
            "customerId": "C010qxghg",
          },
        "actor":
          { "email": "example@acme.com", "profileId": "1111111111111111111" },
        "type": "acl_change",
        "name": "change_user_access",
        "parameters":
          {
            "primary_event": true,
            "visibility_change": "external",
            "target_user": "outside@acme.com",
            "old_visibility": "private",
            "doc_id": "1111111111111111111",
            "doc_type": "document",
            "doc_title": "1 Document Title Primary",
            "visibility": "shared_externally",
            "originating_app_id": "1111111111111111111",
            "owner_is_shared_drive": false,
            "owner_is_team_drive": false,
            "old_value": ["none"],
            "new_value": ["can_edit"],
          },
        "p_log_type": "GSuite.ActivityEvent",
      }
  - Name: Dangerous Share of Unknown Document
    LogType: GSuite.ActivityEvent
    ExpectedResult: true
    Log:
      {
        "kind": "admin#reports#activity",
        "id":
          {
            "time": "2020-09-07T15:50:49.617Z",
            "uniqueQualifier": "1111111111111111111",
            "applicationName": "drive",
            "customerId": "C010qxghg",
          },
        "actor":
          { "email": "example@acme.com", "profileId": "1111111111111111111" },
        "type": "acl_change",
        "name": "change_user_access",
        "parameters":
          {
            "primary_event": true,
            "visibility_change": "external",
            "target_domain": "external.com",
            "old_visibility": "private",
            "doc_id": "1111111111111111111",
            "doc_type": "document",
            "doc_title": "Untitled document",
            "visibility": "shared_externally",
            "originating_app_id": "1111111111111111111",
            "owner_is_shared_drive": false,
            "owner_is_team_drive": false,
            "old_value": ["none"],
            "new_value": ["can_edit"],
          },
        "p_log_type": "GSuite.ActivityEvent",
      }
  - Name: Share Allowed by Exception
    LogType: GSuite.ActivityEvent
    ExpectedResult: false
    Log:
      {
        "kind": "admin#reports#activity",
        "id":
          {
            "time": "2020-07-07T15:50:49.617Z",
            "uniqueQualifier": "1111111111111111111",
            "applicationName": "drive",
            "customerId": "C010qxghg",
          },
        "actor":
          { "email": "alice@acme.com", "profileId": "1111111111111111111" },
        "type": "acl_change",
        "name": "change_user_access",
        "parameters":
          {
            "primary_event": true,
            "billable": true,
            "visibility_change": "external",
            "target_user": "samuel@abc.com",
            "old_visibility": "private",
            "doc_id": "1111111111111111111",
            "doc_type": "document",
            "doc_title": "1 Document Title Pattern",
            "visibility": "shared_externally",
            "originating_app_id": "1111111111111111111",
            "owner_is_shared_drive": false,
            "owner_is_team_drive": false,
            "old_value": ["none"],
            "new_value": ["people_within_domain_with_link"],
          },
        "p_log_type": "GSuite.ActivityEvent",
      }
  - Name: Share Allowed by Exception - 2
    LogType: GSuite.ActivityEvent
    ExpectedResult: false
    Log:
      {
        "kind": "admin#reports#activity",
        "id":
          {
            "time": "2020-07-07T15:50:49.617Z",
            "uniqueQualifier": "1111111111111111111",
            "applicationName": "drive",
            "customerId": "C010qxghg",
          },
        "actor":
          { "email": "alice@abc.com", "profileId": "1111111111111111111" },
        "type": "acl_change",
        "name": "change_user_access",
        "parameters":
          {
            "primary_event": true,
            "billable": true,
            "visibility_change": "external",
            "target_user": "samuel@acme.com",
            "old_visibility": "private",
            "doc_id": "1111111111111111111",
            "doc_type": "document",
            "doc_title": "2 Document Title Pattern",
            "visibility": "shared_externally",
            "originating_app_id": "1111111111111111111",
            "owner_is_shared_drive": false,
            "owner_is_team_drive": false,
            "old_value": ["none"],
            "new_value": ["people_within_domain_with_link"],
          },
        "p_log_type": "GSuite.ActivityEvent",
      }


# ------ paired body: gsuite_drive_external_share.py ------

import datetime

from panther_base_helpers import pattern_match, pattern_match_list

COMPANY_DOMAIN = "your-company-name.com"
EXCEPTION_PATTERNS = {
    # The glob pattern for the document title (lowercased)
    "1 document title p*": {  # allow any title "all"
        "allowed_to_send": {
            "alice@acme.com",
            "samuel@acme.com",
            "nathan@acme.com",
            "barry@acme.com",
            # Allow any user
            # "all"
            # Allow any user in a specific domain
            # "*@acme.com"
        },
        "allowed_to_receive": {
            "alice@abc.com",
            "samuel@abc.com",
            "nathan@abc.com",
            "barry@abc.com",
            # Allow any user
            # "all"
            # Allow any user in a specific domain
            # "*@acme.com"
        },
        # The time limit for how long the file share stays valid
        "allowed_until": datetime.datetime(year=2030, month=6, day=2),
    },
    "2 document title p*": {
        "allowed_to_send": {
            "alice@abc.com",
        },
        "allowed_to_receive": {
            "*@acme.com",
        },
        # The time limit for how long the file share stays valid
        "allowed_until": datetime.datetime(year=2030, month=6, day=2),
    },
}


def _check_acl_change_event(actor_email, event):
    # For GSuite.ActivityEvent, parameters is a dict, not an array
    parameters = event.get("parameters", {})

    doc_title = parameters.get("doc_title", "TITLE_UNKNOWN")
    old_visibility = parameters.get("old_visibility", "OLD_VISIBILITY_UNKNOWN")
    new_visibility = parameters.get("visibility", "NEW_VISIBILITY_UNKNOWN")
    target_user = parameters.get("target_user") or parameters.get("target_domain") or "USER_UNKNOWN"
    current_time = datetime.datetime.now()

    if (
        new_visibility == "shared_externally"
        and old_visibility == "private"
        and not target_user.endswith(f"@{COMPANY_DOMAIN}")
    ):
        # This is a dangerous share, check exceptions:

        for pattern, details in EXCEPTION_PATTERNS.items():
            proper_title = pattern_match(doc_title.lower(), pattern) or pattern == "all"

            proper_sender = pattern_match_list(
                actor_email, details.get("allowed_to_send")
            ) or details.get("allowed_to_send") == {"all"}

            proper_receiver = pattern_match_list(
                target_user, details.get("allowed_to_receive")
            ) or details.get("allowed_to_receive") == {"all"}

            if (
                proper_title
                and proper_sender
                and proper_receiver
                and current_time < details.get("allowed_until")
            ):
                return False
        # No exceptions match.
        # Return the event summary (which is True) to alert & use in title.
        return {
            "actor": actor_email,
            "doc_title": doc_title,
            "target_user": target_user,
        }
    return False


def rule(event):
    application_name = event.deep_get("id", "applicationName")
    actor_email = event.deep_get("actor", "email", default="EMAIL_UNKNOWN")

    # For GSuite.ActivityEvent, each log is a single event (no events array)
    if application_name == "drive" and event.get("type") == "acl_change":
        # If this event is a dangerous file share, alert:
        return bool(_check_acl_change_event(actor_email, event))
    return False


def title(event):
    actor_email = event.deep_get("actor", "email", default="EMAIL_UNKNOWN")
    matching_event = _check_acl_change_event(actor_email, event)

    if matching_event:
        actor = matching_event.get("actor", "ACTOR_UNKNOWN")
        doc_title = matching_event.get("doc_title", "DOC_TITLE_UNKNOWN")
        target_user = matching_event.get("target_user", "USER_UNKNOWN")
        return f'Dangerous file share by [{actor}]: "{doc_title}" to {target_user}'
    return "No matching events, but DangerousShares still fired"

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.