コード例 #1
0
def rule(event):
    # Pre-filter to save compute time where possible. event_type_id = 5 is login events.
    if event.get('event_type_id') != 5 or event.get('ipaddr') is None:
        return False

    # Lookup geo-ip data via API call
    url = 'https://ipinfo.io/' + event['ipaddr'] + '/geo'

    # Skip API call if this is a unit test
    if 'panther_api_data' in event:
        resp = lambda: None
        setattr(resp, 'status_code', 200)
        setattr(resp, 'text', event['panther_api_data'])
    else:
        # This response looks like the following:
        # {‘ip': '8.8.8.8', 'city': 'Mountain View', 'region': 'California', 'country': 'US',
        # 'loc': '37.4056,-122.0775', 'postal': '94043', 'timezone': 'America/Los_Angeles'}
        resp = requests.get(url)

    if resp.status_code != 200:
        # Could raise an exception here for ops team to look into
        return False
    login_info = json.loads(resp.text)
    # The idea is to create a fingerprint of this login, and then keep track of all the fingerprints
    # for a given user's logins. In this way, we can detect unusual logins.
    login_tuple = login_info.get('region', '<REGION>') + ":" + login_info.get(
        'city', '<CITY>')
    EVENT_LOGIN_INFO[event['p_row_id']] = login_tuple

    # Lookup & store persistent data
    event_key = get_key(event)
    last_login_info = get_string_set(event_key)
    if not last_login_info:
        # Store this as the first login if we've never seen this user login before
        put_string_set(event_key, [json.dumps({login_tuple: 1})])
        return False
    last_login_info = json.loads(last_login_info.pop())

    last_login_info[login_tuple] = last_login_info.get(login_tuple, 0) + 1
    put_string_set(event_key, [json.dumps(last_login_info)])

    # Here we are checking if this login's fingerprint is one of the top three most common
    # fingerprints for this user. If it is not, we fire an alert.
    tuple_count = last_login_info[login_tuple]
    higher_tuples = 0
    for tcount in last_login_info.values():
        if tcount > tuple_count:
            higher_tuples += 1
        if higher_tuples >= FINGERPRINT_THRESHOLD:
            return True

    return False
コード例 #2
0
def rule(event):
    if event.udm("event_type") != event_type.ACCOUNT_CREATED:
        return False
    account_id = parse_new_account_id(event)
    event_time = resolve_timestamp_string(event.get("p_event_time"))
    expiry_time = event_time + TTL
    account_event_id = f"new_aws_account_{event.get('p_row_id')}"

    if account_id:
        put_string_set("new_account - " + account_id, [account_event_id],
                       expiry_time.strftime("%s"))

    return True
コード例 #3
0
def rule(event):
    if event.get("action").startswith(
            "git.") and not event.get("repository_public"):
        # if the actor field is empty, short circuit the rule
        if not event.udm("actor_user"):
            return False
        # otherwise trigger on any of the git actions, http or ssh
        key = get_key(event)
        previous_access = get_string_set(key)
        if not previous_access:
            put_string_set(key, key)
            return True
    return False
コード例 #4
0
def rule(event):
    if event.udm("event_type") != event_type.USER_ACCOUNT_CREATED:
        return False

    user_event_id = f"new_user_{event.get('p_row_id')}"
    new_user = event.udm("user")
    new_account = event.udm("user_account_id") or "<UNKNOWN_ACCOUNT>"
    event_time = resolve_timestamp_string(event.get("p_event_time"))
    expiry_time = event_time + TTL

    if new_user:
        put_string_set(new_user + "-" + str(new_account), [user_event_id],
                       expiry_time.strftime("%s"))
    return True
コード例 #5
0
def rule(event):
    # if the actor field is empty, short circuit the rule
    if not event.udm("actor_user"):
        return False

    if event.get("action") in CODE_ACCESS_ACTIONS and not event.get(
            "repository_public"):

        # Compute unique entry for this user + repo
        key = get_key(event)
        previous_access = get_string_set(key)
        if not previous_access:
            put_string_set(key, key)
            return True
    return False
