Detection rules › Panther

Panther rules: notion

Notion Audit Log Exported

#
Severity
medium
Log types
Notion.AuditLogs
Tags
Notion, Data Security, Data Exfiltration
Reference
www.notion.so
Source
github.com/panther-labs/panther-analysis

A Notion User exported audit logs for your organization’s workspace.

Detection logic

from panther_notion_helpers import notion_alert_context


def rule(event):

    event_type = event.deep_get("event", "type", default="<NO_EVENT_TYPE_FOUND>")
    return event_type == "workspace.audit_log_exported"


def title(event):
    user = event.deep_get("event", "actor", "person", "email", default="<NO_USER_FOUND>")
    workspace_id = event.deep_get("event", "workspace_id", default="<NO_WORKSPACE_ID_FOUND>")
    duration_in_days = event.deep_get(
        "event",
        "details",
        "duration_in_days",
        default="<NO_DURATION_IN_DAYS_FOUND>",
    )
    return (
        f"Notion User [{user}] exported audit logs for the last "
        f"{duration_in_days} days for workspace id {workspace_id}"
    )


def alert_context(event):
    return notion_alert_context(event)

Rule specification

AnalysisType: rule
Filename: notion_workspace_audit_log_exported.py
RuleID: "Notion.Audit.Log.Exported"
DisplayName: "Notion Audit Log Exported"
Enabled: true
LogTypes:
  - Notion.AuditLogs
Tags:
  - Notion
  - Data Security
  - Data Exfiltration
Severity: Medium
Description: A Notion User exported audit logs for your organization’s workspace.
DedupPeriodMinutes: 60
Threshold: 1
Runbook: Possible Data Exfiltration. Follow up with the Notion User to determine if this was done for a valid business reason.
Reference: https://www.notion.so/help/audit-log#export-your-audit-log

Stages and Predicates

Fires on Notion.AuditLogs events when the condition below holds.

Condition

  • event.type is workspace.audit_log_exported

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
event.typeeq
  • workspace.audit_log_exported
field:"event.type" kind:eq value:"workspace.audit_log_exported"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
emailevent.actor.person.email
duration_in_daysevent.details.duration_in_days
workspace_idevent.workspace_id

Response runbook

Possible Data Exfiltration. Follow up with the Notion User to determine if this was done for a valid business reason.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "event": {
    "actor": {
      "id": "..",
      "object": "user",
      "person": {
        "email": "homer.simpson@yourcompany.io"
      },
      "type": "person"
    },
    "details": {
      "duration_in_days": 30
    },
    "id": "...",
    "ip_address": "...",
    "platform": "web",
    "timestamp": "2023-05-15T19:14:21.031Z",
    "type": "workspace.audit_log_exported",
    "workspace_id": ".."
  }
}

Notion Login From Blocked IP

#
Severity
medium
Log types
Notion.AuditLogs
Tags
Notion, Network Security Monitoring, Malicious Connections, Configuration Required
Reference
www.notion.so
Source
github.com/panther-labs/panther-analysis

A user attempted to access Notion from a blocked IP address. Note: before deployinh, make sure to add Rule Filters checking if event.ip_address is in a certain CIDR range(s).

Detection logic

from panther_notion_helpers import notion_alert_context


def rule(event):
    # Users can specify inline-filters to permit rules based on IPs

    return event.deep_get("event", "type", default="<NO_EVENT_TYPE_FOUND>") == "user.login"


def title(event):
    user = event.deep_get("event", "actor", "person", "email", default="<NO_USER_FOUND>")
    ip_addr = event.deep_get("event", "ip_address", default="<UNKNOWN IP>")
    return f"Notion User [{user}] attempted to login from a blocked IP: [{ip_addr}]."


def alert_context(event):
    return notion_alert_context(event)

Rule specification

AnalysisType: rule
Filename: notion_login_from_blocked_ip.py
RuleID: "Notion.LoginFromBlockedIP"
DisplayName: "Notion Login From Blocked IP"
Enabled: false
LogTypes:
  - Notion.AuditLogs
Tags:
  - Notion
  - Network Security Monitoring
  - Malicious Connections
  - Configuration Required
Severity: Medium
Description: "A user attempted to access Notion from a blocked IP address. Note: before deployinh, make sure to add Rule Filters checking if event.ip_address is in a certain CIDR range(s)."
DedupPeriodMinutes: 60
Threshold: 1
Runbook: Confirm with user if the login was legitimate. If so, determine why the IP is blocked.
Reference: https://www.notion.so/help/allowlist-ip

Stages and Predicates

Fires on Notion.AuditLogs events when the condition below holds.

Condition

  • event.type is user.login

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.

FieldSource
emailevent.actor.person.email
ip_addressevent.ip_address

Response runbook

Confirm with user if the login was legitimate. If so, determine why the IP is blocked.

Notion Login from New Location

#
Severity
medium
Log types
Notion.AuditLogs
Tags
Notion, Identity & Access Management, Login & Access Patterns
Reference
ipinfo.io
Source
github.com/panther-labs/panther-analysis

A Notion User logged in from a new location.

Detection logic

import datetime
import json
import time

from panther_detection_helpers.caching import get_dictionary, put_dictionary
from panther_ipinfo_helpers import IPInfoLocation
from panther_notion_helpers import notion_alert_context

# How long (in seconds) to keep previous login locations in cached memory
DEFAULT_CACHE_PERIOD = 2419200


def rule(event):

    # Only focused on login events
    if event.deep_walk("event", "type") != "user.login":
        return False

    # Get the user's location, via IPInfo
    # Return False if we have no location information
    if "ipinfo_location" not in event.get("p_enrichment", {}):
        return False
    # pylint: disable=global-variable-undefined
    global IPINFO_LOC
    IPINFO_LOC = IPInfoLocation(event)
    path_to_ip = "event.ip_address"
    city = IPINFO_LOC.city(path_to_ip) or ""
    region = IPINFO_LOC.region(path_to_ip) or ""
    country = IPINFO_LOC.country(path_to_ip) or ""
    loc_string = "_".join((city, region, country))

    # Store the login location. The premise is to create a new entry for each combimation of user
    # and location, and then have those records persist for some length of time (4 weeks by
    # default).
    # Store the login location. Here, we use Panther's cache to store a dictionary, using the
    #   user's unique ID to ensure it hold data unique to them. In this dictionary, we'll use the
    #   location strings (loc_string) as the key, and the values will be the timestamp of the last
    #   recorded login from that location.
    user = event.deep_walk("event", "actor", "id")
    cache = get_dictionary(user) or {}

    # If this is a unit test, convert cache from string
    if isinstance(cache, str):
        cache = json.loads(cache)

    # -- Step 1: Record this login.
    new_cache = cache.copy()
    new_cache[loc_string] = time.time()
    put_dictionary(user, new_cache)

    # -- Step 2: Determine if we shoul raise an alert.
    if not cache:
        # User hasn't been recorded logging in before. Since this is their first login, we don't
        #   have a baseline to know if it's unusual, so we won't raise an alert.
        return False

    if is_recent_login(cache, loc_string, event.get("p_parse_time")):
        # User has logged in from this location in the recent past. No need to raise an alert.
        return False

    # User has NOT logged in from this location in the recent past - we should trigger an alert!
    return True


