SentinelOneEventCollector

This integration fetches activities, threats, and alerts from SentinelOne.

Analytics & SIEM · SentinelOne

Details

IDSentinelOneEventCollector
ProviderSentinelOne
CategoryAnalytics & SIEM
From Version6.8.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

This integration fetches activities, threats, and alerts from SentinelOne.
This integration was integrated and tested with API version 2.1 of SentinelOne.

This is the default integration for this content pack when configured by the Data Onboarder in Cortex XSIAM.

Configure SentinelOne Activity and Alerts in Cortex

Parameter Description Required
Server URL (e.g., https://usea1.sentinelone.net) The URL to use for connection True
API Token The API Token to use for connection True
Trust any certificate (not secure) Use SSL secure connection or not. False
Use system proxy settings Use proxy settings for connection or not. False
Fetch events Whether to bring events or not. False
Event types What types of events to bring, Possible values are (activities, threats, alerts) False
First fetch time First fetch query <number> <time unit>, e.g., 7 days. Default 3 days) False
The maximum number of events per fetch should be between 1-1000 The limit is per event type. For example, if you choose 3 event types (ACTIVITIES, THREATS, ALERTS) with a limit of 100, the actually limit will be 300. False

Commands

You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.

sentinelone-get-events


Gets events from SentinelOne.

Base Command

sentinelone-get-events

Input

Argument Name Description Required
should_push_events If true, the command will create events, otherwise it will only display them. Possible values are: true, false. Default is false. Optional
limit Maximum number of results to return. Value should be between 1 - 1000. Default is 1000. Optional

Context Output

There is no context output for this command.