コード例 #6
0
def store_login_info(key, event):
    # Map the user to the lon/lat and time of the most recent login
    put_string_set(key, [
        dumps({
            'lon':
            event['client']['geographicalContext']['geolocation']['lon'],
            'lat':
            event['client']['geographicalContext']['geolocation']['lat'],
            'time':
            event['p_event_time']
        })
    ])
    # Expire the entry after a week so the table doesn't fill up with past users
    set_key_expiration(key,
                       str((datetime.now() + timedelta(days=7)).timestamp()))
コード例 #7
0
def store_login_info(key, event):
    # Map the user to the lon/lat and time of the most recent login
    put_string_set(
        key,
        [
            dumps({
                "city":
                deep_get(event, "client", "geographicalContext", "city"),
                "lon":
                deep_get(event, "client", "geographicalContext", "geolocation",
                         "lon"),
                "lat":
                deep_get(event, "client", "geographicalContext", "geolocation",
                         "lat"),
                "time":
                event.get("p_event_time"),
            })
        ],
    )
    # Expire the entry after a week so the table doesn't fill up with past users
    set_key_expiration(key,
                       str((datetime.now() + timedelta(days=7)).timestamp()))
コード例 #8
0
def rule(event):
    # Pre-filter to save compute time where possible.
    if event.udm("event_type") != event_type.SUCCESSFUL_LOGIN:
        return False

    # we use udm 'actor_user' field as a ddb and 'source_ip' in the api call
    if not event.udm("actor_user") or not event.udm("source_ip"):
        return False

    # Lookup geo-ip data via API call
    url = "https://ipinfo.io/" + event.udm("source_ip") + "/geo"

    # Skip API call if this is a unit test
    if "panther_api_data" in event:
        resp = lambda: None
        setattr(resp, "status_code", 200)
        setattr(resp, "text", event.get("panther_api_data"))
    else:
        # This response looks like the following:
        # {‘ip': '8.8.8.8', 'city': 'Mountain View', 'region': 'California', 'country': 'US',
        # 'loc': '37.4056,-122.0775', 'postal': '94043', 'timezone': 'America/Los_Angeles'}
        resp = requests.get(url)

    if resp.status_code != 200:
        raise Exception(
            f"API call failed: GET {url} returned {resp.status_code}")
    login_info = json.loads(resp.text)
    # The idea is to create a fingerprint of this login, and then keep track of all the fingerprints
    # for a given user's logins. In this way, we can detect unusual logins.
    login_tuple = login_info.get("region", "<REGION>") + ":" + login_info.get(
        "city", "<CITY>")
    EVENT_LOGIN_INFO[event.get("p_row_id")] = login_tuple

    # Lookup & store persistent data
    event_key = get_key(event)
    last_login_info = get_string_set(event_key)
    fingerprint_timestamp = str(datetime.datetime.now())
    if not last_login_info:
        # Store this as the first login if we've never seen this user login before
        put_string_set(event_key,
                       [json.dumps({login_tuple: fingerprint_timestamp})])
        return False
    last_login_info = json.loads(last_login_info.pop())

    # update the timestamp associated with this fingerprint
    last_login_info[login_tuple] = fingerprint_timestamp
    put_string_set(event_key, [json.dumps(last_login_info)])

    # fire an alert when number of unique, recent fingerprints is greater than a threshold
    if len(last_login_info) > FINGERPRINT_THRESHOLD:
        oldest = login_tuple
        for fp_tuple, fp_time in last_login_info.items():
            if fp_time < oldest:
                oldest = fp_tuple
        # remove oldest login tuple
        last_login_info.pop(oldest)
        put_string_set(event_key, [json.dumps(last_login_info)])
        return True
    return False
コード例 #9
0
def rule(event):
    # Pre-filter: event_type_id = 5 is login events.
    if event.get('event_type_id') != 5 or not event.get(
            'ipaddr') or not event.get('user_id'):
        return False
    # We expect to see multiple user logins from these shared, common ip addresses
    if is_ip_in_network(event.get('ipaddr'), SHARED_IP_SPACE):
        return False
    # This tracks multiple successful logins for different accounts from the same ip address
    # First, keep a list of unique user ids that have logged in from this ip address
    event_key = get_key(event)
    user_ids = get_string_set(event_key)
    # the user id of the user that has just logged in
    user_id = str(event.get('user_id'))
    if not user_ids:
        # store this as the first user login from this ip address
        put_string_set(event_key, [user_id])
        set_key_expiration(event_key, int(time.time()) + THRESH_TTL)
        return False
    # add a new username if this is a unique user from this ip address
    if user_id not in user_ids:
        user_ids = add_to_string_set(event_key, user_id)
        set_key_expiration(event_key, int(time.time()) + THRESH_TTL)
    return len(user_ids) > THRESH