def title(event):
    path_to_ip = "event.ip_address"
    city = IPINFO_LOC.city(path_to_ip)
    region = IPINFO_LOC.region(path_to_ip)
    country = IPINFO_LOC.country(path_to_ip)

    user_email = event.deep_walk("event", "actor", "person", "email", default="UNKNOWN_EMAIL")
    return f"Notion [{user_email}] logged in from a new location: {city}, {region}, {country}."


def alert_context(event):
    path_to_ip = "event.ip_address"
    city = IPINFO_LOC.city(path_to_ip)
    region = IPINFO_LOC.region(path_to_ip)
    country = IPINFO_LOC.country(path_to_ip)
    user_email = event.deep_walk("event", "actor", "person", "email", default="UNKNOWN_EMAIL")

    context = notion_alert_context(event)
    context["user_email"] = user_email
    context["location"] = {"city": city, "region": region, "country": country}

    return context


def is_recent_login(cache: dict, loc_string: str, parse_time: str) -> bool:
    # Use p_parse_time to calculate current timestamp, so that unit tests work.
    now = time.mktime(datetime.datetime.fromisoformat(parse_time[:23]).timetuple())
    return (
        loc_string in cache  # location was previously recorded
        and cache[loc_string] > now - DEFAULT_CACHE_PERIOD  # last recorded login is recent
    )

Rule specification

AnalysisType: rule
Filename: notion_login_from_new_location.py
RuleID: "Notion.LoginFromNewLocation"
DisplayName: "Notion Login from New Location"
Enabled: true
LogTypes:
  - Notion.AuditLogs
Tags:
  - Notion
  - Identity & Access Management
  - Login & Access Patterns
Severity: Medium
Description: A Notion User logged in from a new location.
DedupPeriodMinutes: 60
Threshold: 1 # Number of pages deleted; please change this value to suit your organization's needs.
Runbook: Possible account takeover. Follow up with the Notion User to determine if this login is genuine.
Reference: https://ipinfo.io/products/ip-geolocation-api

Stages and Predicates

Fires on Notion.AuditLogs events when all of the conditions below hold.

Condition

  • event.type is user.login
  • p_enrichment contains ipinfo_location

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.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
emailevent.actor.person.email

Response runbook

Possible account takeover. Follow up with the Notion User to determine if this login is genuine.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "event": {
    "actor": {
      "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "object": "user",
      "person": {
        "email": "aragorn.elessar@lotr.com"
      },
      "type": "person"
    },
    "details": {
      "authType": "email"
    },
    "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "ip_address": "192.168.100.100",
    "platform": "web",
    "timestamp": "2023-06-12 21:40:28.690000000",
    "type": "user.login",
    "workspace_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  },
  "p_enrichment": {
    "ipinfo_location": {
      "event.ip_address": {
        "city": "Barad-Dur",
        "country": "Mordor",
        "lat": "0.00000",
        "lng": "0.00000",
        "postal_code": "55555",
        "region": "Mount Doom",
        "region_code": "MD",
        "timezone": "Middle Earth/Mordor"
      }
    }
  },
  "p_event_time": "2023-06-12 21:40:28.690000000",
  "p_log_type": "Notion.AuditLogs",
  "p_parse_time": "2023-06-12 22:53:51.602223297",
  "p_row_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "p_schema_version": 0,
  "p_source_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "p_source_label": "Notion Logs"
}

Notion Login WITH AccountChange

#
Severity
medium
Time window
30h
Match by
p_alert_context.actor_id
Reference
www.notion.so
Source
github.com/panther-labs/panther-analysis

A Notion User logged in then changed their account details.

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Rule specification

AnalysisType: correlation_rule
RuleID: "Notion.Login.WITH.AccountChange"
DisplayName: "Notion Login WITH AccountChange"
Enabled: false
Severity: Medium
Description: A Notion User logged in then changed their account details.
Reference: https://www.notion.so/help/account-settings
Runbook: Possible account takeover. Follow up with the Notion User to determine if this email change is genuine.
Reports:
  MITRE ATT&CK:
    - TA0004:T1098  # Account Manipulation
Detection:
    - Group:
        - ID: Login
          RuleID: Notion.Login
        - ID: AccountChange
          RuleID: Notion.AccountChange
      MatchCriteria:
        field_name:
          - GroupID: Login
            Match: p_alert_context.actor_id
          - GroupID: AccountChange
            Match: p_alert_context.actor_id
      LookbackWindowMinutes: 1800
      Schedule:
        RateMinutes: 1440
        TimeoutMinutes: 5

Stages and Predicates

Fires when the steps below all occur within 30h, correlated by p_alert_context.actor_id. Each step needs one match unless a higher minimum is shown.

Stage 1: step Login

References detection Signal - Notion Login.

Stage 2: step AccountChange

References detection Signal - Notion Account Changed.

Response runbook

Possible account takeover. Follow up with the Notion User to determine if this email change is genuine.

Notion Many Pages Deleted

#
Severity
medium
Tags
Notion, Data Security, Data Destruction
Reference
www.notion.so
Source
github.com/panther-labs/panther-analysis

A Notion User deleted multiple pages, which were not created or restored from the trash within the same hour.

Detection logic

def rule(_):
    return True


def title(event):
    user = event.get("user", "<NO_USER_FOUND>")
    return f"Notion User [{user}] deleted multiple pages."

Rule specification

AnalysisType: scheduled_rule
Filename: notion_many_pages_deleted_sched.py
RuleID: "Notion.Many.Pages.Deleted.Sched"
DisplayName: "Notion Many Pages Deleted"
Enabled: true
ScheduledQueries:
  - Notion Many Pages Deleted Query
Tags:
  - Notion
  - Data Security
  - Data Destruction
Severity: Medium
Description: A Notion User deleted multiple pages, which were not created or restored from the trash within the same hour.
DedupPeriodMinutes: 60
Threshold: 10 # Number of pages deleted; please change this value to suit your organization's needs.
Runbook: Possible Data Destruction. Follow up with the Notion User to determine if this was done for a valid business reason.
Reference: https://www.notion.so/help/duplicate-delete-and-restore-content

Stages and Predicates