Configuration parameters

  • url — Server URL (e.g., https://usea1.sentinelone.net) (required)
  • credentials — (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • isFetch — Fetch events
  • event_type — Event types
  • first_fetch — First fetch time
  • fetch_limit — The maximum number of events per fetch should be between 1-1000

Commands (1)

  • sentinelone-get-events

    Gets events from SentinelOne.

from CommonServerPython import *
import demistomock as demisto
from freezegun import freeze_time
import requests_mock

""" CONSTANTS """

ACTIVITIES_MOCK_URL = (
    "https://test.com/web/api/v2.1/activities?createdAt__gt=2022-01-04+00%3A00%3A00&limit=1000&sortBy=createdAt&sortOrder=asc"  # noqa: E501
)
ACTIVITIES_SECOND_MOCK_URL = "https://test.com/web/api/v2.1/activities?createdAt__gt=2022-09-06T20%3A37%3A55.912951Z&limit=1000&sortBy=createdAt&sortOrder=asc"  # noqa: E501
THREATS_MOCK_URL = (
    "https://test.com/web/api/v2.1/threats?createdAt__gt=2022-01-04+00%3A00%3A00&limit=1000&sortBy=createdAt&sortOrder=asc"  # noqa: E501
)
THREATS_SECOND_MOCK_URL = "https://test.com/web/api/v2.1/threats?createdAt__gt=2022-12-20T15%3A51%3A17.514437Z&limit=1000&sortBy=createdAt&sortOrder=asc"  # noqa: E501
ALERTS_MOCK_URL = "https://test.com/web/api/v2.1/cloud-detection/alerts?limit=1000&createdAt__gt=2022-01-04+00%3A00%3A00&sortBy=alertInfoCreatedAt&sortOrder=asc"  # noqa: E501
ALERTS_SECOND_MOCK_URL = "https://test.com/web/api/v2.1/cloud-detection/alerts?limit=1000&createdAt__gt=2022-12-20T13%3A54%3A43.027000Z&sortBy=alertInfoCreatedAt&sortOrder=asc"  # noqa: E501

""" HELPER FUNCTIONS """


def util_load_json(path):
    with open(path, encoding="utf-8") as f:
        return json.loads(f.read())


def mock_send_events_to_xsiam(events, vendor, product):
    return events, vendor, product


""" TEST FUNCTIONS """


@freeze_time("2022-01-07 00:00:00Z")
def test_test_module():
    from SentinelOneEventCollector import Client, test_module

    client = Client("https://test.com/web/api/v2.1")

    with requests_mock.Mocker() as m:
        m.get(ACTIVITIES_MOCK_URL, json={})
        m.get(THREATS_MOCK_URL, json={})
        m.get(ALERTS_MOCK_URL, json={})

        result = test_module(client, ["activities", "threats", "alerts"])

    assert result == "ok"


@freeze_time("2022-01-07 00:00:00Z")
def test_get_events_command():
    """
    Tests get-events command function.

        Given:
            - Few events in SentinelOne.

        When:
            - Running the 'get_events_command'.

        Then:
            - Ensures the outputs of the command function match the expected outputs.

    """
    from SentinelOneEventCollector import Client, get_events_command

    client = Client(base_url="https://test.com/web/api/v2.1")

    with requests_mock.Mocker() as m:
        m.get(ACTIVITIES_MOCK_URL, json=util_load_json("test_data/activities.json"))
        m.get(THREATS_MOCK_URL, json=util_load_json("test_data/threats.json"))
        m.get(ALERTS_MOCK_URL, json=util_load_json("test_data/alerts.json"))

        events, _ = get_events_command(client, str(arg_to_datetime("3 days")), ["activities", "threats", "alerts"])

    assert len(events) == 6


@freeze_time("2022-01-07 00:00:00Z")
def test_fetch_events():
    """
    Tests fetch-events command function.

        Given:
            - Few events in SentinelOne.
            - fetch-events is limited to 2 events per fetch and to all 3 event types.
             (which mean actually 6 events per fetch since we are fetching from 3 endpoints).
        When:
            - Running the 'fetch-events' command.

        Then:
            - Ensures the outputs of the command function match the expected outputs.

    """
    from SentinelOneEventCollector import Client, fetch_events, first_run

    client = Client(base_url="https://test.com/web/api/v2.1")
    last_run = first_run(arg_to_datetime("3 days"))

    with requests_mock.Mocker() as m:
        m.get(ACTIVITIES_MOCK_URL, json=util_load_json("test_data/activities.json"))
        m.get(THREATS_MOCK_URL, json=util_load_json("test_data/threats.json"))
        m.get(ALERTS_MOCK_URL, json=util_load_json("test_data/alerts.json"))
        m.get(ACTIVITIES_SECOND_MOCK_URL, json=util_load_json("test_data/activities_second_fetch.json"))
        m.get(THREATS_SECOND_MOCK_URL, json={})
        m.get(ALERTS_SECOND_MOCK_URL, json={})

        next_run, events = fetch_events(client, last_run, ["activities", "threats", "alerts"])

        assert next_run.get("last_activity_created") == "2022-09-06T20:37:55.912951Z"
        assert next_run.get("last_alert_created") == "2022-12-20T13:54:43.027000Z"
        assert len(events) == 6

        next_run, events = fetch_events(client, last_run, ["activities", "threats", "alerts"])

        assert next_run.get("last_activity_created") == "2022-09-06T20:39:15.445218Z"
        assert next_run.get("last_alert_created") == "2022-12-20T13:54:43.027000Z"
        assert len(events) == 2


@freeze_time("2022-01-07 00:00:00Z")
def test_main(mocker):
    """
    Tests the main function. (the whole flow).

        Given:
            - Few events in SentinelOne.

        When:
            - Running the 'main' function.

        Then:
            - Ensures the outputs of the main function match the expected outputs.

    """
    from SentinelOneEventCollector import main, VENDOR, PRODUCT

    mocker.patch.object(demisto, "command", return_value="sentinelone-get-events")
    mocker.patch.object(demisto, "params", return_value={"url": "https://test.com", "fetch_limit": 2})
    mocker.patch.object(demisto, "args", return_value={"should_push_events": True})
    events = mocker.patch("SentinelOneEventCollector.send_events_to_xsiam", side_effect=mock_send_events_to_xsiam)

    with requests_mock.Mocker() as m:
        m.get(ACTIVITIES_MOCK_URL.replace("limit=1000", "limit=2"), json=util_load_json("test_data/activities.json"))
        m.get(THREATS_MOCK_URL.replace("limit=1000", "limit=2"), json=util_load_json("test_data/threats.json"))
        m.get(ALERTS_MOCK_URL.replace("limit=1000", "limit=2"), json=util_load_json("test_data/alerts.json"))

        main()

    assert len(events.call_args[0][0]) == 6
    assert events.call_args[0][0][0].get("_time") == events.call_args[0][0][0].get("updatedAt")
    assert events.call_args[1].get("vendor") == VENDOR
    assert events.call_args[1].get("product") == PRODUCT


def test_add_keys_to_events_with_external_url(mocker):
    """
    Tests add_keys_to_events function with _EXTERNAL_URL field construction.

        Given:
            - Events from SentinelOne (threats, alerts, and activities).
            - An instance URL for the SentinelOne instance.

        When:
            - Calling the 'add_keys_to_events' function with instance_url parameter.

        Then:
            - Ensure _EXTERNAL_URL field is correctly constructed for each event type:
              - Threats: {instance_url}/incidents/threats/{threat_id}/overview
              - Alerts: {instance_url}/incidents/alerts/{alert_id}/overview
              - Activities: {instance_url}/activity

    """
    from SentinelOneEventCollector import add_keys_to_events

    mocker.patch.object(demisto, "debug")
    instance_url = "https://example-instance.com"

    # Test data
    threat_event = {"threatInfo": {"threatId": "123456", "updatedAt": "2022-12-20T15:51:17.514437Z"}}

    alert_event = {"alertInfo": {"alertId": "789012", "updatedAt": "2022-12-20T13:54:43.027000Z"}}

    activity_event = {"updatedAt": "2022-09-06T20:37:55.912951Z"}

    events = [threat_event, alert_event, activity_event]

    add_keys_to_events(events, instance_url)

    # Assertions for Threat
    assert threat_event.get("eventType") == "Threat"
    assert threat_event.get("_EXTERNAL_URL") == f"{instance_url}/incidents/threats/123456/overview"
    assert threat_event.get("_time") == "2022-12-20T15:51:17.514437Z"

    # Assertions for Alert
    assert alert_event.get("eventType") == "Alert"
    assert alert_event.get("_EXTERNAL_URL") == f"{instance_url}/incidents/alerts/789012/overview"
    assert alert_event.get("_time") == "2022-12-20T13:54:43.027000Z"

    # Assertions for Activity
    assert activity_event.get("eventType") == "Activity"
    assert activity_event.get("_EXTERNAL_URL") == f"{instance_url}/activity"
    assert activity_event.get("_time") == "2022-09-06T20:37:55.912951Z"


def test_add_keys_to_events_missing_ids(mocker):
    """
    Tests add_keys_to_events function when alert/threat IDs are missing.

        Given:
            - Events from SentinelOne with missing alertId or threatId.
            - An instance URL for the SentinelOne instance.

        When:
            - Calling the 'add_keys_to_events' function.

        Then:
            - Ensure _EXTERNAL_URL field is NOT added when IDs are missing.
            - Ensure debug messages are logged for missing IDs.
            - Ensure other fields (_time, eventType) are still added correctly.

    """
    from SentinelOneEventCollector import add_keys_to_events

    instance_url = "https://example-instance.com"
    mock_debug = mocker.patch.object(demisto, "debug")

    # Test data - threat without threatId
    threat_event = {"threatInfo": {"updatedAt": "2022-12-20T15:51:17.514437Z"}}

    # Test data - alert without alertId
    alert_event = {"alertInfo": {"updatedAt": "2022-12-20T13:54:43.027000Z"}}

    events = [threat_event, alert_event]

    add_keys_to_events(events, instance_url)

    # Assertions for Threat
    assert threat_event.get("eventType") == "Threat"
    assert threat_event.get("_time") == "2022-12-20T15:51:17.514437Z"
    assert "_EXTERNAL_URL" not in threat_event

    # Assertions for Alert
    assert alert_event.get("eventType") == "Alert"
    assert alert_event.get("_time") == "2022-12-20T13:54:43.027000Z"
    assert "_EXTERNAL_URL" not in alert_event

    # Verify debug messages were called (1 for the count log + 2 for missing IDs)
    assert mock_debug.call_count == 3