コード例 #10
0
def rule(event):
    # unique key for global dictionary
    log = event.get("p_row_id")

    # GEO_INFO is mocked as a string in unit tests and redeclared as a dict
    global GEO_INFO  # pylint: disable=global-statement
    # Pre-filter to save compute time where possible.
    if event.udm("event_type") != event_type.SUCCESSFUL_LOGIN:
        return False

    # we use udm 'actor_user' field as a ddb and 'source_ip' in the api call
    if not event.udm("actor_user") or not event.udm("source_ip"):
        return False

    # Lookup geo-ip data via API call
    # Mocked during unit testing
    GEO_INFO[log] = geoinfo_from_ip(event.udm("source_ip"))

    # As of Panther 1.19, mocking returns all mocked objects in a string
    # GEO_INFO must be converted back to a dict to mimic the API call
    if isinstance(GEO_INFO[log], str):
        GEO_INFO[log] = json.loads(GEO_INFO[log])

    # Look up history of unique geolocations
    event_key = get_key(event)
    # Mocked during unit testing
    previous_geo_logins = get_string_set(event_key)

    # As of Panther 1.19, mocking returns all mocked objects in a string
    # previous_geo_logins must be converted back to a set to mimic the API call
    if isinstance(previous_geo_logins, str):
        logging.debug("previous_geo_logins is a mocked string:")
        logging.debug(previous_geo_logins)
        if previous_geo_logins:
            previous_geo_logins = ast.literal_eval(previous_geo_logins)
        else:
            previous_geo_logins = set()
        logging.debug("new type of previous_geo_logins should be 'set':")
        logging.debug(type(previous_geo_logins))

    new_login_geo = (f"{GEO_INFO[log].get('region', '<UNKNOWN_REGION>')}"
                     ":"
                     f"{GEO_INFO[log].get('city', '<UNKNOWN_CITY>')}")
    new_login_timestamp = event.get("p_event_time", "")

    # convert set of single string to dictionary
    if previous_geo_logins:
        previous_geo_logins = json.loads(previous_geo_logins.pop())
    else:
        previous_geo_logins = {}
    logging.debug("new type of previous_geo_logins should be 'dict':")
    logging.debug(type(previous_geo_logins))

    # don't alert if the geo is already in the history
    if previous_geo_logins.get(new_login_geo):
        # update timestamp of the existing geo in the history
        previous_geo_logins[new_login_geo] = new_login_timestamp

        # write the dictionary of geolocs:timestamps back to Dynamo
        # Mocked during unit testing
        put_string_set(event_key, [json.dumps(previous_geo_logins)])
        return False

    # fire an alert when there are more unique geolocs:timestamps in the login history
    # add a new geo to the dictionary
    updated_geo_logins = previous_geo_logins
    updated_geo_logins[new_login_geo] = new_login_timestamp

    # remove the oldest geo from the history if the updated dict exceeds the
    # specified history length
    if len(updated_geo_logins) > GEO_HISTORY_LENGTH:
        oldest = updated_geo_logins[new_login_geo]
        for geo, time in updated_geo_logins.items():
            if time < oldest:
                oldest = time
                oldest_login = geo
        logging.debug("updated_geo_logins before removing oldest entry:")
        logging.debug(updated_geo_logins)
        updated_geo_logins.pop(oldest_login)
        logging.debug("updated_geo_logins after removing oldest entry:")
        logging.debug(updated_geo_logins)

    # Mocked during unit testing
    put_string_set(event_key, [json.dumps(updated_geo_logins)])

    global GEO_HISTORY  # pylint: disable=global-statement
    GEO_HISTORY[log] = updated_geo_logins
    logging.debug("GEO_HISTORY in main rule:\n%s",
                  json.dumps(GEO_HISTORY[log]))
    return True