Rule logic

This rule alerts on rows returned by its scheduled query Notion Many Pages Deleted Query; its Python module (Detection logic above) shapes the alert rather than filtering.

Alert cadence
alerts after 10 matches within 1h

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
user

Response runbook

Possible Data Destruction. Follow up with the Notion User to determine if this was done for a valid business reason.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "actions": [
    "page.deleted"
  ],
  "id": "1360a5bb-da41-8177-bedb-d015d012392a",
  "page_name": "Newslette",
  "user": "bob.ross@happytrees.com"
}

Notion Many Pages Deleted [Deprecated]

#
Status
Deprecated
Severity
medium
Log types
Notion.AuditLogs
Tags
Notion, Data Security, Data Destruction, Deprecated
Reference
www.notion.so
Source
github.com/panther-labs/panther-analysis

(Deprecated due to false-positive rate) A Notion User deleted multiple pages.

Detection logic

from panther_notion_helpers import notion_alert_context


def rule(event):

    return event.deep_get("event", "type", default="<NO_EVENT_TYPE_FOUND>") == "page.deleted"


def title(event):
    user = event.deep_get("event", "actor", "person", "email", default="<NO_USER_FOUND>")
    return f"Notion User [{user}] deleted multiple pages."


def alert_context(event):
    context = notion_alert_context(event)
    page_id = event.deep_get("event", "details", "target", "page_id", default="<NO_PAGE_ID_FOUND>")
    context["page_id"] = page_id
    return context

Rule specification

AnalysisType: rule
Filename: notion_many_pages_deleted.py
RuleID: "Notion.Many.Pages.Deleted"
DisplayName: "Notion Many Pages Deleted [Deprecated]"
Status: Deprecated
Enabled: false
LogTypes:
  - Notion.AuditLogs
Tags:
  - Notion
  - Data Security
  - Data Destruction
  - Deprecated
Severity: Medium
Description: (Deprecated due to false-positive rate) A Notion User deleted multiple pages.
DedupPeriodMinutes: 60
Threshold: 10 # Number of pages deleted; please change this value to suit your organization's needs.
Runbook: Possible Data Destruction. Follow up with the Notion User to determine if this was done for a valid business reason.
Reference: https://www.notion.so/help/duplicate-delete-and-restore-content

Stages and Predicates

Fires on Notion.AuditLogs events when the condition below holds.

Condition

  • event.type is page.deleted
Alert cadence
alerts after 10 matches within 1h

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.

FieldSource
emailevent.actor.person.email

Response runbook

Possible Data Destruction. Follow up with the Notion User to determine if this was done for a valid business reason.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "event": {
    "actor": {
      "id": "af06b6ff-dd5e-4024-b9ef-78fe77f55884",
      "object": "user",
      "person": {
        "email": "homer.simpson@yourcompany.io"
      },
      "type": "person"
    },
    "details": {
      "parent": {
        "database_id": "543af759-3010-4355-a71e-4sdfs3566a",
        "type": "database_id"
      },
      "target": {
        "page_id": "93cf05d3-6805-4ddc-abba-adsfjhnlkwje785",
        "type": "page_id"
      }
    },
    "id": "768873bf-6b2c-40e8-b27c-1c199c4d6ae7",
    "ip_address": "12.12.12.12",
    "platform": "web",
    "timestamp": "2023-05-24 20:17:41.905000000",
    "type": "page.deleted",
    "workspace_id": "ea65b016-6abc-4dcf-808b-sdfg445654"
  }
}

Notion Many Pages Deleted Query

#
Tags
Notion, Data Security, Data Destruction
Source
github.com/panther-labs/panther-analysis

A Notion User deleted multiple pages, which were not created or restored from the trash within the same hour.

Rule specification

AnalysisType: scheduled_query
QueryName: Notion Many Pages Deleted Query
Enabled: false
Tags:
  - Notion
  - Data Security
  - Data Destruction
Description: >
  A Notion User deleted multiple pages, which were not created or restored from the trash within the same hour.
SnowflakeQuery: |
  SELECT
    event:actor.person.email AS user
    ,ARRAY_AGG(event:type) AS actions
    ,event:details.page_name AS page_name
    ,event:details.target.page_id AS id
  FROM
    panther_logs.public.notion_auditlogs
  WHERE
    p_occurs_since(1 hour)
    AND event:type IN ('page.deleted','page.created','page.restored_from_trash')
    AND event:details.target.type = 'page_id'
    AND page_name != ''
    AND event:actor.type = 'person'
  GROUP BY id, user, page_name
  HAVING
    actions = ARRAY_CONSTRUCT('page.deleted')

DatabricksQuery: |
  SELECT
    event.actor.person.email AS user
    ,COLLECT_LIST(event:type) AS actions
    ,event.details.page_name AS page_name
    ,event.details.target.page_id AS id
  FROM
    panther_logs.notion_auditlogs
  WHERE
    p_occurs_since(1 hour)
    AND event:type IN ('page.deleted','page.created','page.restored_from_trash')
    AND event.details.target.type = 'page_id'
    AND event.details.page_name != ''
    AND event.actor.type = 'person'
  GROUP BY id, user, page_name
  HAVING
    actions = ARRAY('page.deleted')
Schedule:
  RateMinutes: 60
  TimeoutMinutes: 2

Stages and Predicates

Stage 1: source

Table
panther_logs.public.notion_auditlogs

Stage 2: filter

  • event:type is one of page.deleted, page.created, page.restored_from_trash
Window
1h

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.

FieldKindValuesSearch
event:typein
  • page.created
  • page.deleted
  • page.restored_from_trash
field:"event:type" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
userevent:actor . person.email
actionsARRAY_AGG ( event:type )
page_nameevent:details . page_name
idevent:details . target.page_id

Notion Many Pages Exported

#
Severity
high
Log types
Notion.AuditLogs
Tags
Notion, Data Security, Data Exfiltration
Reference
www.notion.so
Source
github.com/panther-labs/panther-analysis

A Notion User exported multiple pages.

Detection logic

from panther_notion_helpers import notion_alert_context


def rule(event):

    return event.deep_get("event", "type", default="<NO_EVENT_TYPE_FOUND>") == "page.exported"


def title(event):
    user = event.deep_get("event", "actor", "person", "email", default="<NO_USER_FOUND>")
    return f"Notion User [{user}] exported multiple pages."


def alert_context(event):
    context = notion_alert_context(event)
    page_id = event.deep_get("event", "details", "target", "page_id", default="<NO_PAGE_ID_FOUND>")
    context["page_id"] = page_id
    return context

Rule specification

AnalysisType: rule
Filename: notion_many_pages_exported.py
RuleID: "Notion.Many.Pages.Exported"
DisplayName: "Notion Many Pages Exported"
Enabled: true
LogTypes:
  - Notion.AuditLogs
