Notion Login from New Location


Description

A Notion User logged in from a new location.

Query · python

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
    )

Analyst notes

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

Raw source Notion Login from New Location · Panther Python
Esc
Published by panther-labs/panther-analysis ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
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
Tests:
  - Name: Login from normal location
    ExpectedResult: false
    Mocks:
      - objectName: get_dictionary
        returnValue: '{ "Minas Tirith_Pellenor_Gondor": 1686542031 }'
      - objectName: put_dictionary
        returnValue: False
    Log:
      {
        "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": "Minas Tirith",
                    "lat": "0.00000",
                    "lng": "0.00000",
                    "country": "Gondor",
                    "postal_code": "55555",
                    "region": "Pellenor",
                    "region_code": "PL",
                    "timezone": "Middle Earth/Pellenor",
                  },
              },
          },
        "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",
      }
  - Name: No previous recorded login
    ExpectedResult: false
    Mocks:
      - objectName: get_dictionary
        returnValue: ""
      - objectName: put_dictionary
        returnValue: False
    Log:
      {
        "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": "Minas Tirith",
                    "lat": "0.00000",
                    "lng": "0.00000",
                    "country": "Gondor",
                    "postal_code": "55555",
                    "region": "Pellenor",
                    "region_code": "PL",
                    "timezone": "Middle Earth/Pellenor",
                  },
              },
          },
        "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",
      }
  - Name: Login from different location
    ExpectedResult: true
    Mocks:
      - objectName: get_dictionary
        returnValue: '{ "Minas Tirith_Pellenor_Gondor": 1686542031 }'
      - objectName: put_dictionary
        returnValue: False
    Log:
      {
        "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",
                    "lat": "0.00000",
                    "lng": "0.00000",
                    "country": "Mordor",
                    "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",
      }
  - Name: Missing enrichment
    ExpectedResult: false
    Log:
      {
        "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": {},
        "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",
      }
  - Name: Unrelated event
    ExpectedResult: false
    Log:
      {
        "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": "page.viewed",
            "workspace_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
          },
        "p_enrichment":
          {
            "ipinfo_location":
              {
                "event.ip_address":
                  {
                    "city": "Barad-Dur",
                    "lat": "0.00000",
                    "lng": "0.00000",
                    "country": "Mordor",
                    "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",
      }
  - Name: Login from different location - no region
    ExpectedResult: true
    Mocks:
      - objectName: get_dictionary
        returnValue: '{ "Minas Tirith_Pellenor_Gondor": 1686542031 }'
      - objectName: put_dictionary
        returnValue: False
    Log:
      {
        "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",
                    "lat": "0.00000",
                    "lng": "0.00000",
                    "country": "Mordor",
                    "postal_code": "55555",
                    "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",
      }


# ------ paired body: notion_login_from_new_location.py ------

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
    )

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.