Tags:
  - Notion
  - Data Security
  - Data Exfiltration
Severity: High
Description: A Notion User exported multiple pages.
DedupPeriodMinutes: 60
Threshold: 10 # Number of pages exported; please change this value to suit your organization's needs.
Runbook: Possible Data Exfiltration. Follow up with the Notion User to determine if this was done for a valid business reason.
Reference: https://www.notion.so/help/export-your-content

Stages and Predicates

Fires on Notion.AuditLogs events when the condition below holds.

Condition

  • event.type is page.exported
Alert cadence
alerts after 10 matches within 1h

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.

FieldSource
emailevent.actor.person.email

Response runbook

Possible Data Exfiltration. Follow up with the Notion User to determine if this was done for a valid business reason.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "event": {
    "actor": {
      "id": "bd37477c-869d-418b-abdb-0fc727b38b5e",
      "object": "user",
      "person": {
        "email": "homer.simpson@yourcompany.io"
      },
      "type": "person"
    },
    "details": {
      "parent": {
        "type": "workspace_id",
        "workspace_id": "ab99as87-6abc-4dcf-808b-111999882299"
      },
      "target": {
        "page_id": "3cd2c560-d1b9-474e-b46e-gh8899002763",
        "type": "page_id"
      }
    },
    "id": "d4b9963f-12a8-4b01-b597-233a140abf5e",
    "ip_address": "12.12.12.12",
    "platform": "web",
    "timestamp": "2023-06-01 18:57:07.486000000",
    "type": "page.exported",
    "workspace_id": "ea65b016-6abc-4dcf-808b-e119617b55d1"
  }
}

Notion Page API Permissions Changed

#
Severity
low
Log types
Notion.AuditLogs
Tags
Notion, Data Security, Unapproved 3rd Party Apps
Reference
www.notion.so
Source
github.com/panther-labs/panther-analysis

A new API integration was added to a Notion page, or it's permissions were changed.

Detection logic

from panther_notion_helpers import notion_alert_context

# These event types correspond to users adding or editing the default role on a public page
event_types = (
    "page.permissions.integration_role_added",
    "page.permissions.integration_role_updated",
)


def rule(event):

    return event.deep_get("event", "type", default="<NO_EVENT_TYPE_FOUND>") in event_types


def title(event):
    user = event.deep_get("event", "actor", "person", "email", default="<NO_USER_FOUND>")
    page_id = event.deep_get("event", "details", "target", "page_id", default="<NO_PAGE_ID_FOUND>")
    return f"Notion User [{user}] added an integration to page [{page_id}]."


def alert_context(event):
    context = notion_alert_context(event)
    page_id = event.deep_get("event", "details", "target", "page_id", default="<NO_PAGE_ID_FOUND>")
    context["page_id"] = page_id
    return context

Rule specification

AnalysisType: rule
Filename: notion_page_accessible_to_api.py
DisplayName: "Notion Page API Permissions Changed"
RuleID: "Notion.PagePerms.APIPermsChanged"
Enabled: true
LogTypes:
  - Notion.AuditLogs
Tags:
  - Notion
  - Data Security
  - Unapproved 3rd Party Apps
Severity: Low
Description: "A new API integration was added to a Notion page, or it's permissions were changed."
DedupPeriodMinutes: 60
Threshold: 1
Runbook: Potential information exposure - review the shared page and rectify if needed.
Reference: https://www.notion.so/help/sharing-and-permissions

Stages and Predicates

Fires on Notion.AuditLogs events when the condition below holds.

Condition

  • event.type is one of page.permissions.integration_role_added, page.permissions.integration_role_updated

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
event.typein
  • page.permissions.integration_role_added
  • page.permissions.integration_role_updated
field:"event.type" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
emailevent.actor.person.email
page_idevent.details.target.page_id

Response runbook

Potential information exposure - review the shared page and rectify if needed.

Notion Page Guest Permissions Changed

#
Severity
low
Log types
Notion.AuditLogs
Tags
Notion, Data Security, Information Disclosure
Reference
www.notion.so
Source
github.com/panther-labs/panther-analysis

The external guest permissions for a Notion page have been altered.

Detection logic

from panther_base_helpers import deep_get
from panther_notion_helpers import notion_alert_context

# These event types correspond to users adding or editing the default role on a public page
event_types = ("page.permissions.guest_role_added", "page.permissions.guest_role_updated")


def rule(event):

    return event.deep_get("event", "type", default="<NO_EVENT_TYPE_FOUND>") in event_types


def title(event):
    user = event.deep_get("event", "actor", "person", "email", default="<NO_USER_FOUND>")
    guest = event.deep_get(
        "event", "details", "entity", "person", "email", default="<NO_USER_FOUND>"
    )
    page_id = event.deep_get("event", "details", "target", "page_id", default="<NO_PAGE_ID_FOUND>")
    event_type = event.deep_get("event", "type", default="<NO_EVENT_TYPE_FOUND>")
    action = {
        "page.permissions.guest_role_added": "added a guest",
        "page.permissions.guest_role_updated": "changed the guest permissions of",
    }.get(event_type, "changed the guest permissions of")
    return f"Notion User [{user}] {action} [{guest}] on page [{page_id}]."


def alert_context(event):
    context = notion_alert_context(event)
    page_id = event.deep_get("event", "details", "target", "page_id", default="<NO_PAGE_ID_FOUND>")
    context["page_id"] = page_id
    details = event.deep_get("event", "details", default={})
    context["guest"] = deep_get(details, "entity", "person", "email", default="<NO_USER_FOUND>")
    context["new_permission"] = deep_get(details, "new_permission", default="<UNKNOWN PERMISSION>")
    context["old_permission"] = deep_get(details, "old_permission", default="<UNKNOWN PERMISSION>")
    return context

Rule specification

AnalysisType: rule
Filename: notion_page_accessible_to_guests.py
RuleID: "Notion.PagePerms.GuestPermsChanged"
DisplayName: "Notion Page Guest Permissions Changed"
Enabled: true
LogTypes:
  - Notion.AuditLogs
Tags:
  - Notion
  - Data Security
  - Information Disclosure
Severity: Low
Description: The external guest permissions for a Notion page have been altered.
DedupPeriodMinutes: 60
Threshold: 1
Runbook: Potential information exposure - review the shared page and rectify if needed.
Reference: https://www.notion.so/help/sharing-and-permissions

Stages and Predicates

Fires on Notion.AuditLogs events when the condition below holds.

Condition

  • event.type is one of page.permissions.guest_role_added, page.permissions.guest_role_updated

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
event.typein
  • page.permissions.guest_role_added
  • page.permissions.guest_role_updated
field:"event.type" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
emailevent.actor.person.email
emailevent.details.entity.person.email
page_idevent.details.target.page_id

Response runbook

Potential information exposure - review the shared page and rectify if needed.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "event": {
    "actor": {
      "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "object": "user",
      "person": {
        "email": "aragorn.elessar@lotr.com"
      },
      "type": "person"
    },
    "details": {
      "entity": {
        "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "object": "user",
        "person": {
          "email": "frodo.baggins@lotr.com"
        },
        "type": "person"
      },
      "new_permission": "full_access",
      "old_permission": "none",
      "page_audience": "shared_internally",
      "target": {
        "page_id": "441356b5-557b-4053-8d2f-7932d2607d66",
        "type": "page_id"
      }
    },
    "id": "e18690f8-e24b-4b03-ba6f-123eb7ec0f08",
    "timestamp": "2023-08-11 23:02:53.113000000",
    "type": "page.permissions.guest_role_added",
    "workspace_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  }
}

Notion Page Published to Web

#
Severity
low
Log types
Notion.AuditLogs
Tags
Notion, Data Security, Information Disclosure
Reference
www.notion.so
Source
github.com/panther-labs/panther-analysis

A Notion User published a page to the web.

Detection logic

from panther_notion_helpers import notion_alert_context

# These event types correspond to users adding or editing the default role on a public page
event_types = (
    "page.permissions.shared_to_public_role_added",
    "page.permissions.shared_to_public_role_updated",
)


def rule(event):

    return event.deep_get("event", "type", default="<NO_EVENT_TYPE_FOUND>") in event_types


def title(event):
    user = event.deep_get("event", "actor", "person", "email", default="<NO_USER_FOUND>")
    page_name = event.deep_get("event", "details", "page_name", default="<NO_PAGE_NAME_FOUND>")
    return f"Notion User [{user}] changed the status of page [{page_name}] to public."


def alert_context(event):
    context = notion_alert_context(event)
    page_name = event.deep_get("event", "details", "page_name", default="<NO_PAGE_NAME_FOUND>")
    context["page_name"] = page_name
    return context

Rule specification

AnalysisType: rule
Filename: notion_page_shared_to_web.py
RuleID: "Notion.PageSharedToWeb"
DisplayName: "Notion Page Published to Web"
Enabled: true
LogTypes:
  - Notion.AuditLogs
Tags:
  - Notion
  - Data Security
  - Information Disclosure
Severity: Low
Description: A Notion User published a page to the web.
DedupPeriodMinutes: 60
Threshold: 1
Runbook: Potential information exposure - review the shared page and rectify if needed.
Reference: https://www.notion.so/help/public-pages-and-web-publishing

Stages and Predicates

Fires on Notion.AuditLogs events when the condition below holds.

Condition

  • event.type is one of page.permissions.shared_to_public_role_added, page.permissions.shared_to_public_role_updated

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
event.typein
  • page.permissions.shared_to_public_role_added
  • page.permissions.shared_to_public_role_updated
field:"event.type" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
emailevent.actor.person.email
page_nameevent.details.page_name

Response runbook

Potential information exposure - review the shared page and rectify if needed.

Notion SAML SSO Configuration Changed

#
Severity
high
Log types
Notion.AuditLogs
Tags
Notion, Identity & Access Management, Credential Security
Reference
www.notion.so
Source
github.com/panther-labs/panther-analysis

A Notion User changed settings to enforce SAML SSO configurations for your organization.

Detection logic

from panther_notion_helpers import notion_alert_context


def rule(event):

    return (
        event.deep_get("event", "type", default="<NO_EVENT_TYPE_FOUND>")
        == "workspace.settings.enforce_saml_sso_config_updated"
    )


def title(event):
    user = event.deep_get("event", "actor", "person", "email", default="<NO_USER_FOUND>")
    workspace_id = event.deep_get("event", "workspace_id", default="<NO_WORKSPACE_ID_FOUND>")
    state = event.deep_get(
        "event",
        "workspace.settings.enforce_saml_sso_config_updated",
        "state",
        default="<NO_STATE_FOUND>",
    )

    if state == "enabled":
        return (
            f"Notion User [{user}] updated settings to enable SAML SSO config "
            f"from workspace id {workspace_id}"
        )

    return (
        f"Notion User [{user}] updated settings to disable SAML SSO config "
        f"from workspace id {workspace_id}"
    )


def severity(event):
    state = event.deep_get(
        "event",
        "workspace.settings.enforce_saml_sso_config_updated",
        "state",
        default="<NO_STATE_FOUND>",
    )

    if state == "enabled":
        return "INFO"

    return "HIGH"


def alert_context(event):
    return notion_alert_context(event)

Rule specification

AnalysisType: rule
Filename: notion_workspace_settings_enforce_saml_sso_config_updated.py
RuleID: "Notion.SAML.SSO.Configuration.Changed"
DisplayName: "Notion SAML SSO Configuration Changed"
Enabled: true
LogTypes:
  - Notion.AuditLogs
Tags:
  - Notion
  - Identity & Access Management
  - Credential Security
Severity: High
Description: A Notion User changed settings to enforce SAML SSO configurations for your organization.
DedupPeriodMinutes: 60
Threshold: 1
Runbook: Follow up with the Notion User to determine if this was done for a valid business reason and to ensure these settings get re-enabled quickly for best security practices.
Reference: https://www.notion.so/help/saml-sso-configuration

Stages and Predicates

Fires on Notion.AuditLogs events when the condition below holds.

Condition

  • event.type is workspace.settings.enforce_saml_sso_config_updated

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
event.typeeq
  • workspace.settings.enforce_saml_sso_config_updated
field:"event.type" kind:eq value:"workspace.settings.enforce_saml_sso_config_updated"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
emailevent.actor.person.email
workspace_idevent.workspace_id

Response runbook

Follow up with the Notion User to determine if this was done for a valid business reason and to ensure these settings get re-enabled quickly for best security practices.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "event": {
    "actor": {
      "id": "..",
      "object": "user",
      "person": {
        "email": "homer.simpson@yourcompany.io"
      },
      "type": "person"
    },
    "id": "...",
    "ip_address": "...",
    "platform": "web",
    "timestamp": "2023-05-15T19:14:21.031Z",
    "type": "workspace.settings.enforce_saml_sso_config_updated",
    "workspace.settings.enforce_saml_sso_config_updated": {
      "state": "enabled"
    },
    "workspace_id": ".."
  }
}

Notion SCIM Token Generated

#
Severity
medium
Log types
Notion.AuditLogs
Tags
Notion, Application Security, Supply Chain Attack
Reference
www.notion.so
Source
github.com/panther-labs/panther-analysis

A Notion User generated a SCIM token.

Detection logic

from panther_notion_helpers import notion_alert_context


def rule(event):

    event_type = event.deep_get("event", "type", default="<NO_EVENT_TYPE_FOUND>")
    return event_type == "workspace.scim_token_generated"


def title(event):
    user = event.deep_get("event", "actor", "person", "email", default="<NO_USER_FOUND>")
    workspace_id = event.deep_get("event", "workspace_id", default="<NO_WORKSPACE_ID_FOUND>")
    return f"Notion User [{user}] generated a SCIM token for workspace id [{workspace_id}]."


def alert_context(event):
    return notion_alert_context(event)

Rule specification

AnalysisType: rule
Filename: notion_scim_token_generated.py
RuleID: "Notion.Workspace.SCIM.Token.Generated"
DisplayName: "Notion SCIM Token Generated"
Enabled: true
LogTypes:
  - Notion.AuditLogs
Tags:
  - Notion
  - Application Security
  - Supply Chain Attack
Description: A Notion User generated a SCIM token.
Severity: Medium
DedupPeriodMinutes: 60
Threshold: 1
Runbook: Possible Initial Access. Follow up with the Notion User to determine if this was done for a valid business reason.
Reference: https://www.notion.so/help/provision-users-and-groups-with-scim

Stages and Predicates

Fires on Notion.AuditLogs events when the condition below holds.

Condition

  • event.type is workspace.scim_token_generated

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
event.typeeq
  • workspace.scim_token_generated
field:"event.type" kind:eq value:"workspace.scim_token_generated"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
emailevent.actor.person.email
workspace_idevent.workspace_id

Response runbook

Possible Initial Access. Follow up with the Notion User to determine if this was done for a valid business reason.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "event": {
    "actor": {
      "id": "..",
      "object": "user",
      "person": {
        "email": "homer.simpson@yourcompany.com"
      },
      "type": "person"
    },
    "id": "...",
    "ip_address": "...",
    "platform": "mac-desktop",
    "timestamp": "2023-06-02T20:21:01.873Z",
    "type": "workspace.scim_token_generated",
    "workspace.scim_token_generated": {},
    "workspace_id": "123"
  }
}

Notion Sharing Settings Updated

#
Severity
medium
Log types
Notion.AuditLogs
Tags
Notion, Data Exfiltration
Source
github.com/panther-labs/panther-analysis

A Notion User enabled sharing for a Workspace or Teamspace.

Detection logic

from panther_notion_helpers import notion_alert_context

EVENTS = (
    "teamspace.settings.allow_public_page_sharing_setting_updated",
    "teamspace.settings.allow_guests_setting_updated",
    "teamspace.settings.allow_content_export_setting_updated",
    "workspace.settings.allow_public_page_sharing_setting_updated",
    "workspace.settings.allow_guests_setting_updated",
    "workspace.settings.allow_content_export_setting_updated",
)


def rule(event):
    return all(
        [
            event.deep_get("event", "type", default="") in EVENTS,
            event.deep_get("event", "details", "state", default="") == "enabled",
        ]
    )


def title(event):
    actor = event.deep_get("event", "actor", "person", "email", default="NO_ACTOR_FOUND")
    action = event.deep_get("event", "type", default="NO.EVENT.FOUND").split(".")[2]
    teamspace = event.deep_get("event", "details", "target", "name", default=None)
    if teamspace:
        return f"[{actor}] enabled [{action}] for [{teamspace}] Teamspace"
    return f"[{actor}] enabled [{action}] for Workspace"


def alert_context(event):
    return notion_alert_context(event)

Rule specification

AnalysisType: rule
Filename: notion_sharing_settings_updated.py
RuleID: "Notion.SharingSettingsUpdated"
DisplayName: "Notion Sharing Settings Updated"
Enabled: true
LogTypes:
  - Notion.AuditLogs
Tags:
  - Notion
  - Data Exfiltration
Description: A Notion User enabled sharing for a Workspace or Teamspace.
Severity: Medium
DedupPeriodMinutes: 60
Threshold: 1
Runbook: Possible Data Exfiltration. Follow up with the Notion User to determine if this was done for a valid business reason.

Stages and Predicates

Fires on Notion.AuditLogs events when all of the conditions below hold.

Condition

  • event.type is one of teamspace.settings.allow_public_page_sharing_setting_updated, teamspace.settings.allow_guests_setting_updated, teamspace.settings.allow_content_export_setting_updated, workspace.settings.allow_public_page_sharing_setting_updated, workspace.settings.allow_guests_setting_updated (+1 more values, see Indicators below)
  • event.details.state is enabled

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
event.details.stateeq
  • enabled
field:"event.details.state" kind:eq value:"enabled"
event.typein
  • teamspace.settings.allow_content_export_setting_updated
  • teamspace.settings.allow_guests_setting_updated
  • teamspace.settings.allow_public_page_sharing_setting_updated
  • workspace.settings.allow_content_export_setting_updated
  • workspace.settings.allow_guests_setting_updated
  • workspace.settings.allow_public_page_sharing_setting_updated
field:"event.type" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
emailevent.actor.person.email
nameevent.details.target.name

Response runbook

Possible Data Exfiltration. Follow up with the Notion User to determine if this was done for a valid business reason.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "event": {
    "actor": {
      "id": "c16137bb-5078-4eac-b026-5cbd2f9a027a",
      "object": "user",
      "person": {
        "email": "aaron@example.com"
      },
      "type": "person"
    },
    "details": {
      "state": "enabled"
    },
    "id": "91b29a4b-4978-40e1-ab56-40221f801ce5",
    "ip_address": "11.22.33.44",
    "platform": "web",
    "timestamp": "2023-12-13 16:39:06.860000000",
    "type": "workspace.settings.allow_guests_setting_updated",
    "workspace_id": "ea65b016-6abc-4dcf-808b-e119617b55d1"
  }
}

Notion Teamspace Owner Added

#
Severity
medium
Log types
Notion.AuditLogs
Tags
Notion, Privilege Escalation
Source
github.com/panther-labs/panther-analysis

A Notion User was added as a Teamspace owner.

MITRE ATT&CK coverage

TacticTechniques
Privilege EscalationNo specific technique

Detection logic

from panther_notion_helpers import notion_alert_context


def rule(event):
    added = (
        event.deep_get("event", "type", default="") == "teamspace.permissions.member_added"
        and event.deep_get("event", "details", "role", default="") == "owner"
    )
    updated = (
        event.deep_get("event", "type", default="") == "teamspace.permissions.member_role_updated"
        and event.deep_get("event", "details", "new_role", default="") == "owner"
    )
    return added or updated


def title(event):
    actor = event.deep_get("event", "actor", "person", "email", default="NO_ACTOR_FOUND")
    member = event.deep_get(
        "event", "details", "member", "person", "email", default="NO_MEMBER_FOUND"
    )
    teamspace = event.deep_get("event", "details", "target", "name", default="NO_TEAMSPACE_FOUND")
    return f"[{actor}] added [{member}] as owner of [{teamspace}] Teamspace"


def alert_context(event):
    return notion_alert_context(event)

Rule specification

AnalysisType: rule
Filename: notion_teamspace_owner_added.py
RuleID: "Notion.TeamspaceOwnerAdded"
DisplayName: "Notion Teamspace Owner Added"
Enabled: true
LogTypes:
  - Notion.AuditLogs
Tags:
  - Notion
  - Privilege Escalation
Description: A Notion User was added as a Teamspace owner.
Severity: Medium
DedupPeriodMinutes: 60
Threshold: 1
Runbook: Possible Privilege Escalation. Follow up with the Notion User to determine if this was done for a valid business reason.

Stages and Predicates

Fires on Notion.AuditLogs events when any of the conditions below holds.

Condition

  • any of:
    • all of:
      • event.type is teamspace.permissions.member_added
      • event.details.role is owner
    • all of:
      • event.type is teamspace.permissions.member_role_updated
      • event.details.new_role is owner

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.

FieldSource
emailevent.actor.person.email
emailevent.details.member.person.email
nameevent.details.target.name

Response runbook

Possible Privilege Escalation. Follow up with the Notion User to determine if this was done for a valid business reason.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "event": {
    "actor": {
      "id": "c16137bb-5078-4eac-b026-5cbd2f9a027a",
      "object": "user",
      "person": {
        "email": "malicious.insider@example.com"
      },
      "type": "person"
    },
    "details": {
      "member": {
        "id": "c16137bb-5078-4eac-b026-5cbd2f9a027a",
        "object": "user",
        "person": {
          "email": "bad.dude@example.com"
        },
        "type": "person"
      },
      "new_role": "owner",
      "target": {
        "id": "b8db234d-71eb-49e2-a5ed-7935ca764920",
        "name": "General",
        "object": "teamspace"
      }
    },
    "id": "6019b995-0158-4430-8263-89ad7905bd1d",
    "ip_address": "11.22.33.44",
    "platform": "web",
    "timestamp": "2023-12-13 16:38:04.264000000",
    "type": "teamspace.permissions.member_role_updated",
    "workspace_id": "ea65b016-6abc-4dcf-808b-e119617b55d1"
  }
}

Notion Workspace Exported

#
Severity
high
Log types
Notion.AuditLogs
Tags
Notion, Data Security, Data Exfiltration
Reference
www.notion.so
Source
github.com/panther-labs/panther-analysis

A Notion User exported an existing workspace.

Detection logic

from panther_notion_helpers import notion_alert_context


def rule(event):

    event_type = event.deep_get("event", "type", default="<NO_EVENT_TYPE_FOUND>")
    return event_type == "workspace.content_exported"


def title(event):
    user = event.deep_get("event", "actor", "person", "email", default="<NO_USER_FOUND>")
    workspace_id = event.deep_get("event", "workspace_id", default="<NO_WORKSPACE_ID_FOUND>")
    return f"Notion User [{user}] exported a workspace with workspace id [{workspace_id}]."


def alert_context(event):
    return notion_alert_context(event)

Rule specification

AnalysisType: rule
Filename: notion_workspace_exported.py
RuleID: "Notion.Workspace.Exported"
DisplayName: "Notion Workspace Exported"
Enabled: true
LogTypes:
  - Notion.AuditLogs
Tags:
  - Notion
  - Data Security
  - Data Exfiltration
Severity: High
Description: A Notion User exported an existing workspace.
DedupPeriodMinutes: 60
Threshold: 1
Runbook: Possible Data Exfiltration. Follow up with the Notion User to determine if this was done for a valid business reason.
Reference: https://www.notion.so/help/workspace-settings#export-an-entire-workspace

Stages and Predicates

Fires on Notion.AuditLogs events when the condition below holds.

Condition

  • event.type is workspace.content_exported

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
event.typeeq
  • workspace.content_exported
field:"event.type" kind:eq value:"workspace.content_exported"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
emailevent.actor.person.email
workspace_idevent.workspace_id

Response runbook

Possible Data Exfiltration. Follow up with the Notion User to determine if this was done for a valid business reason.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "event": {
    "actor": {
      "id": "..",
      "object": "user",
      "person": {
        "email": "homer.simpson@yourcompany.io"
      },
      "type": "person"
    },
    "id": "...",
    "ip_address": "...",
    "platform": "mac-desktop",
    "timestamp": "2023-06-02T20:16:41.217Z",
    "type": "workspace.content_exported",
    "workspace.content_exported": {},
    "workspace_id": ".."
  }
}

Notion Workspace public page added

#
Severity
informational
Log types
Notion.AuditLogs
Tags
Notion, Data Security, Information Disclosure
Reference
www.notion.so
Source
github.com/panther-labs/panther-analysis

A Notion page was set to public in your worksace.

Detection logic

from panther_notion_helpers import notion_alert_context


def rule(event):

    event_type = event.deep_get("event", "type", default="<NO_EVENT_TYPE_FOUND>")
    return event_type == "workspace.settings.public_homepage_added"


def title(event):
    actor = event.deep_get("event", "actor", "person", "email", default="<NO_EMAIL_FOUND>")
    wkspc_id = event.deep_get("event", "workspace_id", default="<NO_WORKSPACE_ID_FOUND>")
    db_id = event.deep_get(
        "event",
        "workspace.settings.public_homepage_added",
        "new_public_page",
        "database_id",
        default="<NO_DATABASE_ID_FOUND>",
    )
    return f"Notion User [{actor}] added a new public homepage [{db_id}] in workspace [{wkspc_id}]"


def alert_context(event):
    context = notion_alert_context(event)
    workspace_id = event.deep_get("event", "workspace_id", default="<NO_WORKSPACE_ID_FOUND>")
    db_id = event.deep_get(
        "event",
        "workspace.settings.public_homepage_added",
        "new_public_page",
        "database_id",
        default="<NO_DATABASE_ID_FOUND>",
    )
    context["workspace_id"] = workspace_id
    context["page_id"] = db_id
    return context

Rule specification

AnalysisType: rule
Filename: notion_workspace_settings_public_homepage_added.py
RuleID: "Notion.Workspace.Public.Page.Added"
DisplayName: "Notion Workspace public page added"
Enabled: true
LogTypes:
  - Notion.AuditLogs
Tags:
  - Notion
  - Data Security
  - Information Disclosure
Severity: Info
Description: A Notion page was set to public in your worksace.
DedupPeriodMinutes: 60
Threshold: 1
Runbook: A Notion page was made public. Check with the author to determine why this page was made public.
Reference: https://www.notion.so/help/public-pages-and-web-publishing

Stages and Predicates

Fires on Notion.AuditLogs events when the condition below holds.

Condition

  • event.type is workspace.settings.public_homepage_added

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
event.typeeq
  • workspace.settings.public_homepage_added
field:"event.type" kind:eq value:"workspace.settings.public_homepage_added"

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
emailevent.actor.person.email
database_idevent.workspace.settings.public_homepage_added.new_public_page.database_id
workspace_idevent.workspace_id

Response runbook

A Notion page was made public. Check with the author to determine why this page was made public.

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "event": {
    "actor": {
      "id": "44444444-cccc-7777-aaaa-666666666666",
      "object": "user",
      "person": {
        "email": "example@personemail.com"
      },
      "type": "person"
    },
    "id": "eeeeeeee-dddd-4444-bbbb-4444444444444",
    "ip_address": "00.000.00.000",
    "platform": "web",
    "timestamp": "2023-05-15T19:14:21.031Z",
    "type": "workspace.settings.public_homepage_added",
    "workspace.settings.public_homepage_added": {
      "new_public_page": {
        "database_id": "4b801dc7-d724-4fbb-afd0-9885cbc12405",
        "type": "database_id"
      }
    },
    "workspace_id": "vvvvvvvv-dddd-4444-bbbb-666666666666"
  }
}

Signal - Notion Account Changed

#
Severity
informational
Log types
Notion.AuditLogs
Tags
Notion, Identity & Access Management, Persistence
Reference
www.notion.so
Source
github.com/panther-labs/panther-analysis

A Notion User changed their account information.

MITRE ATT&CK coverage

TacticTechniques
PersistenceNo specific technique

Detection logic

from panther_notion_helpers import notion_alert_context


def rule(event):

    allowed_event_types = {
        "user.settings.login_method.email_updated",
        "user.settings.login_method.password_updated",
        "user.settings.login_method.password_added",
        "user.settings.login_method.password_removed",
    }
    if event.deep_walk("event", "type") in allowed_event_types:
        return True
    return False


def title(event):
    user_email = event.deep_walk("event", "actor", "person", "email", default="UNKNOWN EMAIL")
    action_taken = {
        "user.settings.login_method.email_updated": "changed their email",
        "user.settings.login_method.password_updated": "changed their password",
        "user.settings.login_method.password_added": "added a password to their account",
        "user.settings.login_method.password_removed": "removed the password from their account",
    }.get(event.deep_get("event", "type"), "altered their account info")
    return f"Notion User [{user_email}] {action_taken}."


def alert_context(event):
    context = notion_alert_context(event)
    context["login_timestamp"] = event.get("p_event_time")
    context["actor_id"] = event.deep_walk("event", "actor", "id")
    return context

Rule specification

AnalysisType: rule
Filename: notion_account_changed.py
RuleID: "Notion.AccountChange"
DisplayName: "Signal - Notion Account Changed"
Enabled: true
CreateAlert: false
LogTypes:
  - Notion.AuditLogs
Tags:
  - Notion
  - Identity & Access Management
  - Persistence
Severity: Info
Description: A Notion User changed their account information.
DedupPeriodMinutes: 60
Threshold: 1
Reference: https://www.notion.so/help/account-settings

Stages and Predicates

Fires on Notion.AuditLogs events when the condition below holds.

Condition

  • event.type is one of user.settings.login_method.email_updated, user.settings.login_method.password_updated, user.settings.login_method.password_added, user.settings.login_method.password_removed

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
event.typein
  • user.settings.login_method.email_updated
  • user.settings.login_method.password_added
  • user.settings.login_method.password_removed
  • user.settings.login_method.password_updated
field:"event.type" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
emailevent.actor.person.email

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "event": {
    "actor": {
      "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "object": "user",
      "person": {
        "email": "aragorn.elessar@lotr.com"
      },
      "type": "person"
    },
    "details": {
      "authType": "email"
    },
    "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "ip_address": "192.168.100.100",
    "platform": "web",
    "timestamp": "2023-06-12 21:40:28.690000000",
    "type": "user.settings.login_method.email_updated",
    "workspace_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  },
  "p_event_time": "2023-06-12 21:40:28.690000000",
  "p_log_type": "Notion.AuditLogs",
  "p_parse_time": "2023-06-12 22:53:51.602223297",
  "p_row_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "p_schema_version": 0,
  "p_source_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "p_source_label": "Notion Logs"
}

Signal - Notion Login

#
Severity
informational
Log types
Notion.AuditLogs
Tags
Notion, Identity & Access Management
Reference
www.notion.so
Source
github.com/panther-labs/panther-analysis

A Notion User logged in.

Detection logic

from panther_notion_helpers import notion_alert_context


def rule(event):

    if event.deep_walk("event", "type") == "user.login":
        return True
    return False


def title(event):
    user_email = event.deep_walk("event", "actor", "person", "email", default="UNKNOWN EMAIL")
    return f"Notion User [{user_email}] logged in."


def alert_context(event):
    context = notion_alert_context(event)
    context["login_timestamp"] = event.get("p_event_time")
    context["actor_id"] = event.deep_walk("event", "actor", "id")
    return context

Rule specification

AnalysisType: rule
Filename: notion_login.py
RuleID: "Notion.Login"
DisplayName: "Signal - Notion Login"
Enabled: true
CreateAlert: false
LogTypes:
  - Notion.AuditLogs
Tags:
  - Notion
  - Identity & Access Management
Severity: Info
Description: A Notion User logged in.
DedupPeriodMinutes: 60
Threshold: 1
Reference: https://www.notion.so/help/account-settings

Stages and Predicates

Fires on Notion.AuditLogs events when the condition below holds.

Condition

  • event.type is user.login

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.

FieldSource
emailevent.actor.person.email

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "event": {
    "actor": {
      "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "object": "user",
      "person": {
        "email": "aragorn.elessar@lotr.com"
      },
      "type": "person"
    },
    "details": {
      "authType": "email"
    },
    "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "ip_address": "192.168.100.100",
    "platform": "web",
    "timestamp": "2023-06-12 21:40:28.690000000",
    "type": "user.login",
    "workspace_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  },
  "p_event_time": "2023-06-12 21:40:28.690000000",
  "p_log_type": "Notion.AuditLogs",
  "p_parse_time": "2023-06-12 22:53:51.602223297",
  "p_row_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "p_schema_version": 0,
  "p_source_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "p_source_label": "Notion Logs"
}